fune/gfx/qcms/src/iccread.rs
Stanca Serban aaea911dc4 Backed out 9 changesets (bug 1799258) for causing multiple failures. CLOSED TREE
Backed out changeset 40351b5987a5 (bug 1799258)
Backed out changeset 87f3532bfbcd (bug 1799258)
Backed out changeset 9c1d9405e8bf (bug 1799258)
Backed out changeset 60a0351d9092 (bug 1799258)
Backed out changeset 5f911de66ec0 (bug 1799258)
Backed out changeset 294a00d1c7b7 (bug 1799258)
Backed out changeset 228200dcaf93 (bug 1799258)
Backed out changeset b25110652394 (bug 1799258)
Backed out changeset 3c3c7366cc40 (bug 1799258)
2023-02-15 12:18:44 +02:00

1716 lines
62 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// qcms
// Copyright (C) 2009 Mozilla Foundation
// Copyright (C) 1998-2007 Marti Maria
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the Software
// is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
use std::{
convert::{TryInto, TryFrom},
sync::atomic::AtomicBool,
sync::Arc,
};
use crate::{
double_to_s15Fixed16Number,
transform::{set_rgb_colorants, PrecacheOuput},
};
use crate::{matrix::Matrix, s15Fixed16Number, s15Fixed16Number_to_float, Intent, Intent::*};
pub static SUPPORTS_ICCV4: AtomicBool = AtomicBool::new(cfg!(feature = "iccv4-enabled"));
pub const RGB_SIGNATURE: u32 = 0x52474220;
pub const GRAY_SIGNATURE: u32 = 0x47524159;
pub const XYZ_SIGNATURE: u32 = 0x58595A20;
pub const LAB_SIGNATURE: u32 = 0x4C616220;
pub const CMYK_SIGNATURE: u32 = 0x434D594B; // 'CMYK'
/// A color profile
#[derive(Default, Debug)]
pub struct Profile {
pub(crate) class_type: u32,
pub(crate) color_space: u32,
pub(crate) pcs: u32,
pub(crate) rendering_intent: Intent,
pub(crate) redColorant: XYZNumber,
pub(crate) blueColorant: XYZNumber,
pub(crate) greenColorant: XYZNumber,
pub(crate) redTRC: Option<Box<curveType>>,
pub(crate) blueTRC: Option<Box<curveType>>,
pub(crate) greenTRC: Option<Box<curveType>>,
pub(crate) grayTRC: Option<Box<curveType>>,
pub(crate) A2B0: Option<Box<lutType>>,
pub(crate) B2A0: Option<Box<lutType>>,
pub(crate) mAB: Option<Box<lutmABType>>,
pub(crate) mBA: Option<Box<lutmABType>>,
pub(crate) chromaticAdaption: Option<Matrix>,
pub(crate) output_table_r: Option<Arc<PrecacheOuput>>,
pub(crate) output_table_g: Option<Arc<PrecacheOuput>>,
pub(crate) output_table_b: Option<Arc<PrecacheOuput>>,
is_srgb: bool,
}
#[derive(Debug, Default)]
#[allow(clippy::upper_case_acronyms)]
pub(crate) struct lutmABType {
pub num_in_channels: u8,
pub num_out_channels: u8,
// 16 is the upperbound, actual is 0..num_in_channels.
pub num_grid_points: [u8; 16],
pub e00: s15Fixed16Number,
pub e01: s15Fixed16Number,
pub e02: s15Fixed16Number,
pub e03: s15Fixed16Number,
pub e10: s15Fixed16Number,
pub e11: s15Fixed16Number,
pub e12: s15Fixed16Number,
pub e13: s15Fixed16Number,
pub e20: s15Fixed16Number,
pub e21: s15Fixed16Number,
pub e22: s15Fixed16Number,
pub e23: s15Fixed16Number,
// reversed elements (for mBA)
pub reversed: bool,
pub clut_table: Option<Vec<f32>>,
pub a_curves: [Option<Box<curveType>>; MAX_CHANNELS],
pub b_curves: [Option<Box<curveType>>; MAX_CHANNELS],
pub m_curves: [Option<Box<curveType>>; MAX_CHANNELS],
}
#[derive(Clone, Debug)]
pub(crate) enum curveType {
Curve(Vec<uInt16Number>),
/// The ICC parametricCurveType is specified in terms of s15Fixed16Number,
/// so it's possible to use this variant to specify greater precision than
/// any raw ICC profile could
Parametric(Vec<f32>),
}
type uInt16Number = u16;
/* should lut8Type and lut16Type be different types? */
#[derive(Debug)]
pub(crate) struct lutType {
// used by lut8Type/lut16Type (mft2) only
pub num_input_channels: u8,
pub num_output_channels: u8,
pub num_clut_grid_points: u8,
pub e00: s15Fixed16Number,
pub e01: s15Fixed16Number,
pub e02: s15Fixed16Number,
pub e10: s15Fixed16Number,
pub e11: s15Fixed16Number,
pub e12: s15Fixed16Number,
pub e20: s15Fixed16Number,
pub e21: s15Fixed16Number,
pub e22: s15Fixed16Number,
pub num_input_table_entries: u16,
pub num_output_table_entries: u16,
pub input_table: Vec<f32>,
pub clut_table: Vec<f32>,
pub output_table: Vec<f32>,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Default)]
#[allow(clippy::upper_case_acronyms)]
pub struct XYZNumber {
pub X: s15Fixed16Number,
pub Y: s15Fixed16Number,
pub Z: s15Fixed16Number,
}
/// A color in the CIE xyY color space
/* the names for the following two types are sort of ugly */
#[repr(C)]
#[derive(Copy, Clone)]
#[allow(clippy::upper_case_acronyms)]
pub struct qcms_CIE_xyY {
pub x: f64,
pub y: f64,
pub Y: f64,
}
/// A more convenient type for specifying primaries and white points where
/// luminosity is irrelevant
struct qcms_chromaticity {
x: f64,
y: f64,
}
impl qcms_chromaticity {
const D65: Self = Self {
x: 0.3127,
y: 0.3290,
};
}
impl From<qcms_chromaticity> for qcms_CIE_xyY {
fn from(qcms_chromaticity { x, y }: qcms_chromaticity) -> Self {
Self { x, y, Y: 1.0 }
}
}
/// a set of CIE_xyY values that can use to describe the primaries of a color space
#[repr(C)]
#[derive(Copy, Clone)]
#[allow(clippy::upper_case_acronyms)]
pub struct qcms_CIE_xyYTRIPLE {
pub red: qcms_CIE_xyY,
pub green: qcms_CIE_xyY,
pub blue: qcms_CIE_xyY,
}
struct Tag {
signature: u32,
offset: u32,
size: u32,
}
/* It might be worth having a unified limit on content controlled
* allocation per profile. This would remove the need for many
* of the arbitrary limits that we used */
type TagIndex = [Tag];
/* a wrapper around the memory that we are going to parse
* into a qcms_profile */
struct MemSource<'a> {
buf: &'a [u8],
valid: bool,
invalid_reason: Option<&'static str>,
}
pub type uInt8Number = u8;
#[inline]
fn uInt8Number_to_float(a: uInt8Number) -> f32 {
a as f32 / 255.0
}
#[inline]
fn uInt16Number_to_float(a: uInt16Number) -> f32 {
a as f32 / 65535.0
}
fn invalid_source(mut mem: &mut MemSource, reason: &'static str) {
mem.valid = false;
mem.invalid_reason = Some(reason);
}
fn read_u32(mem: &mut MemSource, offset: usize) -> u32 {
let val = mem.buf.get(offset..offset + 4);
if let Some(val) = val {
let val = val.try_into().unwrap();
u32::from_be_bytes(val)
} else {
invalid_source(mem, "Invalid offset");
0
}
}
fn read_u16(mem: &mut MemSource, offset: usize) -> u16 {
let val = mem.buf.get(offset..offset + 2);
if let Some(val) = val {
let val = val.try_into().unwrap();
u16::from_be_bytes(val)
} else {
invalid_source(mem, "Invalid offset");
0
}
}
fn read_u8(mem: &mut MemSource, offset: usize) -> u8 {
let val = mem.buf.get(offset);
if let Some(val) = val {
*val
} else {
invalid_source(mem, "Invalid offset");
0
}
}
fn read_s15Fixed16Number(mem: &mut MemSource, offset: usize) -> s15Fixed16Number {
read_u32(mem, offset) as s15Fixed16Number
}
fn read_uInt8Number(mem: &mut MemSource, offset: usize) -> uInt8Number {
read_u8(mem, offset)
}
fn read_uInt16Number(mem: &mut MemSource, offset: usize) -> uInt16Number {
read_u16(mem, offset)
}
pub fn write_u32(mem: &mut [u8], offset: usize, value: u32) {
// we use get() and expect() instead of [..] so there's only one call to panic
// instead of two
mem.get_mut(offset..offset + std::mem::size_of_val(&value))
.expect("OOB")
.copy_from_slice(&value.to_be_bytes());
}
pub fn write_u16(mem: &mut [u8], offset: usize, value: u16) {
// we use get() and expect() instead of [..] so there's only one call to panic
// intead of two
mem.get_mut(offset..offset + std::mem::size_of_val(&value))
.expect("OOB")
.copy_from_slice(&value.to_be_bytes());
}
/* An arbitrary 4MB limit on profile size */
pub(crate) const MAX_PROFILE_SIZE: usize = 1024 * 1024 * 4;
const MAX_TAG_COUNT: u32 = 1024;
fn check_CMM_type_signature(_src: &mut MemSource) {
//uint32_t CMM_type_signature = read_u32(src, 4);
//TODO: do the check?
}
fn check_profile_version(src: &mut MemSource) {
/*
uint8_t major_revision = read_u8(src, 8 + 0);
uint8_t minor_revision = read_u8(src, 8 + 1);
*/
let reserved1: u8 = read_u8(src, (8 + 2) as usize);
let reserved2: u8 = read_u8(src, (8 + 3) as usize);
/* Checking the version doesn't buy us anything
if (major_revision != 0x4) {
if (major_revision > 0x2)
invalid_source(src, "Unsupported major revision");
if (minor_revision > 0x40)
invalid_source(src, "Unsupported minor revision");
}
*/
if reserved1 != 0 || reserved2 != 0 {
invalid_source(src, "Invalid reserved bytes");
};
}
const INPUT_DEVICE_PROFILE: u32 = 0x73636e72; // 'scnr'
pub const DISPLAY_DEVICE_PROFILE: u32 = 0x6d6e7472; // 'mntr'
const OUTPUT_DEVICE_PROFILE: u32 = 0x70727472; // 'prtr'
const DEVICE_LINK_PROFILE: u32 = 0x6c696e6b; // 'link'
const COLOR_SPACE_PROFILE: u32 = 0x73706163; // 'spac'
const ABSTRACT_PROFILE: u32 = 0x61627374; // 'abst'
const NAMED_COLOR_PROFILE: u32 = 0x6e6d636c; // 'nmcl'
fn read_class_signature(mut profile: &mut Profile, mem: &mut MemSource) {
profile.class_type = read_u32(mem, 12);
match profile.class_type {
DISPLAY_DEVICE_PROFILE
| INPUT_DEVICE_PROFILE
| OUTPUT_DEVICE_PROFILE
| COLOR_SPACE_PROFILE => {}
_ => {
invalid_source(mem, "Invalid Profile/Device Class signature");
}
};
}
fn read_color_space(mut profile: &mut Profile, mem: &mut MemSource) {
profile.color_space = read_u32(mem, 16);
match profile.color_space {
RGB_SIGNATURE | GRAY_SIGNATURE => {}
#[cfg(feature = "cmyk")]
CMYK_SIGNATURE => {}
_ => {
invalid_source(mem, "Unsupported colorspace");
}
};
}
fn read_pcs(mut profile: &mut Profile, mem: &mut MemSource) {
profile.pcs = read_u32(mem, 20);
match profile.pcs {
XYZ_SIGNATURE | LAB_SIGNATURE => {}
_ => {
invalid_source(mem, "Unsupported pcs");
}
};
}
fn read_tag_table(_profile: &mut Profile, mem: &mut MemSource) -> Vec<Tag> {
let count = read_u32(mem, 128);
if count > MAX_TAG_COUNT {
invalid_source(mem, "max number of tags exceeded");
return Vec::new();
}
let mut index = Vec::with_capacity(count as usize);
for i in 0..count {
let tag_start = (128 + 4 + 4 * i * 3) as usize;
let offset = read_u32(mem, tag_start + 4);
if offset as usize > mem.buf.len() {
invalid_source(mem, "tag points beyond the end of the buffer");
}
index.push(Tag {
signature: read_u32(mem, tag_start),
offset,
size: read_u32(mem, tag_start + 8),
});
}
index
}
/// Checks a profile for obvious inconsistencies and returns
/// true if the profile looks bogus and should probably be
/// ignored.
#[no_mangle]
pub extern "C" fn qcms_profile_is_bogus(profile: &mut Profile) -> bool {
let mut sum: [f32; 3] = [0.; 3];
let mut target: [f32; 3] = [0.; 3];
let mut tolerance: [f32; 3] = [0.; 3];
let rX: f32;
let rY: f32;
let rZ: f32;
let gX: f32;
let gY: f32;
let gZ: f32;
let bX: f32;
let bY: f32;
let bZ: f32;
let negative: bool;
let mut i: u32;
// We currently only check the bogosity of RGB profiles
if profile.color_space != RGB_SIGNATURE {
return false;
}
if profile.A2B0.is_some()
|| profile.B2A0.is_some()
|| profile.mAB.is_some()
|| profile.mBA.is_some()
{
return false;
}
rX = s15Fixed16Number_to_float(profile.redColorant.X);
rY = s15Fixed16Number_to_float(profile.redColorant.Y);
rZ = s15Fixed16Number_to_float(profile.redColorant.Z);
gX = s15Fixed16Number_to_float(profile.greenColorant.X);
gY = s15Fixed16Number_to_float(profile.greenColorant.Y);
gZ = s15Fixed16Number_to_float(profile.greenColorant.Z);
bX = s15Fixed16Number_to_float(profile.blueColorant.X);
bY = s15Fixed16Number_to_float(profile.blueColorant.Y);
bZ = s15Fixed16Number_to_float(profile.blueColorant.Z);
// Sum the values; they should add up to something close to white
sum[0] = rX + gX + bX;
sum[1] = rY + gY + bY;
sum[2] = rZ + gZ + bZ;
// Build our target vector (see mozilla bug 460629)
target[0] = 0.96420;
target[1] = 1.00000;
target[2] = 0.82491;
// Our tolerance vector - Recommended by Chris Murphy based on
// conversion from the LAB space criterion of no more than 3 in any one
// channel. This is similar to, but slightly more tolerant than Adobe's
// criterion.
tolerance[0] = 0.02;
tolerance[1] = 0.02;
tolerance[2] = 0.04;
// Compare with our tolerance
i = 0;
while i < 3 {
if !(sum[i as usize] - tolerance[i as usize] <= target[i as usize]
&& sum[i as usize] + tolerance[i as usize] >= target[i as usize])
{
return true;
}
i += 1
}
if false {
negative = (rX < 0.)
|| (rY < 0.)
|| (rZ < 0.)
|| (gX < 0.)
|| (gY < 0.)
|| (gZ < 0.)
|| (bX < 0.)
|| (bY < 0.)
|| (bZ < 0.);
} else {
// Chromatic adaption to D50 can result in negative XYZ, but the white
// point D50 tolerance test has passed. Accept negative values herein.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=498245#c18 onwards
// for discussion about whether profile XYZ can or cannot be negative,
// per the spec. Also the https://bugzil.la/450923 user report.
// Also: https://bugzil.la/1799391 and https://bugzil.la/1792469
negative = false; // bogus
}
if negative {
return true;
}
// All Good
false
}
pub const TAG_bXYZ: u32 = 0x6258595a;
pub const TAG_gXYZ: u32 = 0x6758595a;
pub const TAG_rXYZ: u32 = 0x7258595a;
pub const TAG_rTRC: u32 = 0x72545243;
pub const TAG_bTRC: u32 = 0x62545243;
pub const TAG_gTRC: u32 = 0x67545243;
pub const TAG_kTRC: u32 = 0x6b545243;
pub const TAG_A2B0: u32 = 0x41324230;
pub const TAG_B2A0: u32 = 0x42324130;
pub const TAG_CHAD: u32 = 0x63686164;
fn find_tag(index: &TagIndex, tag_id: u32) -> Option<&Tag> {
for t in index {
if t.signature == tag_id {
return Some(t);
}
}
None
}
pub const XYZ_TYPE: u32 = 0x58595a20; // 'XYZ '
pub const CURVE_TYPE: u32 = 0x63757276; // 'curv'
pub const PARAMETRIC_CURVE_TYPE: u32 = 0x70617261; // 'para'
pub const LUT16_TYPE: u32 = 0x6d667432; // 'mft2'
pub const LUT8_TYPE: u32 = 0x6d667431; // 'mft1'
pub const LUT_MAB_TYPE: u32 = 0x6d414220; // 'mAB '
pub const LUT_MBA_TYPE: u32 = 0x6d424120; // 'mBA '
pub const CHROMATIC_TYPE: u32 = 0x73663332; // 'sf32'
fn read_tag_s15Fixed16ArrayType(src: &mut MemSource, tag: &Tag) -> Matrix {
let mut matrix: Matrix = Matrix { m: [[0.; 3]; 3] };
let offset: u32 = tag.offset;
let type_0: u32 = read_u32(src, offset as usize);
// Check mandatory type signature for s16Fixed16ArrayType
if type_0 != CHROMATIC_TYPE {
invalid_source(src, "unexpected type, expected \'sf32\'");
}
for i in 0..=8 {
matrix.m[(i / 3) as usize][(i % 3) as usize] = s15Fixed16Number_to_float(
read_s15Fixed16Number(src, (offset + 8 + (i * 4) as u32) as usize),
);
}
matrix
}
fn read_tag_XYZType(src: &mut MemSource, index: &TagIndex, tag_id: u32) -> XYZNumber {
let mut num = XYZNumber { X: 0, Y: 0, Z: 0 };
let tag = find_tag(&index, tag_id);
if let Some(tag) = tag {
let offset: u32 = tag.offset;
let type_0: u32 = read_u32(src, offset as usize);
if type_0 != XYZ_TYPE {
invalid_source(src, "unexpected type, expected XYZ");
}
num.X = read_s15Fixed16Number(src, (offset + 8) as usize);
num.Y = read_s15Fixed16Number(src, (offset + 12) as usize);
num.Z = read_s15Fixed16Number(src, (offset + 16) as usize)
} else {
invalid_source(src, "missing xyztag");
}
num
}
// Read the tag at a given offset rather then the tag_index.
// This method is used when reading mAB tags where nested curveType are
// present that are not part of the tag_index.
fn read_curveType(src: &mut MemSource, offset: u32, len: &mut u32) -> Option<Box<curveType>> {
const COUNT_TO_LENGTH: [u32; 5] = [1, 3, 4, 5, 7]; //PARAMETRIC_CURVE_TYPE
let type_0: u32 = read_u32(src, offset as usize);
let count: u32;
if type_0 != CURVE_TYPE && type_0 != PARAMETRIC_CURVE_TYPE {
invalid_source(src, "unexpected type, expected CURV or PARA");
return None;
}
if type_0 == CURVE_TYPE {
count = read_u32(src, (offset + 8) as usize);
//arbitrary
if count > 40000 {
invalid_source(src, "curve size too large");
return None;
}
let mut table = Vec::with_capacity(count as usize);
for i in 0..count {
table.push(read_u16(src, (offset + 12 + i * 2) as usize));
}
*len = 12 + count * 2;
Some(Box::new(curveType::Curve(table)))
} else {
count = read_u16(src, (offset + 8) as usize) as u32;
if count > 4 {
invalid_source(src, "parametric function type not supported.");
return None;
}
let mut params = Vec::with_capacity(count as usize);
for i in 0..COUNT_TO_LENGTH[count as usize] {
params.push(s15Fixed16Number_to_float(read_s15Fixed16Number(
src,
(offset + 12 + i * 4) as usize,
)));
}
*len = 12 + COUNT_TO_LENGTH[count as usize] * 4;
if count == 1 || count == 2 {
/* we have a type 1 or type 2 function that has a division by 'a' */
let a: f32 = params[1];
if a == 0.0 {
invalid_source(src, "parametricCurve definition causes division by zero");
}
}
Some(Box::new(curveType::Parametric(params)))
}
}
fn read_tag_curveType(
src: &mut MemSource,
index: &TagIndex,
tag_id: u32,
) -> Option<Box<curveType>> {
let tag = find_tag(index, tag_id);
if let Some(tag) = tag {
let mut len: u32 = 0;
return read_curveType(src, tag.offset, &mut len);
} else {
invalid_source(src, "missing curvetag");
}
None
}
const MAX_LUT_SIZE: u32 = 500000; // arbitrary
const MAX_CHANNELS: usize = 10; // arbitrary
fn read_nested_curveType(
src: &mut MemSource,
curveArray: &mut [Option<Box<curveType>>; MAX_CHANNELS],
num_channels: u8,
curve_offset: u32,
) {
let mut channel_offset: u32 = 0;
#[allow(clippy::needless_range_loop)]
for i in 0..usize::from(num_channels) {
let mut tag_len: u32 = 0;
curveArray[i] = read_curveType(src, curve_offset + channel_offset, &mut tag_len);
if curveArray[i].is_none() {
invalid_source(src, "invalid nested curveType curve");
break;
} else {
channel_offset += tag_len;
// 4 byte aligned
if tag_len % 4 != 0 {
channel_offset += 4 - tag_len % 4
}
}
}
}
/* See section 10.10 for specs */
fn read_tag_lutmABType(src: &mut MemSource, tag: &Tag) -> Option<Box<lutmABType>> {
let offset: u32 = tag.offset;
let mut clut_size: u32 = 1;
let type_0: u32 = read_u32(src, offset as usize);
if type_0 != LUT_MAB_TYPE && type_0 != LUT_MBA_TYPE {
return None;
}
let num_in_channels = read_u8(src, (offset + 8) as usize);
let num_out_channels = read_u8(src, (offset + 9) as usize);
if num_in_channels > 10 || num_out_channels > 10 {
return None;
}
// We require 3in/out channels since we only support RGB->XYZ (or RGB->LAB)
// XXX: If we remove this restriction make sure that the number of channels
// is less or equal to the maximum number of mAB curves in qcmsint.h
// also check for clut_size overflow. Also make sure it's != 0
if num_in_channels != 3 || num_out_channels != 3 {
return None;
}
// some of this data is optional and is denoted by a zero offset
// we also use this to track their existance
let mut a_curve_offset = read_u32(src, (offset + 28) as usize);
let mut clut_offset = read_u32(src, (offset + 24) as usize);
let mut m_curve_offset = read_u32(src, (offset + 20) as usize);
let mut matrix_offset = read_u32(src, (offset + 16) as usize);
let mut b_curve_offset = read_u32(src, (offset + 12) as usize);
// Convert offsets relative to the tag to relative to the profile
// preserve zero for optional fields
if a_curve_offset != 0 {
a_curve_offset += offset
}
if clut_offset != 0 {
clut_offset += offset
}
if m_curve_offset != 0 {
m_curve_offset += offset
}
if matrix_offset != 0 {
matrix_offset += offset
}
if b_curve_offset != 0 {
b_curve_offset += offset
}
if clut_offset != 0 {
debug_assert!(num_in_channels == 3);
// clut_size can not overflow since lg(256^num_in_channels) = 24 bits.
for i in 0..u32::from(num_in_channels) {
clut_size *= read_u8(src, (clut_offset + i) as usize) as u32;
if clut_size == 0 {
invalid_source(src, "bad clut_size");
}
}
} else {
clut_size = 0
}
// 24bits * 3 won't overflow either
clut_size *= num_out_channels as u32;
if clut_size > MAX_LUT_SIZE {
return None;
}
let mut lut = Box::new(lutmABType::default());
if clut_offset != 0 {
for i in 0..usize::from(num_in_channels) {
lut.num_grid_points[i] = read_u8(src, clut_offset as usize + i);
if lut.num_grid_points[i] == 0 {
invalid_source(src, "bad grid_points");
}
}
}
// Reverse the processing of transformation elements for mBA type.
lut.reversed = type_0 == LUT_MBA_TYPE;
lut.num_in_channels = num_in_channels;
lut.num_out_channels = num_out_channels;
#[allow(clippy::identity_op, clippy::erasing_op)]
if matrix_offset != 0 {
// read the matrix if we have it
lut.e00 = read_s15Fixed16Number(src, (matrix_offset + (4 * 0) as u32) as usize); // the caller checks that this doesn't happen
lut.e01 = read_s15Fixed16Number(src, (matrix_offset + (4 * 1) as u32) as usize);
lut.e02 = read_s15Fixed16Number(src, (matrix_offset + (4 * 2) as u32) as usize);
lut.e10 = read_s15Fixed16Number(src, (matrix_offset + (4 * 3) as u32) as usize);
lut.e11 = read_s15Fixed16Number(src, (matrix_offset + (4 * 4) as u32) as usize);
lut.e12 = read_s15Fixed16Number(src, (matrix_offset + (4 * 5) as u32) as usize);
lut.e20 = read_s15Fixed16Number(src, (matrix_offset + (4 * 6) as u32) as usize);
lut.e21 = read_s15Fixed16Number(src, (matrix_offset + (4 * 7) as u32) as usize);
lut.e22 = read_s15Fixed16Number(src, (matrix_offset + (4 * 8) as u32) as usize);
lut.e03 = read_s15Fixed16Number(src, (matrix_offset + (4 * 9) as u32) as usize);
lut.e13 = read_s15Fixed16Number(src, (matrix_offset + (4 * 10) as u32) as usize);
lut.e23 = read_s15Fixed16Number(src, (matrix_offset + (4 * 11) as u32) as usize)
}
if a_curve_offset != 0 {
read_nested_curveType(src, &mut lut.a_curves, num_in_channels, a_curve_offset);
}
if m_curve_offset != 0 {
read_nested_curveType(src, &mut lut.m_curves, num_out_channels, m_curve_offset);
}
if b_curve_offset != 0 {
read_nested_curveType(src, &mut lut.b_curves, num_out_channels, b_curve_offset);
} else {
invalid_source(src, "B curves required");
}
if clut_offset != 0 {
let clut_precision = read_u8(src, (clut_offset + 16) as usize);
let mut clut_table = Vec::with_capacity(clut_size as usize);
if clut_precision == 1 {
for i in 0..clut_size {
clut_table.push(uInt8Number_to_float(read_uInt8Number(
src,
(clut_offset + 20 + i) as usize,
)));
}
lut.clut_table = Some(clut_table);
} else if clut_precision == 2 {
for i in 0..clut_size {
clut_table.push(uInt16Number_to_float(read_uInt16Number(
src,
(clut_offset + 20 + i * 2) as usize,
)));
}
lut.clut_table = Some(clut_table);
} else {
invalid_source(src, "Invalid clut precision");
}
}
if !src.valid {
return None;
}
Some(lut)
}
fn read_tag_lutType(src: &mut MemSource, tag: &Tag) -> Option<Box<lutType>> {
let offset: u32 = tag.offset;
let type_0: u32 = read_u32(src, offset as usize);
let num_input_table_entries: u16;
let num_output_table_entries: u16;
let input_offset: u32;
let entry_size: usize;
if type_0 == LUT8_TYPE {
num_input_table_entries = 256u16;
num_output_table_entries = 256u16;
entry_size = 1;
input_offset = 48
} else if type_0 == LUT16_TYPE {
num_input_table_entries = read_u16(src, (offset + 48) as usize);
num_output_table_entries = read_u16(src, (offset + 50) as usize);
// these limits come from the spec
if !(2..=4096).contains(&num_input_table_entries)
|| !(2..=4096).contains(&num_output_table_entries)
{
invalid_source(src, "Bad channel count");
return None;
}
entry_size = 2;
input_offset = 52
} else {
debug_assert!(false);
invalid_source(src, "Unexpected lut type");
return None;
}
let in_chan = read_u8(src, (offset + 8) as usize);
let out_chan = read_u8(src, (offset + 9) as usize);
if !(in_chan == 3 || in_chan == 4) || out_chan != 3 {
invalid_source(src, "CLUT only supports RGB and CMYK");
return None;
}
let grid_points = read_u8(src, (offset + 10) as usize);
let clut_size = match (grid_points as u32).checked_pow(in_chan as u32) {
Some(clut_size) => clut_size,
_ => {
invalid_source(src, "CLUT size overflow");
return None;
}
};
match clut_size {
1..=MAX_LUT_SIZE => {} // OK
0 => {
invalid_source(src, "CLUT must not be empty.");
return None;
}
_ => {
invalid_source(src, "CLUT too large");
return None;
}
}
let e00 = read_s15Fixed16Number(src, (offset + 12) as usize);
let e01 = read_s15Fixed16Number(src, (offset + 16) as usize);
let e02 = read_s15Fixed16Number(src, (offset + 20) as usize);
let e10 = read_s15Fixed16Number(src, (offset + 24) as usize);
let e11 = read_s15Fixed16Number(src, (offset + 28) as usize);
let e12 = read_s15Fixed16Number(src, (offset + 32) as usize);
let e20 = read_s15Fixed16Number(src, (offset + 36) as usize);
let e21 = read_s15Fixed16Number(src, (offset + 40) as usize);
let e22 = read_s15Fixed16Number(src, (offset + 44) as usize);
let mut input_table = Vec::with_capacity((num_input_table_entries * in_chan as u16) as usize);
for i in 0..(num_input_table_entries * in_chan as u16) {
if type_0 == LUT8_TYPE {
input_table.push(uInt8Number_to_float(read_uInt8Number(
src,
(offset + input_offset) as usize + i as usize * entry_size,
)))
} else {
input_table.push(uInt16Number_to_float(read_uInt16Number(
src,
(offset + input_offset) as usize + i as usize * entry_size,
)))
}
}
let clut_offset = ((offset + input_offset) as usize
+ (num_input_table_entries as i32 * in_chan as i32) as usize * entry_size)
as u32;
let mut clut_table = Vec::with_capacity((clut_size * out_chan as u32) as usize);
for i in 0..clut_size * out_chan as u32 {
if type_0 == LUT8_TYPE {
clut_table.push(uInt8Number_to_float(read_uInt8Number(
src,
clut_offset as usize + i as usize * entry_size,
)));
} else if type_0 == LUT16_TYPE {
clut_table.push(uInt16Number_to_float(read_uInt16Number(
src,
clut_offset as usize + i as usize * entry_size,
)));
}
}
let output_offset =
(clut_offset as usize + (clut_size * out_chan as u32) as usize * entry_size) as u32;
let mut output_table =
Vec::with_capacity((num_output_table_entries * out_chan as u16) as usize);
for i in 0..num_output_table_entries as i32 * out_chan as i32 {
if type_0 == LUT8_TYPE {
output_table.push(uInt8Number_to_float(read_uInt8Number(
src,
output_offset as usize + i as usize * entry_size,
)))
} else {
output_table.push(uInt16Number_to_float(read_uInt16Number(
src,
output_offset as usize + i as usize * entry_size,
)))
}
}
Some(Box::new(lutType {
num_input_table_entries,
num_output_table_entries,
num_input_channels: in_chan,
num_output_channels: out_chan,
num_clut_grid_points: grid_points,
e00,
e01,
e02,
e10,
e11,
e12,
e20,
e21,
e22,
input_table,
clut_table,
output_table,
}))
}
fn read_rendering_intent(mut profile: &mut Profile, src: &mut MemSource) {
let intent = read_u32(src, 64);
profile.rendering_intent = match intent {
x if x == Perceptual as u32 => Perceptual,
x if x == RelativeColorimetric as u32 => RelativeColorimetric,
x if x == Saturation as u32 => Saturation,
x if x == AbsoluteColorimetric as u32 => AbsoluteColorimetric,
_ => {
invalid_source(src, "unknown rendering intent");
Intent::default()
}
};
}
fn profile_create() -> Box<Profile> {
Box::new(Profile::default())
}
/* build sRGB gamma table */
/* based on cmsBuildParametricGamma() */
#[allow(clippy::many_single_char_names)]
fn build_sRGB_gamma_table(num_entries: i32) -> Vec<u16> {
/* taken from lcms: Build_sRGBGamma() */
let gamma: f64 = 2.4;
let a: f64 = 1.0 / 1.055;
let b: f64 = 0.055 / 1.055;
let c: f64 = 1.0 / 12.92;
let d: f64 = 0.04045;
build_trc_table(
num_entries,
// IEC 61966-2.1 (sRGB)
// Y = (aX + b)^Gamma | X >= d
// Y = cX | X < d
|x| {
if x >= d {
let e: f64 = a * x + b;
if e > 0. {
e.powf(gamma)
} else {
0.
}
} else {
c * x
}
},
)
}
/// eotf: electro-optical transfer characteristic function, maps from [0, 1]
/// in non-linear (voltage) space to [0, 1] in linear (optical) space. Should
/// generally be a concave up function.
fn build_trc_table(num_entries: i32, eotf: impl Fn(f64) -> f64) -> Vec<u16> {
let mut table = Vec::with_capacity(num_entries as usize);
for i in 0..num_entries {
let x: f64 = i as f64 / (num_entries - 1) as f64;
let y: f64 = eotf(x);
let mut output: f64;
// Saturate -- this could likely move to a separate function
output = y * 65535.0 + 0.5;
if output > 65535.0 {
output = 65535.0
}
if output < 0.0 {
output = 0.0
}
table.push(output.floor() as u16);
}
table
}
fn curve_from_table(table: &[u16]) -> Box<curveType> {
Box::new(curveType::Curve(table.to_vec()))
}
pub fn float_to_u8Fixed8Number(a: f32) -> u16 {
if a > 255.0 + 255.0 / 256f32 {
0xffffu16
} else if a < 0.0 {
0u16
} else {
(a * 256.0 + 0.5).floor() as u16
}
}
fn curve_from_gamma(gamma: f32) -> Box<curveType> {
Box::new(curveType::Curve(vec![float_to_u8Fixed8Number(gamma)]))
}
fn identity_curve() -> Box<curveType> {
Box::new(curveType::Curve(Vec::new()))
}
/* from lcms: cmsWhitePointFromTemp */
/* tempK must be >= 4000. and <= 25000.
* Invalid values of tempK will return
* (x,y,Y) = (-1.0, -1.0, -1.0)
* similar to argyll: icx_DTEMP2XYZ() */
fn white_point_from_temp(temp_K: i32) -> qcms_CIE_xyY {
let mut white_point: qcms_CIE_xyY = qcms_CIE_xyY {
x: 0.,
y: 0.,
Y: 0.,
};
// No optimization provided.
let T = temp_K as f64; // Square
let T2 = T * T; // Cube
let T3 = T2 * T;
// For correlated color temperature (T) between 4000K and 7000K:
let x = if (4000.0..=7000.0).contains(&T) {
-4.6070 * (1E9 / T3) + 2.9678 * (1E6 / T2) + 0.09911 * (1E3 / T) + 0.244063
} else if T > 7000.0 && T <= 25000.0 {
-2.0064 * (1E9 / T3) + 1.9018 * (1E6 / T2) + 0.24748 * (1E3 / T) + 0.237040
} else {
// or for correlated color temperature (T) between 7000K and 25000K:
// Invalid tempK
white_point.x = -1.0;
white_point.y = -1.0;
white_point.Y = -1.0;
debug_assert!(false, "invalid temp");
return white_point;
};
// Obtain y(x)
let y = -3.000 * (x * x) + 2.870 * x - 0.275;
// wave factors (not used, but here for futures extensions)
// let M1 = (-1.3515 - 1.7703*x + 5.9114 *y)/(0.0241 + 0.2562*x - 0.7341*y);
// let M2 = (0.0300 - 31.4424*x + 30.0717*y)/(0.0241 + 0.2562*x - 0.7341*y);
// Fill white_point struct
white_point.x = x;
white_point.y = y;
white_point.Y = 1.0;
white_point
}
#[no_mangle]
pub extern "C" fn qcms_white_point_sRGB() -> qcms_CIE_xyY {
white_point_from_temp(6504)
}
/// See [Rec. ITU-T H.273 (12/2016)](https://www.itu.int/rec/T-REC-H.273-201612-I/en) Table 2
/// Values 0, 3, 1321, 23255 are all reserved so all map to the same variant
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ColourPrimaries {
/// For future use by ITU-T | ISO/IEC
Reserved,
/// Rec. ITU-R BT.709-6<br />
/// Rec. ITU-R BT.1361-0 conventional colour gamut system and extended colour gamut system (historical)<br />
/// IEC 61966-2-1 sRGB or sYCC IEC 61966-2-4<br />
/// Society of Motion Picture and Television Engineers (MPTE) RP 177 (1993) Annex B<br />
Bt709 = 1,
/// Unspecified<br />
/// Image characteristics are unknown or are determined by the application.
Unspecified = 2,
/// Rec. ITU-R BT.470-6 System M (historical)<br />
/// United States National Television System Committee 1953 Recommendation for transmission standards for color television<br />
/// United States Federal Communications Commission (2003) Title 47 Code of Federal Regulations 73.682 (a) (20)<br />
Bt470M = 4,
/// Rec. ITU-R BT.470-6 System B, G (historical) Rec. ITU-R BT.601-7 625<br />
/// Rec. ITU-R BT.1358-0 625 (historical)<br />
/// Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM<br />
Bt470Bg = 5,
/// Rec. ITU-R BT.601-7 525<br />
/// Rec. ITU-R BT.1358-1 525 or 625 (historical) Rec. ITU-R BT.1700-0 NTSC<br />
/// SMPTE 170M (2004)<br />
/// (functionally the same as the value 7)<br />
Bt601 = 6,
/// SMPTE 240M (1999) (historical) (functionally the same as the value 6)<br />
Smpte240 = 7,
/// Generic film (colour filters using Illuminant C)<br />
Generic_film = 8,
/// Rec. ITU-R BT.2020-2<br />
/// Rec. ITU-R BT.2100-0<br />
Bt2020 = 9,
/// SMPTE ST 428-1<br />
/// (CIE 1931 XYZ as in ISO 11664-1)<br />
Xyz = 10,
/// SMPTE RP 431-2 (2011)<br />
Smpte431 = 11,
/// SMPTE EG 432-1 (2010)<br />
Smpte432 = 12,
/// EBU Tech. 3213-E (1975)<br />
Ebu3213 = 22,
}
impl From<u8> for ColourPrimaries {
fn from(value: u8) -> Self {
match value {
0 | 3 | 13..=21 | 23..=255 => Self::Reserved,
1 => Self::Bt709,
2 => Self::Unspecified,
4 => Self::Bt470M,
5 => Self::Bt470Bg,
6 => Self::Bt601,
7 => Self::Smpte240,
8 => Self::Generic_film,
9 => Self::Bt2020,
10 => Self::Xyz,
11 => Self::Smpte431,
12 => Self::Smpte432,
22 => Self::Ebu3213,
}
}
}
#[test]
fn colour_primaries() {
for value in 0..=u8::MAX {
match ColourPrimaries::from(value) {
ColourPrimaries::Reserved => {}
variant => assert_eq!(value, variant as u8),
}
}
}
impl From<ColourPrimaries> for qcms_CIE_xyYTRIPLE {
fn from(value: ColourPrimaries) -> Self {
let red;
let green;
let blue;
match value {
ColourPrimaries::Reserved => panic!("CP={} is reserved", value as u8),
ColourPrimaries::Bt709 => {
green = qcms_chromaticity { x: 0.300, y: 0.600 };
blue = qcms_chromaticity { x: 0.150, y: 0.060 };
red = qcms_chromaticity { x: 0.640, y: 0.330 };
}
ColourPrimaries::Unspecified => panic!("CP={} is unspecified", value as u8),
ColourPrimaries::Bt470M => {
green = qcms_chromaticity { x: 0.21, y: 0.71 };
blue = qcms_chromaticity { x: 0.14, y: 0.08 };
red = qcms_chromaticity { x: 0.67, y: 0.33 };
}
ColourPrimaries::Bt470Bg => {
green = qcms_chromaticity { x: 0.29, y: 0.60 };
blue = qcms_chromaticity { x: 0.15, y: 0.06 };
red = qcms_chromaticity { x: 0.64, y: 0.33 };
}
ColourPrimaries::Bt601 | ColourPrimaries::Smpte240 => {
green = qcms_chromaticity { x: 0.310, y: 0.595 };
blue = qcms_chromaticity { x: 0.155, y: 0.070 };
red = qcms_chromaticity { x: 0.630, y: 0.340 };
}
ColourPrimaries::Generic_film => {
green = qcms_chromaticity { x: 0.243, y: 0.692 };
blue = qcms_chromaticity { x: 0.145, y: 0.049 };
red = qcms_chromaticity { x: 0.681, y: 0.319 };
}
ColourPrimaries::Bt2020 => {
green = qcms_chromaticity { x: 0.170, y: 0.797 };
blue = qcms_chromaticity { x: 0.131, y: 0.046 };
red = qcms_chromaticity { x: 0.708, y: 0.292 };
}
ColourPrimaries::Xyz => {
green = qcms_chromaticity { x: 0.0, y: 1.0 };
blue = qcms_chromaticity { x: 0.0, y: 0.0 };
red = qcms_chromaticity { x: 1.0, y: 0.0 };
}
// These two share primaries, but have distinct white points
ColourPrimaries::Smpte431 | ColourPrimaries::Smpte432 => {
green = qcms_chromaticity { x: 0.265, y: 0.690 };
blue = qcms_chromaticity { x: 0.150, y: 0.060 };
red = qcms_chromaticity { x: 0.680, y: 0.320 };
}
ColourPrimaries::Ebu3213 => {
green = qcms_chromaticity { x: 0.295, y: 0.605 };
blue = qcms_chromaticity { x: 0.155, y: 0.077 };
red = qcms_chromaticity { x: 0.630, y: 0.340 };
}
}
Self {
red: red.into(),
green: green.into(),
blue: blue.into(),
}
}
}
impl ColourPrimaries {
fn white_point(self) -> qcms_CIE_xyY {
match self {
Self::Reserved => panic!("CP={} is reserved", self as u8),
Self::Bt709
| Self::Bt470Bg
| Self::Bt601
| Self::Smpte240
| Self::Bt2020
| Self::Smpte432
| Self::Ebu3213 => qcms_chromaticity::D65,
Self::Unspecified => panic!("CP={} is unspecified", self as u8),
Self::Bt470M => qcms_chromaticity { x: 0.310, y: 0.316 },
Self::Generic_film => qcms_chromaticity { x: 0.310, y: 0.316 },
Self::Xyz => qcms_chromaticity {
x: 1. / 3.,
y: 1. / 3.,
},
Self::Smpte431 => qcms_chromaticity { x: 0.314, y: 0.351 },
}
.into()
}
}
/// See [Rec. ITU-T H.273 (12/2016)](https://www.itu.int/rec/T-REC-H.273-201612-I/en) Table 3
/// Values 0, 3, 19255 are all reserved so all map to the same variant
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TransferCharacteristics {
/// For future use by ITU-T | ISO/IEC
Reserved,
/// Rec. ITU-R BT.709-6<br />
/// Rec. ITU-R BT.1361-0 conventional colour gamut system (historical)<br />
/// (functionally the same as the values 6, 14 and 15) <br />
Bt709 = 1,
/// Image characteristics are unknown or are determined by the application.<br />
Unspecified = 2,
/// Rec. ITU-R BT.470-6 System M (historical)<br />
/// United States National Television System Committee 1953 Recommendation for transmission standards for color television<br />
/// United States Federal Communications Commission (2003) Title 47 Code of Federal Regulations 73.682 (a) (20)<br />
/// Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM<br />
Bt470M = 4,
/// Rec. ITU-R BT.470-6 System B, G (historical)<br />
Bt470Bg = 5,
/// Rec. ITU-R BT.601-7 525 or 625<br />
/// Rec. ITU-R BT.1358-1 525 or 625 (historical)<br />
/// Rec. ITU-R BT.1700-0 NTSC SMPTE 170M (2004)<br />
/// (functionally the same as the values 1, 14 and 15)<br />
Bt601 = 6,
/// SMPTE 240M (1999) (historical)<br />
Smpte240 = 7,
/// Linear transfer characteristics<br />
Linear = 8,
/// Logarithmic transfer characteristic (100:1 range)<br />
Log_100 = 9,
/// Logarithmic transfer characteristic (100 * Sqrt( 10 ) : 1 range)<br />
Log_100_sqrt10 = 10,
/// IEC 61966-2-4<br />
Iec61966 = 11,
/// Rec. ITU-R BT.1361-0 extended colour gamut system (historical)<br />
Bt_1361 = 12,
/// IEC 61966-2-1 sRGB or sYCC<br />
Srgb = 13,
/// Rec. ITU-R BT.2020-2 (10-bit system)<br />
/// (functionally the same as the values 1, 6 and 15)<br />
Bt2020_10bit = 14,
/// Rec. ITU-R BT.2020-2 (12-bit system)<br />
/// (functionally the same as the values 1, 6 and 14)<br />
Bt2020_12bit = 15,
/// SMPTE ST 2084 for 10-, 12-, 14- and 16-bitsystems<br />
/// Rec. ITU-R BT.2100-0 perceptual quantization (PQ) system<br />
Smpte2084 = 16,
/// SMPTE ST 428-1<br />
Smpte428 = 17,
/// ARIB STD-B67<br />
/// Rec. ITU-R BT.2100-0 hybrid log- gamma (HLG) system<br />
Hlg = 18,
}
#[test]
fn transfer_characteristics() {
for value in 0..=u8::MAX {
match TransferCharacteristics::from(value) {
TransferCharacteristics::Reserved => {}
variant => assert_eq!(value, variant as u8),
}
}
}
impl From<u8> for TransferCharacteristics {
fn from(value: u8) -> Self {
match value {
0 | 3 | 19..=255 => Self::Reserved,
1 => Self::Bt709,
2 => Self::Unspecified,
4 => Self::Bt470M,
5 => Self::Bt470Bg,
6 => Self::Bt601,
7 => Self::Smpte240, // unimplemented
8 => Self::Linear,
9 => Self::Log_100,
10 => Self::Log_100_sqrt10,
11 => Self::Iec61966, // unimplemented
12 => Self::Bt_1361, // unimplemented
13 => Self::Srgb,
14 => Self::Bt2020_10bit,
15 => Self::Bt2020_12bit,
16 => Self::Smpte2084,
17 => Self::Smpte428, // unimplemented
18 => Self::Hlg,
}
}
}
impl TryFrom<TransferCharacteristics> for curveType {
type Error = ();
/// See [ICC.1:2010](https://www.color.org/specification/ICC1v43_2010-12.pdf)
/// See [Rec. ITU-R BT.2100-2](https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2100-2-201807-I!!PDF-E.pdf)
fn try_from(value: TransferCharacteristics) -> Result<Self, Self::Error> {
const NUM_TRC_TABLE_ENTRIES: i32 = 1024;
Ok(match value {
TransferCharacteristics::Reserved => panic!("TC={} is reserved", value as u8),
TransferCharacteristics::Bt709
| TransferCharacteristics::Bt601
| TransferCharacteristics::Bt2020_10bit
| TransferCharacteristics::Bt2020_12bit => {
// The opto-electronic transfer characteristic function (OETF)
// as defined in ITU-T H.273 table 3, row 1:
//
// V = (α * Lc^0.45) (α 1) for 1 >= Lc >= β
// V = 4.500 * Lc for β > Lc >= 0
//
// Inverting gives the electro-optical transfer characteristic
// function (EOTF) which can be represented as ICC
// parametricCurveType with 4 parameters (ICC.1:2010 Table 5).
// Converting between the two (Lc ↔︎ Y, V ↔︎ X):
//
// Y = (a * X + b)^g for (X >= d)
// Y = c * X for (X < d)
//
// g, a, b, c, d can then be defined in terms of α and β:
//
// g = 1 / 0.45
// a = 1 / α
// b = 1 - α
// c = 1 / 4.500
// d = 4.500 * β
//
// α and β are determined by solving the piecewise equations to
// ensure continuity of both value and slope at the value β.
// We use the values specified for 10-bit systems in
// https://www.itu.int/rec/R-REC-BT.2020-2-201510-I Table 4
// since this results in the similar values as available ICC
// profiles after converting to s15Fixed16Number, providing us
// good test coverage.
type Float = f32;
const alpha: Float = 1.099;
const beta: Float = 0.018;
const linear_coef: Float = 4.500;
const pow_exp: Float = 0.45;
const g: Float = 1. / pow_exp;
const a: Float = 1. / alpha;
const b: Float = 1. - a;
const c: Float = 1. / linear_coef;
const d: Float = linear_coef * beta;
curveType::Parametric(vec![g, a, b, c, d])
}
TransferCharacteristics::Unspecified => panic!("TC={} is unspecified", value as u8),
TransferCharacteristics::Bt470M => *curve_from_gamma(2.2),
TransferCharacteristics::Bt470Bg => *curve_from_gamma(2.8),
TransferCharacteristics::Smpte240 => return Err(()),
TransferCharacteristics::Linear => *curve_from_gamma(1.),
TransferCharacteristics::Log_100 => {
// See log_100_transfer_characteristics() for derivation
// The opto-electronic transfer characteristic function (OETF)
// as defined in ITU-T H.273 table 3, row 9:
//
// V = 1.0 + Log10(Lc) ÷ 2 for 1 >= Lc >= 0.01
// V = 0.0 for 0.01 > Lc >= 0
//
// Inverting this to give the EOTF required for the profile gives
//
// Lc = 10^(2*V - 2) for 1 >= V >= 0
let table = build_trc_table(NUM_TRC_TABLE_ENTRIES, |v| 10f64.powf(2. * v - 2.));
curveType::Curve(table)
}
TransferCharacteristics::Log_100_sqrt10 => {
// The opto-electronic transfer characteristic function (OETF)
// as defined in ITU-T H.273 table 3, row 10:
//
// V = 1.0 + Log10(Lc) ÷ 2.5 for 1 >= Lc >= Sqrt(10) ÷ 1000
// V = 0.0 for Sqrt(10) ÷ 1000 > Lc >= 0
//
// Inverting this to give the EOTF required for the profile gives
//
// Lc = 10^(2.5*V - 2.5) for 1 >= V >= 0
let table = build_trc_table(NUM_TRC_TABLE_ENTRIES, |v| 10f64.powf(2.5 * v - 2.5));
curveType::Curve(table)
}
TransferCharacteristics::Iec61966 => return Err(()),
TransferCharacteristics::Bt_1361 => return Err(()),
TransferCharacteristics::Srgb => {
// Should we prefer this or curveType::Parametric?
curveType::Curve(build_sRGB_gamma_table(NUM_TRC_TABLE_ENTRIES))
}
TransferCharacteristics::Smpte2084 => {
// Despite using Lo rather than Lc, H.273 gives the OETF:
//
// V = ( ( c1 + c2 * (Lo)^n ) ÷ ( 1 + c3 * (Lo)^n ) )^m
const c1: f64 = 0.8359375;
const c2: f64 = 18.8515625;
const c3: f64 = 18.6875;
const m: f64 = 78.84375;
const n: f64 = 0.1593017578125;
// Inverting this to give the EOTF required for the profile
// (and confirmed by Rec. ITU-R BT.2100-2, Table 4) gives
//
// Y = ( max[( X^(1/m) - c1 ), 0] ÷ ( c2 - c3 * X^(1/m) ) )^(1/n)
let table = build_trc_table(NUM_TRC_TABLE_ENTRIES, |x| {
((x.powf(1. / m) - c1).max(0.) / (c2 - c3 * x.powf(1. / m))).powf(1. / n)
});
curveType::Curve(table)
}
TransferCharacteristics::Smpte428 => return Err(()),
TransferCharacteristics::Hlg => {
// The opto-electronic transfer characteristic function (OETF)
// as defined in ITU-T H.273 table 3, row 18:
//
// V = a * Ln(12 * Lc - b) + c for 1 >= Lc > 1 ÷ 12
// V = Sqrt(3) * Lc^0.5 for 1 ÷ 12 >= Lc >= 0
const a: f64 = 0.17883277;
const b: f64 = 0.28466892;
const c: f64 = 0.55991073;
// Inverting this to give the EOTF required for the profile
// (and confirmed by Rec. ITU-R BT.2100-2, Table 4) gives
//
// Y = (X^2) / 3 for 0 <= X <= 0.5
// Y = ((e^((X-c)/a))+b)/12 for 0.5 < X <= 1
let table = build_trc_table(NUM_TRC_TABLE_ENTRIES, |x| {
if x <= 0.5 {
let y1 = x.powf(2.) / 3.;
assert!((0. ..=1. / 12.).contains(&y1));
y1
} else {
(std::f64::consts::E.powf((x - c) / a) + b) / 12.
}
});
curveType::Curve(table)
}
})
}
}
#[cfg(test)]
fn check_transfer_characteristics(cicp: TransferCharacteristics, icc_path: &str) {
let mut cicp_out = [0u8; crate::transform::PRECACHE_OUTPUT_SIZE];
let mut icc_out = [0u8; crate::transform::PRECACHE_OUTPUT_SIZE];
let cicp_tc = curveType::try_from(cicp).unwrap();
let icc = Profile::new_from_path(icc_path).unwrap();
let icc_tc = icc.redTRC.as_ref().unwrap();
eprintln!("cicp_tc: {:?}", cicp_tc);
eprintln!("icc_tc: {:?}", icc_tc);
crate::transform_util::compute_precache(icc_tc, &mut icc_out);
crate::transform_util::compute_precache(&cicp_tc, &mut cicp_out);
let mut off_by_one = 0;
for i in 0..cicp_out.len() {
match (cicp_out[i] as i16) - (icc_out[i] as i16) {
0 => {}
1 | -1 => {
off_by_one += 1;
}
_ => assert_eq!(cicp_out[i], icc_out[i], "difference at index {}", i),
}
}
eprintln!("{} / {} off by one", off_by_one, cicp_out.len());
}
#[test]
fn srgb_transfer_characteristics() {
check_transfer_characteristics(TransferCharacteristics::Srgb, "sRGB_lcms.icc");
}
#[test]
fn bt709_transfer_characteristics() {
check_transfer_characteristics(TransferCharacteristics::Bt709, "ITU-709.icc");
}
#[test]
fn bt2020_10bit_transfer_characteristics() {
check_transfer_characteristics(TransferCharacteristics::Bt2020_10bit, "ITU-2020.icc");
}
#[test]
fn bt2020_12bit_transfer_characteristics() {
check_transfer_characteristics(TransferCharacteristics::Bt2020_12bit, "ITU-2020.icc");
}
impl Profile {
//XXX: it would be nice if we had a way of ensuring
// everything in a profile was initialized regardless of how it was created
//XXX: should this also be taking a black_point?
/* similar to CGColorSpaceCreateCalibratedRGB */
pub fn new_rgb_with_table(
white_point: qcms_CIE_xyY,
primaries: qcms_CIE_xyYTRIPLE,
table: &[u16],
) -> Option<Box<Profile>> {
let mut profile = profile_create();
//XXX: should store the whitepoint
if !set_rgb_colorants(&mut profile, white_point, primaries) {
return None;
}
profile.redTRC = Some(curve_from_table(table));
profile.blueTRC = Some(curve_from_table(table));
profile.greenTRC = Some(curve_from_table(table));
profile.class_type = DISPLAY_DEVICE_PROFILE;
profile.rendering_intent = Perceptual;
profile.color_space = RGB_SIGNATURE;
profile.pcs = XYZ_TYPE;
Some(profile)
}
pub fn new_sRGB() -> Box<Profile> {
let D65 = qcms_white_point_sRGB();
let table = build_sRGB_gamma_table(1024);
let mut srgb = Profile::new_rgb_with_table(
D65,
qcms_CIE_xyYTRIPLE::from(ColourPrimaries::Bt709),
&table,
)
.unwrap();
srgb.is_srgb = true;
srgb
}
/// Returns true if this profile is sRGB
pub fn is_sRGB(&self) -> bool {
self.is_srgb
}
pub(crate) fn new_sRGB_parametric() -> Box<Profile> {
let primaries = qcms_CIE_xyYTRIPLE::from(ColourPrimaries::Bt709);
let white_point = qcms_white_point_sRGB();
let mut profile = profile_create();
set_rgb_colorants(&mut profile, white_point, primaries);
let curve = Box::new(curveType::Parametric(vec![
2.4,
1. / 1.055,
0.055 / 1.055,
1. / 12.92,
0.04045,
]));
profile.redTRC = Some(curve.clone());
profile.blueTRC = Some(curve.clone());
profile.greenTRC = Some(curve);
profile.class_type = DISPLAY_DEVICE_PROFILE;
profile.rendering_intent = Perceptual;
profile.color_space = RGB_SIGNATURE;
profile.pcs = XYZ_TYPE;
profile.is_srgb = true;
profile
}
/// Create a new profile with D50 adopted white and identity transform functions
pub fn new_XYZD50() -> Box<Profile> {
let mut profile = profile_create();
profile.redColorant.X = double_to_s15Fixed16Number(1.);
profile.redColorant.Y = double_to_s15Fixed16Number(0.);
profile.redColorant.Z = double_to_s15Fixed16Number(0.);
profile.greenColorant.X = double_to_s15Fixed16Number(0.);
profile.greenColorant.Y = double_to_s15Fixed16Number(1.);
profile.greenColorant.Z = double_to_s15Fixed16Number(0.);
profile.blueColorant.X = double_to_s15Fixed16Number(0.);
profile.blueColorant.Y = double_to_s15Fixed16Number(0.);
profile.blueColorant.Z = double_to_s15Fixed16Number(1.);
profile.redTRC = Some(identity_curve());
profile.blueTRC = Some(identity_curve());
profile.greenTRC = Some(identity_curve());
profile.class_type = DISPLAY_DEVICE_PROFILE;
profile.rendering_intent = Perceptual;
profile.color_space = RGB_SIGNATURE;
profile.pcs = XYZ_TYPE;
profile
}
pub fn new_cicp(cp: ColourPrimaries, tc: TransferCharacteristics) -> Option<Box<Profile>> {
let mut profile = profile_create();
//XXX: should store the whitepoint
if !set_rgb_colorants(&mut profile, cp.white_point(), qcms_CIE_xyYTRIPLE::from(cp)) {
return None;
}
let curve = curveType::try_from(tc).ok()?;
profile.redTRC = Some(Box::new(curve.clone()));
profile.blueTRC = Some(Box::new(curve.clone()));
profile.greenTRC = Some(Box::new(curve));
profile.class_type = DISPLAY_DEVICE_PROFILE;
profile.rendering_intent = Perceptual;
profile.color_space = RGB_SIGNATURE;
profile.pcs = XYZ_TYPE;
profile.is_srgb = (cp, tc) == (ColourPrimaries::Bt709, TransferCharacteristics::Srgb);
Some(profile)
}
pub fn new_gray_with_gamma(gamma: f32) -> Box<Profile> {
let mut profile = profile_create();
profile.grayTRC = Some(curve_from_gamma(gamma));
profile.class_type = DISPLAY_DEVICE_PROFILE;
profile.rendering_intent = Perceptual;
profile.color_space = GRAY_SIGNATURE;
profile.pcs = XYZ_TYPE;
profile
}
pub fn new_rgb_with_gamma_set(
white_point: qcms_CIE_xyY,
primaries: qcms_CIE_xyYTRIPLE,
redGamma: f32,
greenGamma: f32,
blueGamma: f32,
) -> Option<Box<Profile>> {
let mut profile = profile_create();
//XXX: should store the whitepoint
if !set_rgb_colorants(&mut profile, white_point, primaries) {
return None;
}
profile.redTRC = Some(curve_from_gamma(redGamma));
profile.blueTRC = Some(curve_from_gamma(blueGamma));
profile.greenTRC = Some(curve_from_gamma(greenGamma));
profile.class_type = DISPLAY_DEVICE_PROFILE;
profile.rendering_intent = Perceptual;
profile.color_space = RGB_SIGNATURE;
profile.pcs = XYZ_TYPE;
Some(profile)
}
pub fn new_from_path(file: &str) -> Option<Box<Profile>> {
Profile::new_from_slice(&std::fs::read(file).ok()?, false)
}
pub fn new_from_slice(mem: &[u8], curves_only: bool) -> Option<Box<Profile>> {
let length: u32;
let mut source: MemSource = MemSource {
buf: mem,
valid: false,
invalid_reason: None,
};
let index;
source.valid = true;
let mut src: &mut MemSource = &mut source;
if mem.len() < 4 {
return None;
}
length = read_u32(src, 0);
if length as usize <= mem.len() {
// shrink the area that we can read if appropriate
src.buf = &src.buf[0..length as usize];
} else {
return None;
}
/* ensure that the profile size is sane so it's easier to reason about */
if src.buf.len() <= 64 || src.buf.len() >= MAX_PROFILE_SIZE {
return None;
}
let mut profile = profile_create();
check_CMM_type_signature(src);
check_profile_version(src);
read_class_signature(&mut profile, src);
read_rendering_intent(&mut profile, src);
read_color_space(&mut profile, src);
read_pcs(&mut profile, src);
//TODO read rest of profile stuff
if !src.valid {
return None;
}
index = read_tag_table(&mut profile, src);
if !src.valid || index.is_empty() {
return None;
}
if let Some(chad) = find_tag(&index, TAG_CHAD) {
profile.chromaticAdaption = Some(read_tag_s15Fixed16ArrayType(src, chad))
} else {
profile.chromaticAdaption = None; //Signal the data is not present
}
if profile.class_type == DISPLAY_DEVICE_PROFILE
|| profile.class_type == INPUT_DEVICE_PROFILE
|| profile.class_type == OUTPUT_DEVICE_PROFILE
|| profile.class_type == COLOR_SPACE_PROFILE
{
if profile.color_space == RGB_SIGNATURE {
if !curves_only {
if let Some(A2B0) = find_tag(&index, TAG_A2B0) {
let lut_type = read_u32(src, A2B0.offset as usize);
if lut_type == LUT8_TYPE || lut_type == LUT16_TYPE {
profile.A2B0 = read_tag_lutType(src, A2B0)
} else if lut_type == LUT_MAB_TYPE {
profile.mAB = read_tag_lutmABType(src, A2B0)
}
}
if let Some(B2A0) = find_tag(&index, TAG_B2A0) {
let lut_type = read_u32(src, B2A0.offset as usize);
if lut_type == LUT8_TYPE || lut_type == LUT16_TYPE {
profile.B2A0 = read_tag_lutType(src, B2A0)
} else if lut_type == LUT_MBA_TYPE {
profile.mBA = read_tag_lutmABType(src, B2A0)
}
}
}
if find_tag(&index, TAG_rXYZ).is_some() || curves_only {
profile.redColorant = read_tag_XYZType(src, &index, TAG_rXYZ);
profile.greenColorant = read_tag_XYZType(src, &index, TAG_gXYZ);
profile.blueColorant = read_tag_XYZType(src, &index, TAG_bXYZ)
}
if !src.valid {
return None;
}
if find_tag(&index, TAG_rTRC).is_some() || curves_only {
profile.redTRC = read_tag_curveType(src, &index, TAG_rTRC);
profile.greenTRC = read_tag_curveType(src, &index, TAG_gTRC);
profile.blueTRC = read_tag_curveType(src, &index, TAG_bTRC);
if profile.redTRC.is_none()
|| profile.blueTRC.is_none()
|| profile.greenTRC.is_none()
{
return None;
}
}
} else if profile.color_space == GRAY_SIGNATURE {
profile.grayTRC = read_tag_curveType(src, &index, TAG_kTRC);
profile.grayTRC.as_ref()?;
} else if profile.color_space == CMYK_SIGNATURE {
if let Some(A2B0) = find_tag(&index, TAG_A2B0) {
let lut_type = read_u32(src, A2B0.offset as usize);
if lut_type == LUT8_TYPE || lut_type == LUT16_TYPE {
profile.A2B0 = read_tag_lutType(src, A2B0)
} else if lut_type == LUT_MBA_TYPE {
profile.mAB = read_tag_lutmABType(src, A2B0)
}
}
} else {
debug_assert!(false, "read_color_space protects against entering here");
return None;
}
} else {
return None;
}
if !src.valid {
return None;
}
Some(profile)
}
/// Precomputes the information needed for this profile to be
/// used as the output profile when constructing a `Transform`.
pub fn precache_output_transform(&mut self) {
crate::transform::qcms_profile_precache_output_transform(self);
}
}