///|
/// Pure ICC v2/v4 input/display profile conversion for straight RGBA images.
///
/// The implementation intentionally keeps the supported surface explicit. It
/// accepts RGB and Gray matrix/TRC profiles with XYZ PCS, and A2B LUT profiles
/// with XYZ or Lab PCS. Device-link, printer,
/// n-component, named-color, and output-only workflows are rejected by the
/// parser instead of being guessed.
const ICC_SIG_ACSP : Int = 0x61637370
///|
const ICC_SIG_SCNR : Int = 0x73636E72
///|
const ICC_SIG_MNTR : Int = 0x6D6E7472
///|
const ICC_SIG_PRTR : Int = 0x70727472
///|
const ICC_SIG_LINK : Int = 0x6C696E6B
///|
const ICC_SIG_SPAC : Int = 0x73706163
///|
const ICC_SIG_NMCL : Int = 0x6E6D636C
///|
const ICC_SIG_RGB : Int = 0x52474220
///|
const ICC_SIG_GRAY : Int = 0x47524159
///|
const ICC_SIG_XYZ : Int = 0x58595A20
///|
const ICC_SIG_LAB : Int = 0x4C616220
///|
const ICC_SIG_CURV : Int = 0x63757276
///|
const ICC_SIG_PARA : Int = 0x70617261
///|
const ICC_SIG_XYZ_TYPE : Int = 0x58595A20
///|
const ICC_SIG_MFT1 : Int = 0x6D667431
///|
const ICC_SIG_MFT2 : Int = 0x6D667432
///|
const ICC_SIG_MAB : Int = 0x6D414220
///|
const ICC_SIG_RXYZ : Int = 0x7258595A
///|
const ICC_SIG_GXYZ : Int = 0x6758595A
///|
const ICC_SIG_BXYZ : Int = 0x6258595A
///|
const ICC_SIG_RTRC : Int = 0x72545243
///|
const ICC_SIG_GTRC : Int = 0x67545243
///|
const ICC_SIG_BTRC : Int = 0x62545243
///|
const ICC_SIG_KTRC : Int = 0x6B545243
///|
const ICC_SIG_WTPT : Int = 0x77747074
///|
const ICC_SIG_A2B0 : Int = 0x41324230
///|
const ICC_SIG_A2B1 : Int = 0x41324231
///|
const ICC_SIG_A2B2 : Int = 0x41324232
///|
/// Maximum supported RGB lattice, retaining the existing 65-point limit.
const ICC_MAX_CLUT_CELLS : Int = 274625
///|
/// ICC rendering intents understood by the policy-aware entry points.
pub(all) enum IccRenderingIntent {
Perceptual
RelativeColorimetric
Saturation
AbsoluteColorimetric
}
///|
/// Rendering and black-point policy for an ICC conversion.
///
/// The legacy `apply_rgba8` and `apply_rgba16` methods use
/// `RelativeColorimetric` with black-point compensation disabled. Use this
/// value with the `*_with_options` methods to select an ICC intent and, when
/// requested, LittleCMS-compatible black-point compensation.
pub struct IccTransformOptions {
intent : IccRenderingIntent
black_point_compensation : Bool
}
///|
pub fn IccTransformOptions::new(
intent : IccRenderingIntent,
black_point_compensation : Bool,
) -> IccTransformOptions {
{ intent, black_point_compensation, }
}
///|
pub fn IccTransformOptions::relative_colorimetric() -> IccTransformOptions {
{ intent: RelativeColorimetric, black_point_compensation: false, }
}
///|
priv enum IccIssue {
IccTruncated(String)
IccInvalid(String)
IccUnsupported(String)
IccLimit(String)
}
///|
priv struct IccTag {
signature : Int
offset : Int
size : Int
}
///|
priv enum IccCurve {
IccIdentity
IccGamma(Double)
IccSampled(Array[Double])
IccParametric(Int, Array[Double])
}
///|
priv struct IccLut {
input_channels : Int
output_channels : Int
grids : Array[Int]
has_clut : Bool
clut : Array[Double]
input_curves : Array[IccCurve]
middle_curves : Array[IccCurve]
output_curves : Array[IccCurve]
matrix : Array[Double]
translation : Array[Double]
pcs_lab : Bool
pcs_xyz_scale : Double
lab_legacy : Bool
}
///|
priv enum IccPipeline {
IccMatrixRgb(Array[Double], IccCurve, IccCurve, IccCurve)
IccMatrixGray(Array[Double], IccCurve)
IccLutPipeline(IccLut)
}
///|
/// A validated, immutable ICC input/display profile.
pub struct IccProfile {
priv color_space : Int
priv pipeline : IccPipeline
priv source_bytes : Array[Byte]
priv declared_size : Int
priv tags : Array[IccTag]
priv perceptual_tag : IccTag?
priv saturation_tag : IccTag?
priv relative_tag : IccTag?
priv device_class : Int
priv matrix_shaper : Bool
priv pcs_lab : Bool
priv pcs_xyz : Bool
priv lab_legacy : Bool
}
///|
fn icc_error(issue : IccIssue) -> DecodeError {
match issue {
IccTruncated(message) => decode_error(TruncatedInput, "icc", message)
IccInvalid(message) => decode_error(InvalidData, "icc", message)
IccUnsupported(message) => decode_error(Unsupported, "icc", message)
IccLimit(message) => decode_error(LimitExceeded, "icc", message)
}
}
///|
fn icc_u8(data : Array[Byte], at : Int, limit : Int) -> Int? {
if at < 0 || at >= limit || at >= data.length() {
None
} else {
Some(data[at].to_int())
}
}
///|
fn icc_u16(data : Array[Byte], at : Int, limit : Int) -> Int? {
if at < 0 || at > limit - 2 || at + 1 >= data.length() {
None
} else {
Some((data[at].to_int() << 8) | data[at + 1].to_int())
}
}
///|
fn icc_u32(data : Array[Byte], at : Int, limit : Int) -> Int? {
if at < 0 || at > limit - 4 || at + 3 >= data.length() {
None
} else {
let high = data[at].to_int()
// All offsets, sizes and counts used by this bounded implementation fit
// in a signed MoonBit Int. Rejecting the sign bit also avoids target
// dependent overflow on 32-bit WASM.
if high >= 128 {
None
} else {
Some(
(high << 24) |
(data[at + 1].to_int() << 16) |
(data[at + 2].to_int() << 8) |
data[at + 3].to_int(),
)
}
}
}
///|
fn icc_s15f16(data : Array[Byte], at : Int, limit : Int) -> Double? {
match (icc_u16(data, at, limit), icc_u16(data, at + 2, limit)) {
(Some(high), Some(low)) => {
let signed = if high >= 32768 { high - 65536 } else { high }
Some(signed.to_double() + low.to_double() / 65536.0)
}
_ => None
}
}
///|
fn icc_has_range(at : Int, length : Int, limit : Int) -> Bool {
at >= 0 && length >= 0 && at <= limit && length <= limit - at
}
///|
fn icc_mab_target(
base : Int,
relative : Int,
tag_end : Int,
) -> Result[Int, IccIssue] {
if relative < 32 || relative % 4 != 0 || relative > tag_end - base {
return Err(IccInvalid("mAB processing-element offset is invalid"))
}
Ok(base + relative)
}
///|
fn icc_tag(tags : Array[IccTag], signature : Int) -> IccTag? {
for tag in tags {
if tag.signature == signature {
return Some(tag)
}
}
None
}
///|
fn icc_clamp(value : Double) -> Double {
if value <= 0.0 {
0.0
} else if value >= 1.0 {
1.0
} else {
value
}
}
///|
fn icc_curve_count(function_type : Int) -> Int? {
match function_type {
0 => Some(1)
1 => Some(3)
2 => Some(4)
3 => Some(5)
4 => Some(7)
_ => None
}
}
///|
fn icc_curve_eval(curve : IccCurve, value : Double) -> Double {
let x = icc_clamp(value)
match curve {
IccIdentity => x
IccGamma(gamma) => if x <= 0.0 { 0.0 } else { @math.pow(x, gamma) }
IccSampled(samples) => {
let count = samples.length()
if count == 0 {
x
} else if count == 1 {
samples[0]
} else {
let position = x * (count - 1).to_double()
let index = position.to_int()
if index >= count - 1 {
samples[count - 1]
} else {
let fraction = position - index.to_double()
samples[index] * (1.0 - fraction) + samples[index + 1] * fraction
}
}
}
IccParametric(function_type, parameters) => {
let power = parameters[0]
let a = if parameters.length() > 1 { parameters[1] } else { 1.0 }
let b = if parameters.length() > 2 { parameters[2] } else { 0.0 }
let c = if parameters.length() > 3 { parameters[3] } else { 0.0 }
let d = if parameters.length() > 4 { parameters[4] } else { 0.0 }
let e = if parameters.length() > 5 { parameters[5] } else { 0.0 }
let f = if parameters.length() > 6 { parameters[6] } else { 0.0 }
let positive_power = fn(base : Double) -> Double {
if base <= 0.0 {
0.0
} else {
@math.pow(base, power)
}
}
icc_clamp(
match function_type {
0 => positive_power(x)
1 => if x >= -b / a { positive_power(a * x + b) } else { 0.0 }
2 => if x >= -b / a { positive_power(a * x + b) + c } else { c }
3 => if x >= d { positive_power(a * x + b) } else { c * x }
4 => if x >= d { positive_power(a * x + b) + e } else { c * x + f }
_ => 0.0
},
)
}
}
}
///|
fn icc_parse_curve_at(
data : Array[Byte],
at : Int,
limit : Int,
) -> Result[(IccCurve, Int), IccIssue] {
let type_signature = match icc_u32(data, at, limit) {
Some(value) => value
None => return Err(IccTruncated("Curve type header is truncated"))
}
if !icc_has_range(at, 12, limit) {
return Err(IccTruncated("Curve header is truncated"))
}
match type_signature {
ICC_SIG_CURV => {
let count = match icc_u32(data, at + 8, limit) {
Some(value) => value
None => return Err(IccTruncated("Sampled curve count is truncated"))
}
if count == 0 {
return Ok((IccIdentity, at + 12))
}
if count == 1 {
let gamma = match icc_u16(data, at + 12, limit) {
Some(value) => value
None => return Err(IccTruncated("Curve gamma is truncated"))
}
if gamma == 0 {
return Err(IccInvalid("Curve gamma must be positive"))
}
return Ok((IccGamma(gamma.to_double() / 256.0), at + 16))
}
if count > 65536 {
return Err(IccLimit("Curve exceeds 65536 sampled entries"))
}
if count > (limit - (at + 12)) / 2 {
return Err(IccInvalid("Sampled curve has an invalid length"))
}
let end = at + 12 + count * 2
let samples = Array::makei(count, i => {
match icc_u16(data, at + 12 + i * 2, limit) {
Some(value) => value.to_double() / 65535.0
None => 0.0
}
})
Ok((IccSampled(samples), (end + 3) / 4 * 4))
}
ICC_SIG_PARA => {
let function_type = match icc_u16(data, at + 8, limit) {
Some(value) => value
None =>
return Err(IccTruncated("Parametric curve function is truncated"))
}
let count = match icc_curve_count(function_type) {
Some(value) => value
None =>
return Err(IccUnsupported("Parametric curve function is unsupported"))
}
if count > (limit - (at + 12)) / 4 {
return Err(IccTruncated("Parametric curve parameters are truncated"))
}
let parameters = Array::makei(count, i => {
icc_s15f16(data, at + 12 + i * 4, limit).unwrap_or(0.0)
})
if parameters[0] <= 0.0 {
return Err(IccInvalid("Parametric curve gamma must be positive"))
}
if (function_type == 1 || function_type == 2) && parameters[1] == 0.0 {
return Err(IccInvalid("Parametric curve has a zero slope"))
}
Ok(
(
IccParametric(function_type, parameters),
(at + 12 + count * 4 + 3) / 4 * 4,
),
)
}
_ => Err(IccUnsupported("Curve tag type is unsupported"))
}
}
///|
fn icc_parse_curve_tag(
data : Array[Byte],
tag : IccTag,
limit : Int,
) -> Result[IccCurve, IccIssue] {
let end = tag.offset + tag.size
if !icc_has_range(tag.offset, tag.size, limit) {
return Err(IccInvalid("Curve tag lies outside the profile"))
}
match icc_parse_curve_at(data, tag.offset, end) {
Ok((curve, _)) => Ok(curve)
Err(issue) => Err(issue)
}
}
///|
fn icc_parse_xyz_tag(
data : Array[Byte],
tag : IccTag,
limit : Int,
) -> Result[(Double, Double, Double), IccIssue] {
if tag.size < 20 || !icc_has_range(tag.offset, tag.size, limit) {
return Err(IccInvalid("XYZ tag has an invalid size"))
}
let type_signature = match icc_u32(data, tag.offset, limit) {
Some(value) => value
None => return Err(IccTruncated("XYZ tag type is truncated"))
}
if type_signature != ICC_SIG_XYZ_TYPE {
return Err(IccInvalid("XYZ tag has the wrong type"))
}
match
(
icc_s15f16(data, tag.offset + 8, limit),
icc_s15f16(data, tag.offset + 12, limit),
icc_s15f16(data, tag.offset + 16, limit),
) {
(Some(x), Some(y), Some(z)) => Ok((x, y, z))
_ => Err(IccTruncated("XYZ tag payload is truncated"))
}
}
///|
fn icc_matrix_from_tags(
data : Array[Byte],
tags : Array[IccTag],
limit : Int,
) -> Result[IccPipeline, IccIssue] {
let rxyz = match icc_tag(tags, ICC_SIG_RXYZ) {
Some(tag) =>
match icc_parse_xyz_tag(data, tag, limit) {
Ok(value) => value
Err(issue) => return Err(issue)
}
None => return Err(IccInvalid("RGB profile is missing rXYZ"))
}
let gxyz = match icc_tag(tags, ICC_SIG_GXYZ) {
Some(tag) =>
match icc_parse_xyz_tag(data, tag, limit) {
Ok(value) => value
Err(issue) => return Err(issue)
}
None => return Err(IccInvalid("RGB profile is missing gXYZ"))
}
let bxyz = match icc_tag(tags, ICC_SIG_BXYZ) {
Some(tag) =>
match icc_parse_xyz_tag(data, tag, limit) {
Ok(value) => value
Err(issue) => return Err(issue)
}
None => return Err(IccInvalid("RGB profile is missing bXYZ"))
}
let rtrc = match icc_tag(tags, ICC_SIG_RTRC) {
Some(tag) =>
match icc_parse_curve_tag(data, tag, limit) {
Ok(value) => value
Err(issue) => return Err(issue)
}
None => return Err(IccInvalid("RGB profile is missing rTRC"))
}
let gtrc = match icc_tag(tags, ICC_SIG_GTRC) {
Some(tag) =>
match icc_parse_curve_tag(data, tag, limit) {
Ok(value) => value
Err(issue) => return Err(issue)
}
None => return Err(IccInvalid("RGB profile is missing gTRC"))
}
let btrc = match icc_tag(tags, ICC_SIG_BTRC) {
Some(tag) =>
match icc_parse_curve_tag(data, tag, limit) {
Ok(value) => value
Err(issue) => return Err(issue)
}
None => return Err(IccInvalid("RGB profile is missing bTRC"))
}
Ok(
IccMatrixRgb(
[rxyz.0, gxyz.0, bxyz.0, rxyz.1, gxyz.1, bxyz.1, rxyz.2, gxyz.2, bxyz.2],
rtrc,
gtrc,
btrc,
),
)
}
///|
fn icc_white_valid(white : (Double, Double, Double)) -> Bool {
if white.0.is_nan() ||
white.1.is_nan() ||
white.2.is_nan() ||
white.0.is_inf() ||
white.1.is_inf() ||
white.2.is_inf() ||
white.0 <= 0.0 ||
white.1 <= 0.0 ||
white.2 <= 0.0 {
return false
}
let cone_l = 0.8951 * white.0 + 0.2664 * white.1 - 0.1614 * white.2
let cone_m = -0.7502 * white.0 + 1.7135 * white.1 + 0.0367 * white.2
let cone_s = 0.0389 * white.0 - 0.0685 * white.1 + 1.0296 * white.2
!cone_l.is_nan() &&
!cone_m.is_nan() &&
!cone_s.is_nan() &&
!cone_l.is_inf() &&
!cone_m.is_inf() &&
!cone_s.is_inf() &&
cone_l != 0.0 &&
cone_m != 0.0 &&
cone_s != 0.0
}
///|
fn icc_parse_gray_pipeline(
data : Array[Byte],
tags : Array[IccTag],
limit : Int,
) -> Result[IccPipeline, IccIssue] {
let white = match icc_tag(tags, ICC_SIG_WTPT) {
Some(tag) =>
match icc_parse_xyz_tag(data, tag, limit) {
Ok(value) => value
Err(issue) => return Err(issue)
}
None => return Err(IccInvalid("Gray profile is missing wtpt"))
}
let trc = match icc_tag(tags, ICC_SIG_KTRC) {
Some(tag) =>
match icc_parse_curve_tag(data, tag, limit) {
Ok(value) => value
Err(issue) => return Err(issue)
}
None => return Err(IccInvalid("Gray profile is missing kTRC"))
}
if !icc_white_valid(white) {
return Err(
IccInvalid("Gray profile media white is not a finite Bradford white"),
)
}
Ok(IccMatrixGray([white.0, white.1, white.2], trc))
}
///|
fn icc_tag_data_type(
data : Array[Byte],
tag : IccTag,
limit : Int,
) -> Result[Int, IccIssue] {
if !icc_has_range(tag.offset, tag.size, limit) || tag.size < 8 {
return Err(IccInvalid("Tag lies outside the profile"))
}
match icc_u32(data, tag.offset, limit) {
Some(value) => Ok(value)
None => Err(IccTruncated("Tag type is truncated"))
}
}
///|
fn icc_lut_count(grid : Int, channels : Int) -> Int? {
if grid < 2 || grid > 65 || channels < 1 || channels > 4 {
return None
}
let mut count = 1
for _ in 0.. ICC_MAX_CLUT_CELLS / grid {
return None
}
count *= grid
}
Some(count)
}
///|
fn icc_matrix_is_identity(matrix : Array[Double]) -> Bool {
for i in 0.. 0.000001 || difference < -0.000001 {
return false
}
}
true
}
///|
fn icc_parse_sampled_table(
data : Array[Byte],
at : Int,
count : Int,
sixteen_bit : Bool,
limit : Int,
) -> Result[(IccCurve, Int), IccIssue] {
let width = if sixteen_bit { 2 } else { 1 }
if count <= 0 || count > 1048576 || count > (limit - at) / width {
return Err(IccInvalid("LUT table has an invalid size"))
}
let samples = Array::makei(count, i => {
if sixteen_bit {
icc_u16(data, at + i * 2, limit).unwrap_or(0).to_double() / 65535.0
} else {
icc_u8(data, at + i, limit).unwrap_or(0).to_double() / 255.0
}
})
Ok((IccSampled(samples), at + count * width))
}
///|
fn icc_parse_mft(
data : Array[Byte],
tag : IccTag,
limit : Int,
sixteen_bit : Bool,
pcs_lab : Bool,
pcs_xyz : Bool,
) -> Result[IccLut, IccIssue] {
let header_size = if sixteen_bit { 52 } else { 48 }
if tag.size < header_size || !icc_has_range(tag.offset, tag.size, limit) {
return Err(IccInvalid("mft LUT header has an invalid size"))
}
let p = tag.offset
let input_channels = icc_u8(data, p + 8, limit).unwrap_or(0)
let output_channels = icc_u8(data, p + 9, limit).unwrap_or(0)
let grid = icc_u8(data, p + 10, limit).unwrap_or(0)
if (input_channels != 1 && input_channels != 3) || output_channels != 3 {
return Err(
IccUnsupported("LUT must have one or three inputs and three outputs"),
)
}
let cells = match icc_lut_count(grid, input_channels) {
Some(value) => value
None => return Err(IccInvalid("LUT grid is outside supported bounds"))
}
let input_entries = if sixteen_bit {
icc_u16(data, p + 48, limit).unwrap_or(0)
} else {
256
}
let output_entries = if sixteen_bit {
icc_u16(data, p + 50, limit).unwrap_or(0)
} else {
256
}
if input_entries < 2 ||
output_entries < 2 ||
input_entries > 65536 ||
output_entries > 65536 {
return Err(IccInvalid("LUT table entry count is invalid"))
}
let tag_end = p + tag.size
let mut pos = p + header_size
let input_curves : Array[IccCurve] = []
for _ in 0.. value
Err(issue) => return Err(issue)
}
input_curves.push(parsed.0)
pos = parsed.1
}
let clut_values = cells * output_channels
let clut_width = if sixteen_bit { 2 } else { 1 }
if clut_values > (tag_end - pos) / clut_width {
return Err(IccTruncated("LUT CLUT is truncated"))
}
let clut = Array::makei(clut_values, i => {
if sixteen_bit {
icc_u16(data, pos + i * 2, tag_end).unwrap_or(0).to_double() / 65535.0
} else {
icc_u8(data, pos + i, tag_end).unwrap_or(0).to_double() / 255.0
}
})
pos += clut_values * clut_width
let output_curves : Array[IccCurve] = []
for _ in 0.. value
Err(issue) => return Err(issue)
}
output_curves.push(parsed.0)
pos = parsed.1
}
let matrix = Array::makei(9, i => {
icc_s15f16(data, p + 12 + i * 4, tag_end).unwrap_or(0.0)
})
if !icc_matrix_is_identity(matrix) {
return Err(IccUnsupported("mft matrix is supported only when identity"))
}
Ok({
input_channels,
output_channels,
grids: Array::makei(input_channels, _ => grid),
has_clut: true,
clut,
input_curves,
middle_curves: Array::makei(output_channels, _ => IccIdentity),
output_curves,
matrix,
translation: [0.0, 0.0, 0.0],
pcs_lab,
pcs_xyz_scale: if pcs_xyz {
if sixteen_bit {
65535.0 / 32768.0
} else {
255.0 / 128.0
}
} else {
1.0
},
// lut16Type's PCS Lab signature is the legacy FF00-domain encoding in
// both v2 and v4 ICC. The profile version does not switch this tag form.
lab_legacy: pcs_lab && sixteen_bit,
})
}
///|
fn icc_parse_curve_sequence(
data : Array[Byte],
at : Int,
count : Int,
limit : Int,
) -> Result[(Array[IccCurve], Int), IccIssue] {
let curves : Array[IccCurve] = []
let mut pos = at
for _ in 0.. value
Err(issue) => return Err(issue)
}
curves.push(parsed.0)
pos = parsed.1
}
Ok((curves, pos))
}
///|
fn icc_parse_mab(
data : Array[Byte],
tag : IccTag,
limit : Int,
pcs_lab : Bool,
pcs_xyz : Bool,
lab_legacy : Bool,
) -> Result[IccLut, IccIssue] {
if tag.size < 32 || !icc_has_range(tag.offset, tag.size, limit) {
return Err(IccInvalid("mAB LUT header has an invalid size"))
}
let p = tag.offset
let input_channels = icc_u8(data, p + 8, limit).unwrap_or(0)
let output_channels = icc_u8(data, p + 9, limit).unwrap_or(0)
if (input_channels != 1 && input_channels != 3) || output_channels != 3 {
return Err(
IccUnsupported("mAB LUT must have one or three inputs and three outputs"),
)
}
let b_offset = match icc_u32(data, p + 12, limit) {
Some(value) => value
None => return Err(IccInvalid("mAB B-curve offset is invalid"))
}
let matrix_offset = match icc_u32(data, p + 16, limit) {
Some(value) => value
None => return Err(IccInvalid("mAB matrix offset is invalid"))
}
let m_offset = match icc_u32(data, p + 20, limit) {
Some(value) => value
None => return Err(IccInvalid("mAB M-curve offset is invalid"))
}
let clut_offset = match icc_u32(data, p + 24, limit) {
Some(value) => value
None => return Err(IccInvalid("mAB CLUT offset is invalid"))
}
let a_offset = match icc_u32(data, p + 28, limit) {
Some(value) => value
None => return Err(IccInvalid("mAB A-curve offset is invalid"))
}
if b_offset == 0 ||
(a_offset == 0) != (clut_offset == 0) ||
(m_offset == 0) != (matrix_offset == 0) {
return Err(
IccInvalid(
"mAB requires B curves, paired A/CLUT and paired M/matrix stages",
),
)
}
let tag_end = p + tag.size
if clut_offset == 0 && input_channels != 3 {
return Err(IccUnsupported("mAB gray pipelines require a CLUT"))
}
let a_target = if a_offset == 0 {
None
} else {
Some(
match icc_mab_target(p, a_offset, tag_end) {
Ok(value) => value
Err(issue) => return Err(issue)
},
)
}
let b_target = if b_offset == 0 {
None
} else {
Some(
match icc_mab_target(p, b_offset, tag_end) {
Ok(value) => value
Err(issue) => return Err(issue)
},
)
}
let m_target = if m_offset == 0 {
None
} else {
Some(
match icc_mab_target(p, m_offset, tag_end) {
Ok(value) => value
Err(issue) => return Err(issue)
},
)
}
let matrix_target = if matrix_offset == 0 {
None
} else {
Some(
match icc_mab_target(p, matrix_offset, tag_end) {
Ok(value) => value
Err(issue) => return Err(issue)
},
)
}
let clut_target = if clut_offset == 0 {
None
} else {
Some(
match icc_mab_target(p, clut_offset, tag_end) {
Ok(value) => value
Err(issue) => return Err(issue)
},
)
}
let input_curves = if a_target is None {
Array::makei(input_channels, _ => IccIdentity)
} else {
let target = a_target.unwrap()
match icc_parse_curve_sequence(data, target, input_channels, tag_end) {
Ok(value) => value.0
Err(issue) => return Err(issue)
}
}
let mut middle_curves : Array[IccCurve] = []
let mut matrix : Array[Double] = []
let mut translation = [0.0, 0.0, 0.0]
if matrix_target is Some(target) {
if !icc_has_range(target, 48, tag_end) {
return Err(IccTruncated("mAB matrix is truncated"))
}
matrix = Array::makei(9, i => {
icc_s15f16(data, target + i * 4, tag_end).unwrap_or(0.0)
})
translation = Array::makei(3, i => {
icc_s15f16(data, target + 36 + i * 4, tag_end).unwrap_or(0.0)
})
}
if m_target is Some(target) {
middle_curves = match
icc_parse_curve_sequence(data, target, output_channels, tag_end) {
Ok(value) => value.0
Err(issue) => return Err(issue)
}
} else {
middle_curves = Array::makei(output_channels, _ => IccIdentity)
}
let (grids, clut) = if clut_target is None {
(
Array::makei(input_channels, _ => 2),
Array::makei(2 * output_channels, _ => 0.0),
)
} else {
let c = clut_target.unwrap()
if !icc_has_range(c, 20, tag_end) {
return Err(IccTruncated("mAB CLUT header is truncated"))
}
let grids = Array::makei(input_channels, channel => {
icc_u8(data, c + channel, tag_end).unwrap_or(0)
})
let precision = icc_u8(data, c + 16, tag_end).unwrap_or(0)
if precision != 1 && precision != 2 {
return Err(IccUnsupported("mAB CLUT precision is unsupported"))
}
let mut cells = 1
for dimension in grids {
if dimension < 2 ||
dimension > 65 ||
cells > ICC_MAX_CLUT_CELLS / dimension {
return Err(IccInvalid("mAB CLUT grid is outside supported bounds"))
}
cells *= dimension
}
if cells <= 0 {
return Err(IccInvalid("mAB CLUT grid is outside supported bounds"))
}
let count = cells * output_channels
let width = if precision == 2 { 2 } else { 1 }
if count > (tag_end - (c + 20)) / width {
return Err(IccTruncated("mAB CLUT values are truncated"))
}
let values = Array::makei(count, i => {
if precision == 2 {
icc_u16(data, c + 20 + i * 2, tag_end).unwrap_or(0).to_double() /
65535.0
} else {
icc_u8(data, c + 20 + i, tag_end).unwrap_or(0).to_double() / 255.0
}
})
(grids, values)
}
let output_curves = if b_target is None {
Array::makei(output_channels, _ => IccIdentity)
} else {
match
icc_parse_curve_sequence(
data,
b_target.unwrap(),
output_channels,
tag_end,
) {
Ok(value) => value.0
Err(issue) => return Err(issue)
}
}
Ok({
input_channels,
output_channels,
grids,
has_clut: clut_target is Some(_),
clut,
input_curves,
middle_curves,
output_curves,
matrix,
translation,
pcs_lab,
pcs_xyz_scale: if pcs_xyz {
// mAB B curves are always 16-bit curves; CLUT precision does not
// change the PCS encoding of their output.
65535.0 / 32768.0
} else {
1.0
},
lab_legacy,
})
}
///|
fn icc_parse_lut_tag(
data : Array[Byte],
tag : IccTag,
limit : Int,
pcs_lab : Bool,
pcs_xyz : Bool,
lab_legacy : Bool,
) -> Result[IccLut, IccIssue] {
let type_signature = match icc_tag_data_type(data, tag, limit) {
Ok(value) => value
Err(issue) => return Err(issue)
}
match type_signature {
ICC_SIG_MFT1 =>
if pcs_xyz {
Err(IccUnsupported("mft1 PCS XYZ scaling is implementation-specific"))
} else {
icc_parse_mft(data, tag, limit, false, pcs_lab, pcs_xyz)
}
ICC_SIG_MFT2 => icc_parse_mft(data, tag, limit, true, pcs_lab, pcs_xyz)
ICC_SIG_MAB =>
if lab_legacy {
Err(IccUnsupported("mAB LUTs are supported only for ICC v4 profiles"))
} else {
icc_parse_mab(data, tag, limit, pcs_lab, pcs_xyz, lab_legacy)
}
_ => Err(IccUnsupported("A2B0 LUT type is unsupported"))
}
}
///|
fn icc_parse_intent_pipeline(
data : Array[Byte],
tag : IccTag,
limit : Int,
pcs_lab : Bool,
pcs_xyz : Bool,
lab_legacy : Bool,
) -> Result[IccPipeline, IccIssue] {
match icc_parse_lut_tag(data, tag, limit, pcs_lab, pcs_xyz, lab_legacy) {
Ok(lut) => Ok(IccLutPipeline(lut))
Err(issue) => Err(issue)
}
}
///|
fn icc_apply_bradford(
x : Double,
y : Double,
z : Double,
source_white : (Double, Double, Double),
) -> (Double, Double, Double) {
let source_l = 0.8951 * source_white.0 +
0.2664 * source_white.1 -
0.1614 * source_white.2
let source_m = -0.7502 * source_white.0 +
1.7135 * source_white.1 +
0.0367 * source_white.2
let source_s = 0.0389 * source_white.0 -
0.0685 * source_white.1 +
1.0296 * source_white.2
let target_x = 0.3127 / 0.3290
let target_z = (1.0 - 0.3127 - 0.3290) / 0.3290
let target_l = 0.8951 * target_x + 0.2664 - 0.1614 * target_z
let target_m = -0.7502 * target_x + 1.7135 + 0.0367 * target_z
let target_s = 0.0389 * target_x - 0.0685 + 1.0296 * target_z
let cone_x = 0.8951 * x + 0.2664 * y - 0.1614 * z
let cone_y = -0.7502 * x + 1.7135 * y + 0.0367 * z
let cone_z = 0.0389 * x - 0.0685 * y + 1.0296 * z
let adapted_l = cone_x * target_l / source_l
let adapted_m = cone_y * target_m / source_m
let adapted_s = cone_z * target_s / source_s
(
0.9869929 * adapted_l - 0.1470543 * adapted_m + 0.1599627 * adapted_s,
0.4323053 * adapted_l + 0.5183603 * adapted_m + 0.0492912 * adapted_s,
-0.0085287 * adapted_l + 0.0400428 * adapted_m + 0.9684867 * adapted_s,
)
}
///|
fn icc_xyz_to_srgb(
x : Double,
y : Double,
z : Double,
source_white : (Double, Double, Double),
) -> (Double, Double, Double) {
let adapted = icc_apply_bradford(x, y, z, source_white)
let r = 3.2409699419045226 * adapted.0 -
1.537383177570094 * adapted.1 -
0.4986107602930034 * adapted.2
let g = -0.9692436362808796 * adapted.0 +
1.8759675015077202 * adapted.1 +
0.04155505740717559 * adapted.2
let b = 0.05563007969699366 * adapted.0 -
0.20397695888897652 * adapted.1 +
1.0569715142428786 * adapted.2
(icc_srgb_encode(r), icc_srgb_encode(g), icc_srgb_encode(b))
}
///|
fn icc_srgb_encode(value : Double) -> Double {
let x = if value <= 0.0 { 0.0 } else { value }
if x <= 0.0031308 {
12.92 * x
} else {
1.055 * @math.pow(x, 1.0 / 2.4) - 0.055
}
}
///|
fn icc_lab_to_xyz(
l_encoded : Double,
a_encoded : Double,
b_encoded : Double,
lab_legacy : Bool,
) -> (Double, Double, Double) {
let l_scale = if lab_legacy { 65535.0 / 65280.0 } else { 1.0 }
// Legacy PCS Lab uses FF00 as the nominal 16-bit maximum for all three
// components. Converting to the current normalized PCS domain first keeps
// 8000 exactly neutral for a*/b* and FF00 exactly 127 for those channels.
let ab_scale = if lab_legacy { 65535.0 / 65280.0 * 255.0 } else { 255.0 }
let l = l_encoded * l_scale * 100.0
let a = a_encoded * ab_scale - 128.0
let b = b_encoded * ab_scale - 128.0
let fy = (l + 16.0) / 116.0
let fx = fy + a / 500.0
let fz = fy - b / 200.0
let delta = 6.0 / 29.0
let inverse = fn(value : Double) -> Double {
if value > delta {
value * value * value
} else {
3.0 * delta * delta * (value - 4.0 / 29.0)
}
}
(0.9642 * inverse(fx), inverse(fy), 0.8249 * inverse(fz))
}
///|
fn icc_lut_index(lut : IccLut, coordinates : Array[Int]) -> Int {
let mut index = 0
for dimension in 0.. Array[Double] {
let lows = Array::makei(lut.input_channels, i => {
let position = icc_clamp(values[i]) * (lut.grids[i] - 1).to_double()
position.to_int()
})
let fractions = Array::makei(lut.input_channels, i => {
let position = icc_clamp(values[i]) * (lut.grids[i] - 1).to_double()
let low = position.to_int()
if low >= lut.grids[i] - 1 {
0.0
} else {
position - low.to_double()
}
})
let output = Array::make(lut.output_channels, 0.0)
if lut.input_channels == 3 {
// Tetrahedral interpolation uses the simplex containing the three
// fractional coordinates. ICC's first input dimension varies slowest.
let order = [0, 1, 2]
for i in 0..<2 {
for j in (i + 1)..<3 {
if fractions[order[j]] > fractions[order[i]] {
let saved = order[i]
order[i] = order[j]
order[j] = saved
}
}
}
let coordinates = lows.copy()
for vertex in 0..<4 {
let weight = if vertex == 0 {
1.0 - fractions[order[0]]
} else if vertex == 3 {
fractions[order[2]]
} else {
fractions[order[vertex - 1]] - fractions[order[vertex]]
}
let base = icc_lut_index(lut, coordinates) * lut.output_channels
for channel in 0..> channel) & 1) != 0
let coordinate = if high { lows[channel] + 1 } else { lows[channel] }
let bounded = if coordinate >= lut.grids[channel] {
lut.grids[channel] - 1
} else {
coordinate
}
weight *= if high { fractions[channel] } else { 1.0 - fractions[channel] }
index = index * lut.grids[channel] + bounded
}
let base = index * lut.output_channels
for channel in 0.. Array[Double] {
let a_values = Array::makei(lut.input_channels, i => {
icc_curve_eval(lut.input_curves[i], encoded[i])
})
let mut values = if lut.has_clut {
icc_lut_clut(lut, a_values)
} else {
a_values
}
values = Array::makei(lut.output_channels, i => {
icc_curve_eval(lut.middle_curves[i], values[i])
})
if lut.matrix.length() == 9 {
let r = values[0]
let g = values[1]
let b = values[2]
values = [
lut.matrix[0] * r +
lut.matrix[1] * g +
lut.matrix[2] * b +
lut.translation[0],
lut.matrix[3] * r +
lut.matrix[4] * g +
lut.matrix[5] * b +
lut.translation[1],
lut.matrix[6] * r +
lut.matrix[7] * g +
lut.matrix[8] * b +
lut.translation[2],
]
}
values = Array::makei(lut.output_channels, i => {
icc_curve_eval(lut.output_curves[i], values[i])
})
values
}
///|
fn icc_profile_xyz(
pipeline : IccPipeline,
encoded : Array[Double],
) -> (Double, Double, Double) {
match pipeline {
IccMatrixRgb(matrix, rtrc, gtrc, btrc) => {
let r = icc_curve_eval(rtrc, encoded[0])
let g = icc_curve_eval(gtrc, encoded[1])
let b = icc_curve_eval(btrc, encoded[2])
(
matrix[0] * r + matrix[1] * g + matrix[2] * b,
matrix[3] * r + matrix[4] * g + matrix[5] * b,
matrix[6] * r + matrix[7] * g + matrix[8] * b,
)
}
IccMatrixGray(white, trc) => {
let value = icc_curve_eval(trc, encoded[0])
(white[0] * value, white[1] * value, white[2] * value)
}
IccLutPipeline(lut) => {
let values = icc_lut_eval(lut, encoded)
if lut.pcs_lab {
icc_lab_to_xyz(values[0], values[1], values[2], lut.lab_legacy)
} else {
(
values[0] * lut.pcs_xyz_scale,
values[1] * lut.pcs_xyz_scale,
values[2] * lut.pcs_xyz_scale,
)
}
}
}
}
///|
fn icc_pipeline_white(pipeline : IccPipeline) -> (Double, Double, Double) {
match pipeline {
IccMatrixGray(white, _) => (white[0], white[1], white[2])
_ => (0.9642, 1.0, 0.8249)
}
}
///|
fn icc_bpc_xyz(
xyz : (Double, Double, Double),
black : (Double, Double, Double),
) -> (Double, Double, Double) {
let map = fn(
value : Double,
source_black : Double,
white : Double,
) -> Double {
(value - source_black) * white / (white - source_black)
}
(
map(xyz.0, black.0, 0.9642),
map(xyz.1, black.1, 1.0),
map(xyz.2, black.2, 0.8249),
)
}
///|
fn icc_bpc_valid(black : (Double, Double, Double)) -> Bool {
let valid = fn(value : Double, white : Double) -> Bool {
let span = white - value
!value.is_nan() &&
!value.is_inf() &&
!span.is_nan() &&
!span.is_inf() &&
span.abs() > 1.0e-12
}
valid(black.0, 0.9642) && valid(black.1, 1.0) && valid(black.2, 0.8249)
}
///|
fn icc_clip_black_lightness(
xyz : (Double, Double, Double),
) -> (Double, Double, Double) {
let fx = fn(value : Double) -> Double {
if value > 216.0 / 24389.0 {
@math.pow(value, 1.0 / 3.0)
} else {
841.0 / 108.0 * value + 16.0 / 116.0
}
}
let fx_value = fx(xyz.0 / 0.9642)
let fy_value = fx(xyz.1)
let fz_value = fx(xyz.2 / 0.8249)
let l = 116.0 * fy_value - 16.0
let clipped_l = if l > 95.0 || l < 0.0 {
0.0
} else if l > 50.0 {
50.0
} else {
l
}
let fy_clipped = (clipped_l + 16.0) / 116.0
let delta = 6.0 / 29.0
let inverse = fn(value : Double) -> Double {
if value > delta {
value * value * value
} else {
3.0 * delta * delta * (value - 4.0 / 29.0)
}
}
(
0.9642 * inverse(fy_clipped + fx_value - fy_value),
inverse(fy_clipped),
0.8249 * inverse(fy_clipped + fz_value - fy_value),
)
}
///|
fn icc_detect_black_point(
profile : IccProfile,
selected : IccPipeline,
intent : IccRenderingIntent,
) -> (Double, Double, Double) {
let perceptual = match intent {
Perceptual | Saturation => true
_ => false
}
if !profile.lab_legacy && perceptual && !profile.matrix_shaper {
return (0.00336, 0.0034731, 0.00287)
}
// LittleCMS's black detector first checks the requested input intent,
// independently of the input-LUT reader's A2B0 fallback.
let supported = profile.matrix_shaper ||
(match intent {
Perceptual => profile.perceptual_tag is Some(_)
RelativeColorimetric => icc_tag(profile.tags, ICC_SIG_A2B1) is Some(_)
Saturation => profile.saturation_tag is Some(_)
AbsoluteColorimetric => false
})
if !supported {
return (0.0, 0.0, 0.0)
}
let pipeline = if !profile.lab_legacy && perceptual && profile.matrix_shaper {
profile.pipeline
} else {
selected
}
let xyz = match pipeline {
IccMatrixGray(_, trc) => {
let value = icc_curve_eval(trc, 0.0)
(0.9642 * value, value, 0.8249 * value)
}
_ => {
let encoded = if profile.color_space == ICC_SIG_GRAY {
[0.0]
} else {
[0.0, 0.0, 0.0]
}
icc_profile_xyz(pipeline, encoded)
}
}
icc_clip_black_lightness(xyz)
}
///|
fn icc_profile_pixel(
profile : IccProfile,
pipeline : IccPipeline,
black_point : (Double, Double, Double)?,
absolute_white : (Double, Double, Double)?,
r : Double,
g : Double,
b : Double,
) -> (Double, Double, Double) {
let encoded = if profile.color_space == ICC_SIG_GRAY {
[r]
} else {
[r, g, b]
}
let policy_pcs = black_point is Some(_) || absolute_white is Some(_)
let xyz = if policy_pcs {
match pipeline {
IccMatrixGray(_, trc) => {
let value = icc_curve_eval(trc, encoded[0])
(0.9642 * value, value, 0.8249 * value)
}
_ => icc_profile_xyz(pipeline, encoded)
}
} else {
icc_profile_xyz(pipeline, encoded)
}
let absolute_xyz = match absolute_white {
Some(white) =>
(xyz.0 * white.0 / 0.9642, xyz.1 * white.1, xyz.2 * white.2 / 0.8249)
None => xyz
}
let mapped_xyz = match black_point {
Some(black) => icc_bpc_xyz(absolute_xyz, black)
None => absolute_xyz
}
let white = if policy_pcs {
(0.9642, 1.0, 0.8249)
} else {
icc_pipeline_white(pipeline)
}
icc_xyz_to_srgb(mapped_xyz.0, mapped_xyz.1, mapped_xyz.2, white)
}
///|
fn icc_image_valid(image : Image) -> Bool {
if image.width < 0 ||
image.height < 0 ||
image.width == 0 ||
image.height == 0 {
image.width >= 0 && image.height >= 0 && image.data.length() == 0
} else if image.width > MAX_IMAGE_PIXELS / image.height {
false
} else {
image.data.length() == image.width * image.height * 4
}
}
///|
fn icc_image16_valid(image : Image16) -> Bool {
if image.width < 0 ||
image.height < 0 ||
image.width == 0 ||
image.height == 0 {
image.width >= 0 && image.height >= 0 && image.data.length() == 0
} else if image.width > MAX_IMAGE_PIXELS / image.height {
false
} else {
image.data.length() == image.width * image.height * 4
}
}
///|
fn icc_quantize8(value : Double) -> Byte {
let clipped = icc_clamp(value)
(clipped * 255.0 + 0.5).to_int().to_byte()
}
///|
fn icc_quantize16(value : Double) -> UInt16 {
let clipped = icc_clamp(value)
(clipped * 65535.0 + 0.5).to_int().to_uint16()
}
///|
/// Parse and validate a pure RGB or Gray ICC v2/v4 input/display profile.
pub fn IccProfile::parse(
bytes : Array[Byte],
) -> Result[IccProfile, DecodeError] {
if bytes.length() > 64 * 1024 * 1024 {
return Err(decode_error(LimitExceeded, "icc", "ICC input exceeds 64 MiB"))
}
if bytes.length() < 132 {
return Err(icc_error(IccTruncated("ICC header or tag table is truncated")))
}
let declared_size = match icc_u32(bytes, 0, bytes.length()) {
Some(value) => value
None => return Err(icc_error(IccTruncated("ICC profile size is truncated")))
}
if declared_size < 132 {
return Err(icc_error(IccInvalid("ICC profile size is too small")))
}
if declared_size > bytes.length() {
return Err(
icc_error(IccTruncated("ICC profile is shorter than its declared size")),
)
}
let version = bytes[8].to_int()
if version != 2 && version != 4 {
return Err(
icc_error(IccUnsupported("Only ICC v2 and v4 profiles are supported")),
)
}
let device_class = match icc_u32(bytes, 12, declared_size) {
Some(value) => value
None => return Err(icc_error(IccTruncated("ICC device class is truncated")))
}
if device_class != ICC_SIG_SCNR && device_class != ICC_SIG_MNTR {
let message = if device_class == ICC_SIG_PRTR ||
device_class == ICC_SIG_LINK {
"Printer and device-link ICC profiles are unsupported"
} else if device_class == ICC_SIG_SPAC || device_class == ICC_SIG_NMCL {
"N-component and named-color ICC profiles are unsupported"
} else {
"ICC profile class is unsupported"
}
return Err(icc_error(IccUnsupported(message)))
}
if icc_u32(bytes, 36, declared_size).unwrap_or(0) != ICC_SIG_ACSP {
return Err(icc_error(IccInvalid("ICC profile signature is not acsp")))
}
let color_space = icc_u32(bytes, 16, declared_size).unwrap_or(0)
if color_space != ICC_SIG_RGB && color_space != ICC_SIG_GRAY {
return Err(icc_error(IccUnsupported("ICC color space is not RGB or Gray")))
}
let pcs = icc_u32(bytes, 20, declared_size).unwrap_or(0)
if pcs != ICC_SIG_XYZ && pcs != ICC_SIG_LAB {
return Err(icc_error(IccUnsupported("ICC PCS is not XYZ or Lab")))
}
let tag_count = match icc_u32(bytes, 128, declared_size) {
Some(value) => value
None => return Err(icc_error(IccTruncated("ICC tag count is truncated")))
}
if tag_count > 4096 {
return Err(decode_error(LimitExceeded, "icc", "Profile exceeds 4096 tags"))
}
if tag_count > (declared_size - 132) / 12 {
return Err(icc_error(IccTruncated("ICC tag table is truncated")))
}
let tag_table_end = 132 + tag_count * 12
let tags : Array[IccTag] = []
for i in 0.. Some(tag)
None => icc_tag(tags, ICC_SIG_A2B0)
}
let pipeline = match relative_tag {
Some(a2b) =>
match
icc_parse_intent_pipeline(
bytes,
a2b,
declared_size,
pcs == ICC_SIG_LAB,
pcs == ICC_SIG_XYZ,
lab_legacy,
) {
Ok(value) => {
let channels_match = match value {
IccLutPipeline(lut) =>
(color_space == ICC_SIG_RGB && lut.input_channels == 3) ||
(color_space == ICC_SIG_GRAY && lut.input_channels == 1)
_ => false
}
if !channels_match {
return Err(
icc_error(
IccUnsupported(
"A2B0 channel count does not match the profile color space",
),
),
)
}
value
}
Err(issue) => return Err(icc_error(issue))
}
None =>
if pcs == ICC_SIG_LAB {
return Err(icc_error(IccUnsupported("Lab PCS requires an A2B0 LUT")))
} else if color_space == ICC_SIG_RGB {
match icc_matrix_from_tags(bytes, tags, declared_size) {
Ok(value) => value
Err(issue) => return Err(icc_error(issue))
}
} else {
match icc_parse_gray_pipeline(bytes, tags, declared_size) {
Ok(value) => value
Err(issue) => return Err(icc_error(issue))
}
}
}
let matrix_shaper = if color_space == ICC_SIG_GRAY {
icc_tag(tags, ICC_SIG_KTRC) is Some(_)
} else {
icc_tag(tags, ICC_SIG_RXYZ) is Some(_) &&
icc_tag(tags, ICC_SIG_GXYZ) is Some(_) &&
icc_tag(tags, ICC_SIG_BXYZ) is Some(_) &&
icc_tag(tags, ICC_SIG_RTRC) is Some(_) &&
icc_tag(tags, ICC_SIG_GTRC) is Some(_) &&
icc_tag(tags, ICC_SIG_BTRC) is Some(_)
}
let perceptual_tag = icc_tag(tags, ICC_SIG_A2B0)
let saturation_tag = icc_tag(tags, ICC_SIG_A2B2)
let source_bytes = bytes.copy()
Ok({
color_space,
pipeline,
source_bytes,
declared_size,
tags,
perceptual_tag,
saturation_tag,
relative_tag,
device_class,
matrix_shaper,
pcs_lab: pcs == ICC_SIG_LAB,
pcs_xyz: pcs == ICC_SIG_XYZ,
lab_legacy,
})
}
///|
fn icc_absolute_white(
profile : IccProfile,
) -> Result[(Double, Double, Double), DecodeError] {
// ICC v2 display profiles report their adapted D50 media white.
if profile.lab_legacy && profile.device_class == ICC_SIG_MNTR {
return Ok((0.9642, 1.0, 0.8249))
}
match icc_tag(profile.tags, ICC_SIG_WTPT) {
None => Ok((0.9642, 1.0, 0.8249))
Some(tag) =>
match
icc_parse_xyz_tag(profile.source_bytes, tag, profile.declared_size) {
Ok(white) =>
if icc_white_valid(white) {
Ok(white)
} else {
Err(
icc_error(
IccInvalid(
"Absolute intent requires a finite positive media white",
),
),
)
}
Err(issue) => Err(icc_error(issue))
}
}
}
///|
fn icc_pipeline_for_intent(
profile : IccProfile,
intent : IccRenderingIntent,
) -> Result[IccPipeline, DecodeError] {
let parse_optional = fn(tag : IccTag?) -> Result[IccPipeline?, DecodeError] {
match tag {
None => Ok(None)
Some(value) => {
if profile.relative_tag is Some(relative) &&
value.offset == relative.offset &&
value.size == relative.size {
return Ok(Some(profile.pipeline))
}
match
icc_parse_intent_pipeline(
profile.source_bytes,
value,
profile.declared_size,
profile.pcs_lab,
profile.pcs_xyz,
profile.lab_legacy,
) {
Ok(pipeline) => {
let channels_match = match pipeline {
IccLutPipeline(lut) =>
(profile.color_space == ICC_SIG_RGB && lut.input_channels == 3) ||
(profile.color_space == ICC_SIG_GRAY && lut.input_channels == 1)
_ => false
}
if !channels_match {
Err(
icc_error(
IccUnsupported(
"Intent LUT channel count does not match profile color space",
),
),
)
} else {
Ok(Some(pipeline))
}
}
Err(issue) => Err(icc_error(issue))
}
}
}
}
let matrix_fallback = fn() -> Result[IccPipeline, DecodeError] {
match profile.pipeline {
IccMatrixRgb(_, _, _, _) | IccMatrixGray(_, _) => Ok(profile.pipeline)
_ => {
if !profile.matrix_shaper || !profile.pcs_xyz {
return Err(
icc_error(
IccUnsupported(
"Requested intent has neither an A2B table nor a matrix/TRC transform",
),
),
)
}
let parsed = if profile.color_space == ICC_SIG_RGB {
icc_matrix_from_tags(
profile.source_bytes,
profile.tags,
profile.declared_size,
)
} else {
icc_parse_gray_pipeline(
profile.source_bytes,
profile.tags,
profile.declared_size,
)
}
match parsed {
Ok(value) => Ok(value)
Err(issue) => Err(icc_error(issue))
}
}
}
}
match intent {
RelativeColorimetric => Ok(profile.pipeline)
// LittleCMS's fixed-point Device2PCS table map uses A2B0 for perceptual
// fallback and the matrix/gray shaper when no LUT exists.
Perceptual =>
match parse_optional(profile.perceptual_tag) {
Ok(Some(pipeline)) => Ok(pipeline)
Ok(None) => matrix_fallback()
Err(error) => Err(error)
}
Saturation =>
match
parse_optional(
match profile.saturation_tag {
Some(tag) => Some(tag)
None => profile.perceptual_tag
},
) {
Ok(Some(pipeline)) => Ok(pipeline)
Ok(None) => matrix_fallback()
Err(error) => Err(error)
}
// cmsio1.c maps fixed-point absolute input transforms to A2B1. The
// absolute media-white adaptation is applied after this shared pipeline.
AbsoluteColorimetric => Ok(profile.pipeline)
}
}
///|
fn icc_black_point_for_options(
profile : IccProfile,
pipeline : IccPipeline,
options : IccTransformOptions,
) -> Result[(Double, Double, Double)?, DecodeError] {
if options.intent is AbsoluteColorimetric {
if options.black_point_compensation {
Err(
icc_error(
IccUnsupported(
"Black-point compensation is disabled for absolute colorimetric transforms",
),
),
)
} else {
Ok(None)
}
} else {
let forced = match options.intent {
// The fixed sRGB destination is v4, so the connection always applies
// perceptual/saturation black normalization, including v2 sources.
Perceptual | Saturation => true
_ => false
}
if !options.black_point_compensation && !forced {
Ok(None)
} else {
let point = icc_detect_black_point(profile, pipeline, options.intent)
if !icc_bpc_valid(point) {
Err(
icc_error(
IccInvalid(
"Black-point compensation black point has an invalid XYZ denominator",
),
),
)
} else {
Ok(Some(point))
}
}
}
}
///|
/// Convert straight RGBA8 to a fresh sRGB RGBA8 image, preserving alpha.
pub fn IccProfile::apply_rgba8(
self : IccProfile,
image : Image,
) -> Result[Image, DecodeError] {
self.apply_rgba8_with_options(
image,
IccTransformOptions::relative_colorimetric(),
)
}
///|
/// Convert straight RGBA8 using an explicitly selected ICC policy.
pub fn IccProfile::apply_rgba8_with_options(
self : IccProfile,
image : Image,
options : IccTransformOptions,
) -> Result[Image, DecodeError] {
if !icc_image_valid(image) {
return Err(
decode_error(
InvalidBuffer,
"icc/image",
"RGBA8 image buffer does not match its dimensions",
),
)
}
let pipeline = match icc_pipeline_for_intent(self, options.intent) {
Ok(value) => value
Err(error) => return Err(error)
}
let black_point = match icc_black_point_for_options(self, pipeline, options) {
Ok(value) => value
Err(error) => return Err(error)
}
let absolute_white = match options.intent {
AbsoluteColorimetric =>
match icc_absolute_white(self) {
Ok(white) => Some(white)
Err(error) => return Err(error)
}
_ => None
}
let output = FixedArray::make(image.data.length(), b'\x00')
for i in 0..<(image.width * image.height) {
let source = i * 4
let converted = icc_profile_pixel(
self,
pipeline,
black_point,
absolute_white,
image.data[source].to_int().to_double() / 255.0,
image.data[source + 1].to_int().to_double() / 255.0,
image.data[source + 2].to_int().to_double() / 255.0,
)
if converted.0.is_nan() ||
converted.1.is_nan() ||
converted.2.is_nan() ||
converted.0.is_inf() ||
converted.1.is_inf() ||
converted.2.is_inf() {
return Err(
decode_error(
InvalidData,
"icc/transform",
"Profile produces non-finite color",
),
)
}
output[source] = icc_quantize8(converted.0)
output[source + 1] = icc_quantize8(converted.1)
output[source + 2] = icc_quantize8(converted.2)
output[source + 3] = image.data[source + 3]
}
Ok({ width: image.width, height: image.height, data: output, })
}
///|
/// Convert straight RGBA16 to a fresh sRGB RGBA16 image, preserving alpha.
pub fn IccProfile::apply_rgba16(
self : IccProfile,
image : Image16,
) -> Result[Image16, DecodeError] {
self.apply_rgba16_with_options(
image,
IccTransformOptions::relative_colorimetric(),
)
}
///|
/// Convert straight RGBA16 using an explicitly selected ICC policy.
pub fn IccProfile::apply_rgba16_with_options(
self : IccProfile,
image : Image16,
options : IccTransformOptions,
) -> Result[Image16, DecodeError] {
if !icc_image16_valid(image) {
return Err(
decode_error(
InvalidBuffer,
"icc/image16",
"RGBA16 image buffer does not match its dimensions",
),
)
}
let pipeline = match icc_pipeline_for_intent(self, options.intent) {
Ok(value) => value
Err(error) => return Err(error)
}
let black_point = match icc_black_point_for_options(self, pipeline, options) {
Ok(value) => value
Err(error) => return Err(error)
}
let absolute_white = match options.intent {
AbsoluteColorimetric =>
match icc_absolute_white(self) {
Ok(white) => Some(white)
Err(error) => return Err(error)
}
_ => None
}
let output = FixedArray::make(image.data.length(), (0 : UInt16))
for i in 0..<(image.width * image.height) {
let source = i * 4
let converted = icc_profile_pixel(
self,
pipeline,
black_point,
absolute_white,
image.data[source].to_int().to_double() / 65535.0,
image.data[source + 1].to_int().to_double() / 65535.0,
image.data[source + 2].to_int().to_double() / 65535.0,
)
if converted.0.is_nan() ||
converted.1.is_nan() ||
converted.2.is_nan() ||
converted.0.is_inf() ||
converted.1.is_inf() ||
converted.2.is_inf() {
return Err(
decode_error(
InvalidData,
"icc/transform",
"Profile produces non-finite color",
),
)
}
output[source] = icc_quantize16(converted.0)
output[source + 1] = icc_quantize16(converted.1)
output[source + 2] = icc_quantize16(converted.2)
output[source + 3] = image.data[source + 3]
}
Ok({ width: image.width, height: image.height, data: output, })
}