///|
/// H.273 (07/2024), Tables 2--4 and equations 55--87. Values here are
/// normalized nonlinear luma and signed chroma, before integer rounding.
/// These conversions preserve the source primaries and transfer function;
/// display gamut conversion and tone mapping are separate operations.
fn av1_matrix_supported(matrix : Int) -> Bool {
matrix >= 0 && matrix <= 14 && matrix != 3
}
///|
fn av1_color_transfer_supported(transfer : Int) -> Bool {
transfer == 1 || (transfer >= 4 && transfer <= 18)
}
///|
/// Chromaticity-derived Y coefficients from H.273 Table 2. These constants
/// were evaluated in double precision by solving P*s = (xw,yw,zw)/yw,
/// where each column of P is (xp,yp,1-xp-yp), then taking (yr*sr,yb*sb).
/// Keeping the result avoids rebuilding/inverting a matrix for each pixel.
/// Unspecified and reserved primaries deliberately have no guessed values.
fn av1_color_derived_coefficients(primaries : Int) -> (Double, Double)? {
match primaries {
1 => Some((0.21263900587151033, 0.07219231536073371))
4 => Some((0.29896661812478992, 0.11461217174222660))
5 => Some((0.22200430999823087, 0.07134092407648639))
6 | 7 => Some((0.21237636070506746, 0.08656378236920959))
8 => Some((0.25358536343373467, 0.06807886086929262))
9 => Some((0.26270021201126698, 0.05930171646986195))
10 => Some((0.0, 0.0))
11 => Some((0.20949167791273055, 0.06891306792622581))
12 => Some((0.22897456406974875, 0.07928691409374498))
22 => Some((0.23175054567210918, 0.09599868152322844))
_ => None
}
}
///|
fn av1_color_supported(matrix : Int, primaries : Int, transfer : Int) -> Bool {
match matrix {
10 => av1_color_transfer_supported(transfer)
12 => av1_color_derived_coefficients(primaries) is Some(_)
13 =>
av1_color_derived_coefficients(primaries) is Some(_) &&
av1_color_transfer_supported(transfer)
14 => transfer == 16 || transfer == 18
_ => av1_matrix_supported(matrix)
}
}
///|
/// Inverse of H.273 Table 3, using its continuous-join constants.
/// PQ is normalized to its 10000-nit peak; HLG uses scene-linear light
/// without an OOTF. Log curves lose the darkest interval: encoded zero is
/// reconstructed as black. Curves without a negative domain clip at black.
/// PQ values beyond its finite inverse domain return None.
fn av1_color_linear(value : Double, transfer : Int) -> Double? {
let positive = if value < 0.0 { 0.0 } else { value }
Some(
match transfer {
1 | 6 | 14 | 15 =>
if positive < 4.5 * 0.018053968510807 {
positive / 4.5
} else {
@math.pow(
(positive + 0.099296826809442) / 1.099296826809442,
1.0 / 0.45,
)
}
4 => @math.pow(positive, 2.2)
5 => @math.pow(positive, 2.8)
7 =>
if positive < 4.0 * 0.022821585529445 {
positive / 4.0
} else {
@math.pow(
(positive + 0.111572195921731) / 1.111572195921731,
1.0 / 0.45,
)
}
8 => value
9 => if value <= 0.0 { 0.0 } else { @math.pow(10.0, 2.0 * (value - 1.0)) }
10 =>
if value <= 0.0 {
0.0
} else {
@math.pow(10.0, 2.5 * (value - 1.0))
}
11 =>
if value < -4.5 * 0.018053968510807 {
-@math.pow(
(-value + 0.099296826809442) / 1.099296826809442,
1.0 / 0.45,
)
} else if value < 4.5 * 0.018053968510807 {
value / 4.5
} else {
@math.pow((value + 0.099296826809442) / 1.099296826809442, 1.0 / 0.45)
}
12 =>
if value < -4.5 * 0.018053968510807 / 4.0 {
-@math.pow(
(-4.0 * value + 0.099296826809442) / 1.099296826809442,
1.0 / 0.45,
) /
4.0
} else if value < 4.5 * 0.018053968510807 {
value / 4.5
} else {
@math.pow((value + 0.099296826809442) / 1.099296826809442, 1.0 / 0.45)
}
13 => {
let magnitude = if value < 0.0 { -value } else { value }
let linear = if magnitude < 12.92 * 0.0030412825601275209 {
magnitude / 12.92
} else {
@math.pow((magnitude + 0.0550107189475866) / 1.0550107189475866, 2.4)
}
if value < 0.0 {
-linear
} else {
linear
}
}
16 => {
let power = @math.pow(positive, 1.0 / 78.84375)
let numerator = if power > 0.8359375 { power - 0.8359375 } else { 0.0 }
let denominator = 18.8515625 - 18.6875 * power
// H.273's PQ curve can extend above the nominal white of 1, but
// reaches an asymptote at (c2/c3)^m. At or beyond that point there
// is no finite nonnegative light value: reject instead of allowing
// a negative fractional-power base to become a NaN pixel.
if !(denominator > 0.0) {
return None
}
@math.pow(numerator / denominator, 1.0 / 0.1593017578125)
}
17 => @math.pow(positive, 2.6) * (52.37 / 48.0)
18 =>
if positive <= 0.5 {
positive * positive / 3.0
} else {
(@math.exp((positive - 0.55991073) / 0.17883277) + 0.28466892) / 12.0
}
_ => return None
},
)
}
///|
/// Forward H.273 transfer. Negative values are retained only by curves
/// defining a negative domain (linear, IEC 61966-2-4, BT.1361 and sYCC).
fn av1_color_nonlinear(value : Double, transfer : Int) -> Double? {
let positive = if value < 0.0 { 0.0 } else { value }
Some(
match transfer {
1 | 6 | 14 | 15 =>
if positive < 0.018053968510807 {
4.5 * positive
} else {
1.099296826809442 * @math.pow(positive, 0.45) - 0.099296826809442
}
4 => @math.pow(positive, 1.0 / 2.2)
5 => @math.pow(positive, 1.0 / 2.8)
7 =>
if positive < 0.022821585529445 {
4.0 * positive
} else {
1.111572195921731 * @math.pow(positive, 0.45) - 0.111572195921731
}
8 => value
9 => if value <= 0.01 { 0.0 } else { 1.0 + @math.log10(value) / 2.0 }
10 =>
if value <= 0.003162277660168379 {
0.0
} else {
1.0 + @math.log10(value) / 2.5
}
11 =>
if value < -0.018053968510807 {
-1.099296826809442 * @math.pow(-value, 0.45) + 0.099296826809442
} else if value < 0.018053968510807 {
4.5 * value
} else {
1.099296826809442 * @math.pow(value, 0.45) - 0.099296826809442
}
12 =>
if value < -0.018053968510807 / 4.0 {
-(1.099296826809442 * @math.pow(-4.0 * value, 0.45) -
0.099296826809442) /
4.0
} else if value < 0.018053968510807 {
4.5 * value
} else {
1.099296826809442 * @math.pow(value, 0.45) - 0.099296826809442
}
13 => {
let magnitude = if value < 0.0 { -value } else { value }
let nonlinear = if magnitude < 0.0030412825601275209 {
12.92 * magnitude
} else {
1.0550107189475866 * @math.pow(magnitude, 1.0 / 2.4) -
0.0550107189475866
}
if value < 0.0 {
-nonlinear
} else {
nonlinear
}
}
16 => {
let power = @math.pow(positive, 0.1593017578125)
@math.pow(
(0.8359375 + 18.8515625 * power) / (1.0 + 18.6875 * power),
78.84375,
)
}
17 => @math.pow(positive * (48.0 / 52.37), 1.0 / 2.6)
18 =>
if positive <= 1.0 / 12.0 {
(3.0 * positive).sqrt()
} else {
0.17883277 * @math.ln(12.0 * positive - 0.28466892) + 0.55991073
}
_ => return None
},
)
}
///|
fn av1_color_ncl(
y : Double,
u : Double,
v : Double,
kr : Double,
kb : Double,
) -> (Double, Double, Double) {
let r = y + 2.0 * (1.0 - kr) * v
let b = y + 2.0 * (1.0 - kb) * u
(r, (y - kr * r - kb * b) / (1.0 - kr - kb), b)
}
///|
fn av1_color_cl(
y : Double,
u : Double,
v : Double,
kr : Double,
kb : Double,
transfer : Int,
) -> (Double, Double, Double)? {
if !av1_color_transfer_supported(transfer) {
return None
}
let blue_scale = if u <= 0.0 {
av1_color_nonlinear(1.0 - kb, transfer).unwrap()
} else {
1.0 - av1_color_nonlinear(kb, transfer).unwrap()
}
let red_scale = if v <= 0.0 {
av1_color_nonlinear(1.0 - kr, transfer).unwrap()
} else {
1.0 - av1_color_nonlinear(kr, transfer).unwrap()
}
let r = y + 2.0 * red_scale * v
let b = y + 2.0 * blue_scale * u
let linear_y = match av1_color_linear(y, transfer) {
Some(value) => value
None => return None
}
let linear_r = match av1_color_linear(r, transfer) {
Some(value) => value
None => return None
}
let linear_b = match av1_color_linear(b, transfer) {
Some(value) => value
None => return None
}
let green_linear = (linear_y - kr * linear_r - kb * linear_b) /
(1.0 - kr - kb)
Some((r, av1_color_nonlinear(green_linear, transfer).unwrap(), b))
}
///|
fn av1_color_ictcp(
intensity : Double,
ct : Double,
cp : Double,
transfer : Int,
) -> (Double, Double, Double)? {
// Inverses of H.273 equations 81/82 and 86/87 (PQ and HLG).
let (lp, mp, sp) = match transfer {
16 =>
(
intensity + 0.008609037037932761 * ct + 0.11102962500302598 * cp,
intensity - 0.008609037037932761 * ct - 0.11102962500302598 * cp,
intensity + 0.56003133571067909 * ct - 0.32062717498731880 * cp,
)
18 =>
(
intensity + 0.015718580108730413 * ct + 0.20958106811640551 * cp,
intensity - 0.015718580108730413 * ct - 0.20958106811640551 * cp,
intensity + 1.0212710798422344 * ct - 0.60527449099243158 * cp,
)
_ => return None
}
let l = match av1_color_linear(lp, transfer) {
Some(value) => value
None => return None
}
let m = match av1_color_linear(mp, transfer) {
Some(value) => value
None => return None
}
let s = match av1_color_linear(sp, transfer) {
Some(value) => value
None => return None
}
// Inverse of the integer LMS matrix in H.273 equations 79 and 84.
// Every row sums to one. Expressing it with channel differences preserves
// the neutral axis exactly, including at the HLG piecewise-function join.
let r = l - 2.5064521186562705 * (m - l) + 0.069845424323191518 * (s - l)
let g = m - 0.79132955559892892 * (l - m) - 0.19227089619336202 * (s - m)
let b = s - 0.025949899690592672 * (l - s) - 0.098913714711726455 * (m - s)
Some(
(
av1_color_nonlinear(r, transfer).unwrap(),
av1_color_nonlinear(g, transfer).unwrap(),
av1_color_nonlinear(b, transfer).unwrap(),
),
)
}
///|
/// Identity (matrix 0) has uncentered G/B/R planes and is handled by the
/// caller. Matrix 2 explicitly uses the decoder's BT.601 default.
/// Matrix 11 recovers the source R/G/B or X/Y/Z components; the caller
/// applies any required XYZ-to-display conversion for primaries 10.
fn av1_color_rgb(
y : Double,
u : Double,
v : Double,
matrix : Int,
primaries : Int,
transfer : Int,
) -> (Double, Double, Double)? {
match matrix {
1 => Some(av1_color_ncl(y, u, v, 0.2126, 0.0722))
2 | 5 | 6 => Some(av1_color_ncl(y, u, v, 0.299, 0.114))
4 => Some(av1_color_ncl(y, u, v, 0.30, 0.11))
7 => Some(av1_color_ncl(y, u, v, 0.212, 0.087))
8 => Some((y - u + v, y + u, y - u - v))
9 => Some(av1_color_ncl(y, u, v, 0.2627, 0.0593))
10 => av1_color_cl(y, u, v, 0.2627, 0.0593, transfer)
11 => Some((2.0 * v + 0.991902 * y, y, (2.0 * u + y) / 0.986566))
12 | 13 => {
let (kr, kb) = match av1_color_derived_coefficients(primaries) {
Some(coefficients) => coefficients
None => return None
}
if matrix == 12 {
Some(av1_color_ncl(y, u, v, kr, kb))
} else {
av1_color_cl(y, u, v, kr, kb, transfer)
}
}
14 => av1_color_ictcp(y, u, v, transfer)
_ => None
}
}