use alloc::vec;
use alloc::vec::Vec;
use core::cmp::min;
use crate::policy::{AlphaPolicy, ConvertOptions, DepthPolicy, LumaCoefficients};
use crate::{
AlphaMode, ChannelLayout, ChannelType, ColorPrimaries, ConvertError, PixelDescriptor,
TransferFunction,
};
use whereat::{At, ResultAtExt};
#[cfg(feature = "hdr-experimental")]
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct HdrConfig {
pub source_peak_nits: f32,
pub target_peak_nits: f32,
pub gamut_knee: f32,
}
#[cfg(feature = "hdr-experimental")]
impl Default for HdrConfig {
fn default() -> Self {
Self {
source_peak_nits: 0.0,
target_peak_nits: 100.0,
gamut_knee: 0.96,
}
}
}
#[cfg(feature = "hdr-experimental")]
impl HdrConfig {
#[must_use]
pub fn for_source_peak(source_peak_nits: f32) -> Self {
Self {
source_peak_nits,
..Self::default()
}
}
#[must_use]
pub fn with_target_peak_nits(mut self, nits: f32) -> Self {
self.target_peak_nits = nits;
self
}
#[must_use]
pub fn with_gamut_knee(mut self, knee: f32) -> Self {
self.gamut_knee = knee;
self
}
}
#[cfg(feature = "hdr-experimental")]
fn is_hdr_to_sdr(from: TransferFunction, to: TransferFunction) -> bool {
let src_is_hdr = matches!(from, TransferFunction::Pq | TransferFunction::Hlg);
let dst_is_sdr_encoded = matches!(
to,
TransferFunction::Srgb | TransferFunction::Bt709 | TransferFunction::Gamma22
);
src_is_hdr && dst_is_sdr_encoded
}
#[derive(Clone, Debug)]
pub struct ConvertPlan {
pub(crate) from: PixelDescriptor,
pub(crate) to: PixelDescriptor,
pub(crate) steps: Vec<ConvertStep>,
pub(crate) pq_anchor_scale: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum FusedKind {
SrgbU8GamutRgb,
SrgbU8GamutRgba,
SrgbU16GamutRgb,
SrgbU8ToLinearF32Rgb,
LinearF32ToSrgbU8Rgb,
}
impl FusedKind {
#[inline]
#[allow(dead_code)] pub(crate) const fn variant_name(self) -> &'static str {
match self {
Self::SrgbU8GamutRgb => "FusedSrgbU8GamutRgb",
Self::SrgbU8GamutRgba => "FusedSrgbU8GamutRgba",
Self::SrgbU16GamutRgb => "FusedSrgbU16GamutRgb",
Self::SrgbU8ToLinearF32Rgb => "FusedSrgbU8ToLinearF32Rgb",
Self::LinearF32ToSrgbU8Rgb => "FusedLinearF32ToSrgbU8Rgb",
}
}
}
#[derive(Clone, Debug)]
pub(crate) enum ConvertStep {
Identity,
SwizzleBgraRgba,
RgbToBgra,
AddAlpha,
DropAlpha,
MatteComposite { r: u8, g: u8, b: u8 },
GrayToRgb,
GrayToRgba,
RgbToGray { coefficients: LumaCoefficients },
RgbaToGray { coefficients: LumaCoefficients },
GrayAlphaToRgba,
GrayAlphaToRgb,
GrayToGrayAlpha,
GrayAlphaToGray,
SrgbU8ToLinearF32,
LinearF32ToSrgbU8,
NaiveU8ToF32,
NaiveF32ToU8,
U16ToU8,
U8ToU16,
U16ToF32,
F32ToU16,
F16ToF32,
F32ToF16,
PqU16ToLinearF32,
LinearF32ToPqU16,
PqF32ToLinearF32,
LinearF32ToPqF32,
HlgU16ToLinearF32,
LinearF32ToHlgU16,
HlgF32ToLinearF32,
LinearF32ToHlgF32,
SrgbF32ToLinearF32,
LinearF32ToSrgbF32,
SrgbF32ToLinearF32Extended,
LinearF32ToSrgbF32Extended,
Bt709F32ToLinearF32,
LinearF32ToBt709F32,
Gamma22F32ToLinearF32,
LinearF32ToGamma22F32,
StraightToPremul,
PremulToStraight,
LinearRgbToOklab,
OklabToLinearRgb,
LinearRgbaToOklaba,
OklabaToLinearRgba,
GamutMatrixRgbF32([f32; 9]),
GamutMatrixRgbaF32([f32; 9]),
Fused { kind: FusedKind, matrix: [f32; 9] },
#[cfg(feature = "hdr-experimental")]
ToneMapBt2446A {
source_peak_nits: f32,
target_peak_nits: f32,
},
#[cfg(feature = "hdr-experimental")]
SoftCompressOklch {
primaries: ColorPrimaries,
knee: f32,
},
}
impl ConvertStep {
#[inline]
#[allow(dead_code)] pub(crate) const fn variant_name(&self) -> &'static str {
match self {
Self::Identity => "Identity",
Self::SwizzleBgraRgba => "SwizzleBgraRgba",
Self::RgbToBgra => "RgbToBgra",
Self::AddAlpha => "AddAlpha",
Self::DropAlpha => "DropAlpha",
Self::MatteComposite { .. } => "MatteComposite",
Self::GrayToRgb => "GrayToRgb",
Self::GrayToRgba => "GrayToRgba",
Self::RgbToGray { .. } => "RgbToGray",
Self::RgbaToGray { .. } => "RgbaToGray",
Self::GrayAlphaToRgba => "GrayAlphaToRgba",
Self::GrayAlphaToRgb => "GrayAlphaToRgb",
Self::GrayToGrayAlpha => "GrayToGrayAlpha",
Self::GrayAlphaToGray => "GrayAlphaToGray",
Self::SrgbU8ToLinearF32 => "SrgbU8ToLinearF32",
Self::LinearF32ToSrgbU8 => "LinearF32ToSrgbU8",
Self::NaiveU8ToF32 => "NaiveU8ToF32",
Self::NaiveF32ToU8 => "NaiveF32ToU8",
Self::U16ToU8 => "U16ToU8",
Self::U8ToU16 => "U8ToU16",
Self::U16ToF32 => "U16ToF32",
Self::F32ToU16 => "F32ToU16",
Self::F16ToF32 => "F16ToF32",
Self::F32ToF16 => "F32ToF16",
Self::PqU16ToLinearF32 => "PqU16ToLinearF32",
Self::LinearF32ToPqU16 => "LinearF32ToPqU16",
Self::PqF32ToLinearF32 => "PqF32ToLinearF32",
Self::LinearF32ToPqF32 => "LinearF32ToPqF32",
Self::HlgU16ToLinearF32 => "HlgU16ToLinearF32",
Self::LinearF32ToHlgU16 => "LinearF32ToHlgU16",
Self::HlgF32ToLinearF32 => "HlgF32ToLinearF32",
Self::LinearF32ToHlgF32 => "LinearF32ToHlgF32",
Self::SrgbF32ToLinearF32 => "SrgbF32ToLinearF32",
Self::LinearF32ToSrgbF32 => "LinearF32ToSrgbF32",
Self::SrgbF32ToLinearF32Extended => "SrgbF32ToLinearF32Extended",
Self::LinearF32ToSrgbF32Extended => "LinearF32ToSrgbF32Extended",
Self::Bt709F32ToLinearF32 => "Bt709F32ToLinearF32",
Self::LinearF32ToBt709F32 => "LinearF32ToBt709F32",
Self::Gamma22F32ToLinearF32 => "Gamma22F32ToLinearF32",
Self::LinearF32ToGamma22F32 => "LinearF32ToGamma22F32",
Self::StraightToPremul => "StraightToPremul",
Self::PremulToStraight => "PremulToStraight",
Self::LinearRgbToOklab => "LinearRgbToOklab",
Self::OklabToLinearRgb => "OklabToLinearRgb",
Self::LinearRgbaToOklaba => "LinearRgbaToOklaba",
Self::OklabaToLinearRgba => "OklabaToLinearRgba",
Self::GamutMatrixRgbF32(_) => "GamutMatrixRgbF32",
Self::GamutMatrixRgbaF32(_) => "GamutMatrixRgbaF32",
Self::Fused { kind, .. } => kind.variant_name(),
#[cfg(feature = "hdr-experimental")]
Self::ToneMapBt2446A { .. } => "ToneMapBt2446A",
#[cfg(feature = "hdr-experimental")]
Self::SoftCompressOklch { .. } => "SoftCompressOklch",
}
}
}
#[inline]
fn native_color_model(m: crate::ColorModel) -> bool {
matches!(
m,
crate::ColorModel::Gray | crate::ColorModel::Rgb | crate::ColorModel::Oklab
)
}
pub fn requires_cms(from: &PixelDescriptor, to: &PixelDescriptor) -> bool {
!native_color_model(from.color_model()) || !native_color_model(to.color_model())
}
impl ConvertPlan {
fn build(from: PixelDescriptor, to: PixelDescriptor, steps: Vec<ConvertStep>) -> Self {
Self {
from,
to,
steps,
pq_anchor_scale: 1.0,
}
}
#[must_use]
pub(crate) fn with_pq_anchor(mut self, anchor: zenpixels::hdr::DiffuseWhite) -> Self {
let diffuse_white_nits = f64::from(anchor.nits());
const PQ_PEAK_NITS: f64 = 10_000.0;
self.pq_anchor_scale = (diffuse_white_nits / PQ_PEAK_NITS) as f32;
self
}
#[track_caller]
pub fn new(from: PixelDescriptor, to: PixelDescriptor) -> Result<Self, At<ConvertError>> {
if requires_cms(&from, &to) {
return Err(whereat::at!(ConvertError::NeedsCms { from, to }));
}
if from == to {
return Ok(Self::build(from, to, vec![ConvertStep::Identity]));
}
if from.signal_range != to.signal_range {
return Err(whereat::at!(ConvertError::NoPath { from, to }));
}
if matches!(
(from.transfer(), to.transfer()),
(TransferFunction::Hlg, TransferFunction::Pq)
| (TransferFunction::Pq, TransferFunction::Hlg)
) {
return Err(whereat::at!(ConvertError::NoPath { from, to }));
}
#[cfg(feature = "hdr-experimental")]
if is_hdr_to_sdr(from.transfer(), to.transfer()) {
return Err(whereat::at!(ConvertError::HdrSourceRequiresPeak {
from,
to,
}));
}
let mut steps = Vec::with_capacity(3);
let need_depth_change = from.channel_type() != to.channel_type();
let need_layout_change = from.layout() != to.layout();
let need_alpha_change =
from.alpha() != to.alpha() && from.alpha().is_some() && to.alpha().is_some();
let need_depth_or_tf = need_depth_change || from.transfer() != to.transfer();
if need_layout_change {
let src_ch = from.layout().channels();
let dst_ch = to.layout().channels();
let involves_oklab =
matches!(from.layout(), ChannelLayout::Oklab | ChannelLayout::OklabA)
|| matches!(to.layout(), ChannelLayout::Oklab | ChannelLayout::OklabA);
if involves_oklab && from.primaries == ColorPrimaries::Unknown {
return Err(whereat::at!(ConvertError::NoPath { from, to }));
}
let depth_first = need_depth_or_tf
&& (dst_ch > src_ch || (involves_oklab && from.channel_type() != ChannelType::F32));
if depth_first {
steps.extend(
depth_steps(
from.channel_type(),
to.channel_type(),
from.transfer(),
to.transfer(),
)
.map_err(|e| whereat::at!(e))?,
);
steps.extend(layout_steps(from.layout(), to.layout()));
} else {
steps.extend(layout_steps(from.layout(), to.layout()));
if need_depth_or_tf {
steps.extend(
depth_steps(
from.channel_type(),
to.channel_type(),
from.transfer(),
to.transfer(),
)
.map_err(|e| whereat::at!(e))?,
);
}
}
} else if need_depth_or_tf {
steps.extend(
depth_steps(
from.channel_type(),
to.channel_type(),
from.transfer(),
to.transfer(),
)
.map_err(|e| whereat::at!(e))?,
);
}
if need_alpha_change {
match (from.alpha(), to.alpha()) {
(Some(AlphaMode::Straight), Some(AlphaMode::Premultiplied)) => {
steps.push(ConvertStep::StraightToPremul);
}
(Some(AlphaMode::Premultiplied), Some(AlphaMode::Straight)) => {
steps.push(ConvertStep::PremulToStraight);
}
_ => {}
}
}
let need_primaries = from.primaries != to.primaries
&& from.primaries != ColorPrimaries::Unknown
&& to.primaries != ColorPrimaries::Unknown;
if need_primaries
&& let Some(matrix) = crate::gamut::conversion_matrix(from.primaries, to.primaries)
{
let flat = [
matrix[0][0],
matrix[0][1],
matrix[0][2],
matrix[1][0],
matrix[1][1],
matrix[1][2],
matrix[2][0],
matrix[2][1],
matrix[2][2],
];
let mut goes_through_linear = false;
{
let mut desc = from;
for step in &steps {
desc = intermediate_desc(desc, step);
if desc.channel_type() == ChannelType::F32
&& desc.transfer() == TransferFunction::Linear
{
goes_through_linear = true;
}
}
}
if goes_through_linear {
let mut insert_pos = 0;
let mut desc = from;
for (i, step) in steps.iter().enumerate() {
desc = intermediate_desc(desc, step);
if desc.channel_type() == ChannelType::F32
&& desc.transfer() == TransferFunction::Linear
{
insert_pos = i + 1;
break;
}
}
let gamut_step = if desc.layout().has_alpha() {
ConvertStep::GamutMatrixRgbaF32(flat)
} else {
ConvertStep::GamutMatrixRgbF32(flat)
};
steps.insert(insert_pos, gamut_step);
} else {
let has_alpha = from.layout().has_alpha() || to.layout().has_alpha();
let mut desc = from;
for step in &steps {
desc = intermediate_desc(desc, step);
}
let gamut_step = if desc.layout().has_alpha() || has_alpha {
ConvertStep::GamutMatrixRgbaF32(flat)
} else {
ConvertStep::GamutMatrixRgbF32(flat)
};
let linearize = match desc.transfer() {
TransferFunction::Srgb => ConvertStep::SrgbF32ToLinearF32,
TransferFunction::Bt709 => ConvertStep::Bt709F32ToLinearF32,
TransferFunction::Pq => ConvertStep::PqF32ToLinearF32,
TransferFunction::Hlg => ConvertStep::HlgF32ToLinearF32,
TransferFunction::Gamma22 => ConvertStep::Gamma22F32ToLinearF32,
TransferFunction::Linear => ConvertStep::Identity,
_ => ConvertStep::SrgbF32ToLinearF32, };
let to_target_tf = match to.transfer() {
TransferFunction::Srgb => ConvertStep::LinearF32ToSrgbF32,
TransferFunction::Bt709 => ConvertStep::LinearF32ToBt709F32,
TransferFunction::Pq => ConvertStep::LinearF32ToPqF32,
TransferFunction::Hlg => ConvertStep::LinearF32ToHlgF32,
TransferFunction::Gamma22 => ConvertStep::LinearF32ToGamma22F32,
TransferFunction::Linear => ConvertStep::Identity,
_ => ConvertStep::LinearF32ToSrgbF32, };
let mut gamut_steps = Vec::new();
if desc.channel_type() == ChannelType::U16
&& desc.transfer() == TransferFunction::Srgb
&& to.channel_type() == ChannelType::U16
&& to.transfer() == TransferFunction::Srgb
&& !desc.layout().has_alpha()
&& !to.layout().has_alpha()
{
gamut_steps.push(ConvertStep::Fused {
kind: FusedKind::SrgbU16GamutRgb,
matrix: flat,
});
steps.extend(gamut_steps);
if steps.is_empty() {
steps.push(ConvertStep::Identity);
}
fuse_matlut_patterns(&mut steps);
return Ok(Self::build(from, to, steps));
}
if desc.channel_type() == ChannelType::U8
&& matches!(desc.transfer(), TransferFunction::Srgb)
&& to.channel_type() == ChannelType::F32
&& to.transfer() == TransferFunction::Linear
&& !desc.layout().has_alpha()
&& !to.layout().has_alpha()
{
gamut_steps.push(ConvertStep::Fused {
kind: FusedKind::SrgbU8ToLinearF32Rgb,
matrix: flat,
});
steps.extend(gamut_steps);
if steps.is_empty() {
steps.push(ConvertStep::Identity);
}
fuse_matlut_patterns(&mut steps);
return Ok(Self::build(from, to, steps));
}
if desc.channel_type() == ChannelType::F32
&& desc.transfer() == TransferFunction::Linear
&& to.channel_type() == ChannelType::U8
&& to.transfer() == TransferFunction::Srgb
&& !desc.layout().has_alpha()
&& !to.layout().has_alpha()
{
gamut_steps.push(ConvertStep::Fused {
kind: FusedKind::LinearF32ToSrgbU8Rgb,
matrix: flat,
});
steps.extend(gamut_steps);
if steps.is_empty() {
steps.push(ConvertStep::Identity);
}
fuse_matlut_patterns(&mut steps);
return Ok(Self::build(from, to, steps));
}
if desc.channel_type() != ChannelType::F32 {
if desc.channel_type() == ChannelType::U8
&& matches!(
desc.transfer(),
TransferFunction::Srgb
| TransferFunction::Bt709
| TransferFunction::Unknown
)
{
gamut_steps.push(ConvertStep::SrgbU8ToLinearF32);
gamut_steps.push(gamut_step);
gamut_steps.push(ConvertStep::LinearF32ToSrgbU8);
} else if desc.channel_type() == ChannelType::U16
&& desc.transfer() == TransferFunction::Pq
{
gamut_steps.push(ConvertStep::PqU16ToLinearF32);
gamut_steps.push(gamut_step);
gamut_steps.push(ConvertStep::LinearF32ToPqU16);
} else if desc.channel_type() == ChannelType::U16
&& desc.transfer() == TransferFunction::Hlg
{
gamut_steps.push(ConvertStep::HlgU16ToLinearF32);
gamut_steps.push(gamut_step);
gamut_steps.push(ConvertStep::LinearF32ToHlgU16);
} else {
gamut_steps.push(ConvertStep::NaiveU8ToF32);
if !matches!(linearize, ConvertStep::Identity) {
gamut_steps.push(linearize);
}
gamut_steps.push(gamut_step);
if !matches!(to_target_tf, ConvertStep::Identity) {
gamut_steps.push(to_target_tf);
}
gamut_steps.push(ConvertStep::NaiveF32ToU8);
}
} else {
if !matches!(linearize, ConvertStep::Identity) {
gamut_steps.push(linearize);
}
gamut_steps.push(gamut_step);
if !matches!(to_target_tf, ConvertStep::Identity) {
gamut_steps.push(to_target_tf);
}
}
steps.extend(gamut_steps);
}
}
if steps.is_empty() {
steps.push(ConvertStep::Identity);
}
fuse_matlut_patterns(&mut steps);
Ok(Self::build(from, to, steps))
}
#[cfg(feature = "hdr-experimental")]
#[track_caller]
pub fn new_with_hdr_peak(
from: PixelDescriptor,
to: PixelDescriptor,
source_peak_nits: f32,
) -> Result<Self, At<ConvertError>> {
Self::new_with_hdr_config(from, to, HdrConfig::for_source_peak(source_peak_nits))
}
#[cfg(feature = "hdr-experimental")]
#[track_caller]
pub fn new_with_hdr_config(
from: PixelDescriptor,
to: PixelDescriptor,
hdr: HdrConfig,
) -> Result<Self, At<ConvertError>> {
if requires_cms(&from, &to) {
return Err(whereat::at!(ConvertError::NeedsCms { from, to }));
}
let src_is_sdr_encoded = matches!(
from.transfer(),
TransferFunction::Srgb | TransferFunction::Bt709 | TransferFunction::Gamma22
);
if src_is_sdr_encoded {
return Self::new(from, to);
}
let peak_usable = |v: f32| v.is_finite() && v > 0.0;
if !peak_usable(hdr.source_peak_nits) || !peak_usable(hdr.target_peak_nits) {
return Err(whereat::at!(ConvertError::HdrSourceRequiresPeak {
from,
to
}));
}
if from.signal_range != to.signal_range {
return Err(whereat::at!(ConvertError::NoPath { from, to }));
}
let mut steps: Vec<ConvertStep> = Vec::with_capacity(8);
let after_decode = PixelDescriptor::new(
ChannelType::F32,
from.layout(),
from.alpha(),
TransferFunction::Linear,
);
steps.extend(
depth_steps(
from.channel_type(),
ChannelType::F32,
from.transfer(),
TransferFunction::Linear,
)
.map_err(|e| whereat::at!(e))?,
);
if from.primaries != ColorPrimaries::Bt2020
&& let Some(matrix) =
crate::gamut::conversion_matrix(from.primaries, ColorPrimaries::Bt2020)
{
let flat = [
matrix[0][0],
matrix[0][1],
matrix[0][2],
matrix[1][0],
matrix[1][1],
matrix[1][2],
matrix[2][0],
matrix[2][1],
matrix[2][2],
];
let step = if after_decode.layout().has_alpha() {
ConvertStep::GamutMatrixRgbaF32(flat)
} else {
ConvertStep::GamutMatrixRgbF32(flat)
};
steps.push(step);
}
steps.push(ConvertStep::ToneMapBt2446A {
source_peak_nits: hdr.source_peak_nits,
target_peak_nits: hdr.target_peak_nits,
});
if to.primaries != ColorPrimaries::Bt2020
&& to.primaries != ColorPrimaries::Unknown
&& let Some(matrix) =
crate::gamut::conversion_matrix(ColorPrimaries::Bt2020, to.primaries)
{
let flat = [
matrix[0][0],
matrix[0][1],
matrix[0][2],
matrix[1][0],
matrix[1][1],
matrix[1][2],
matrix[2][0],
matrix[2][1],
matrix[2][2],
];
let step = if after_decode.layout().has_alpha() {
ConvertStep::GamutMatrixRgbaF32(flat)
} else {
ConvertStep::GamutMatrixRgbF32(flat)
};
steps.push(step);
}
if to.primaries != ColorPrimaries::Bt2020 && to.primaries != ColorPrimaries::Unknown {
steps.push(ConvertStep::SoftCompressOklch {
primaries: to.primaries,
knee: hdr.gamut_knee,
});
}
if from.layout() != to.layout() {
steps.extend(layout_steps(from.layout(), to.layout()));
}
let need_depth_or_tf_encode =
to.channel_type() != ChannelType::F32 || to.transfer() != TransferFunction::Linear;
if need_depth_or_tf_encode {
steps.extend(
depth_steps(
ChannelType::F32,
to.channel_type(),
TransferFunction::Linear,
to.transfer(),
)
.map_err(|e| whereat::at!(e))?,
);
}
if from.alpha() != to.alpha() && from.alpha().is_some() && to.alpha().is_some() {
match (from.alpha(), to.alpha()) {
(Some(AlphaMode::Straight), Some(AlphaMode::Premultiplied)) => {
steps.push(ConvertStep::StraightToPremul);
}
(Some(AlphaMode::Premultiplied), Some(AlphaMode::Straight)) => {
steps.push(ConvertStep::PremulToStraight);
}
_ => {}
}
}
if steps.is_empty() {
steps.push(ConvertStep::Identity);
}
Ok(Self::build(from, to, steps))
}
#[track_caller]
pub fn new_explicit(
from: PixelDescriptor,
to: PixelDescriptor,
options: &ConvertOptions,
) -> Result<Self, At<ConvertError>> {
if requires_cms(&from, &to) {
return Err(whereat::at!(ConvertError::NeedsCms { from, to }));
}
let drops_alpha = from.alpha().is_some() && to.alpha().is_none();
if drops_alpha && options.alpha_policy == AlphaPolicy::Forbid {
return Err(whereat::at!(ConvertError::AlphaRemovalForbidden));
}
let reduces_depth = crate::negotiate::channel_bits(from.channel_type())
> crate::negotiate::channel_bits(to.channel_type());
if reduces_depth && options.depth_policy == DepthPolicy::Forbid {
return Err(whereat::at!(ConvertError::DepthReductionForbidden));
}
let src_is_rgb = matches!(
from.layout(),
ChannelLayout::Rgb | ChannelLayout::Rgba | ChannelLayout::Bgra
);
let dst_is_gray = matches!(to.layout(), ChannelLayout::Gray | ChannelLayout::GrayAlpha);
if src_is_rgb && dst_is_gray && options.luma.is_none() {
return Err(whereat::at!(ConvertError::RgbToGray));
}
let mut plan = Self::new(from, to).at()?;
if drops_alpha && let AlphaPolicy::CompositeOnto { r, g, b } = options.alpha_policy {
let src_is_premul = from.alpha() == Some(AlphaMode::Premultiplied);
let mut idx = 0;
while idx < plan.steps.len() {
if matches!(plan.steps[idx], ConvertStep::DropAlpha) {
plan.steps[idx] = ConvertStep::MatteComposite { r, g, b };
if src_is_premul {
plan.steps.insert(idx, ConvertStep::PremulToStraight);
idx += 1;
}
}
idx += 1;
}
}
if !options.clip_out_of_gamut {
for step in &mut plan.steps {
match step {
ConvertStep::SrgbF32ToLinearF32 => {
*step = ConvertStep::SrgbF32ToLinearF32Extended;
}
ConvertStep::LinearF32ToSrgbF32 => {
*step = ConvertStep::LinearF32ToSrgbF32Extended;
}
_ => {}
}
}
}
let user_luma = options.luma.unwrap_or(LumaCoefficients::Bt709);
for step in &mut plan.steps {
match step {
ConvertStep::RgbToGray { coefficients }
| ConvertStep::RgbaToGray { coefficients } => {
*coefficients = user_luma;
}
_ => {}
}
}
Ok(plan)
}
pub(crate) fn identity(from: PixelDescriptor, to: PixelDescriptor) -> Self {
Self::build(from, to, vec![ConvertStep::Identity])
}
pub fn compose(&self, other: &Self) -> Option<Self> {
if self.to != other.from {
return None;
}
let mut steps = self.steps.clone();
for step in &other.steps {
if matches!(step, ConvertStep::Identity) {
continue;
}
steps.push(step.clone());
}
let mut changed = true;
while changed {
changed = false;
let mut i = 0;
while i + 1 < steps.len() {
if are_inverse(&steps[i], &steps[i + 1]) {
steps.remove(i + 1);
steps.remove(i);
changed = true;
} else {
i += 1;
}
}
}
if steps.is_empty() {
steps.push(ConvertStep::Identity);
}
if steps.len() > 1 {
steps.retain(|s| !matches!(s, ConvertStep::Identity));
if steps.is_empty() {
steps.push(ConvertStep::Identity);
}
}
Some(Self::build(self.from, other.to, steps))
}
#[must_use]
pub fn is_identity(&self) -> bool {
self.steps.len() == 1 && matches!(self.steps[0], ConvertStep::Identity)
}
pub(crate) fn max_intermediate_bpp(&self) -> usize {
let mut desc = self.from;
let mut max_bpp = desc.bytes_per_pixel();
for step in &self.steps {
desc = intermediate_desc(desc, step);
max_bpp = max_bpp.max(desc.bytes_per_pixel());
}
max_bpp
}
pub(crate) fn steps(&self) -> &[ConvertStep] {
&self.steps
}
pub fn from(&self) -> PixelDescriptor {
self.from
}
pub fn to(&self) -> PixelDescriptor {
self.to
}
#[must_use]
pub fn estimate_in(
&self,
image: &crate::estimate::ImageCharacteristics,
compute: &crate::estimate::ComputeEnvironment,
) -> crate::estimate::ResourceEstimate {
crate::estimate::estimate_plan(self, image, compute)
}
#[must_use]
pub fn estimate(&self, width: u32, height: u32) -> crate::estimate::ResourceEstimate {
let image = crate::estimate::ImageCharacteristics::new(width, height, self.from());
let compute = crate::estimate::ComputeEnvironment::new();
self.estimate_in(&image, &compute)
}
}
pub(crate) fn intermediate_desc_for_estimate(
current: PixelDescriptor,
step: &ConvertStep,
) -> PixelDescriptor {
intermediate_desc(current, step)
}
fn layout_steps(from: ChannelLayout, to: ChannelLayout) -> Vec<ConvertStep> {
if from == to {
return Vec::new();
}
match (from, to) {
(ChannelLayout::Bgra, ChannelLayout::Rgba) | (ChannelLayout::Rgba, ChannelLayout::Bgra) => {
vec![ConvertStep::SwizzleBgraRgba]
}
(ChannelLayout::Rgb, ChannelLayout::Rgba) => vec![ConvertStep::AddAlpha],
(ChannelLayout::Rgb, ChannelLayout::Bgra) => {
vec![ConvertStep::RgbToBgra]
}
(ChannelLayout::Rgba, ChannelLayout::Rgb) => vec![ConvertStep::DropAlpha],
(ChannelLayout::Bgra, ChannelLayout::Rgb) => {
vec![ConvertStep::SwizzleBgraRgba, ConvertStep::DropAlpha]
}
(ChannelLayout::Gray, ChannelLayout::Rgb) => vec![ConvertStep::GrayToRgb],
(ChannelLayout::Gray, ChannelLayout::Rgba) => vec![ConvertStep::GrayToRgba],
(ChannelLayout::Gray, ChannelLayout::Bgra) => {
vec![ConvertStep::GrayToRgba, ConvertStep::SwizzleBgraRgba]
}
(ChannelLayout::Rgb, ChannelLayout::Gray) => vec![ConvertStep::RgbToGray {
coefficients: LumaCoefficients::Bt709,
}],
(ChannelLayout::Rgba, ChannelLayout::Gray) => vec![ConvertStep::RgbaToGray {
coefficients: LumaCoefficients::Bt709,
}],
(ChannelLayout::Bgra, ChannelLayout::Gray) => {
vec![
ConvertStep::SwizzleBgraRgba,
ConvertStep::RgbaToGray {
coefficients: LumaCoefficients::Bt709,
},
]
}
(ChannelLayout::GrayAlpha, ChannelLayout::Rgba) => vec![ConvertStep::GrayAlphaToRgba],
(ChannelLayout::GrayAlpha, ChannelLayout::Bgra) => {
vec![ConvertStep::GrayAlphaToRgba, ConvertStep::SwizzleBgraRgba]
}
(ChannelLayout::GrayAlpha, ChannelLayout::Rgb) => vec![ConvertStep::GrayAlphaToRgb],
(ChannelLayout::Gray, ChannelLayout::GrayAlpha) => vec![ConvertStep::GrayToGrayAlpha],
(ChannelLayout::GrayAlpha, ChannelLayout::Gray) => vec![ConvertStep::GrayAlphaToGray],
(ChannelLayout::Rgb, ChannelLayout::Oklab) => vec![ConvertStep::LinearRgbToOklab],
(ChannelLayout::Oklab, ChannelLayout::Rgb) => vec![ConvertStep::OklabToLinearRgb],
(ChannelLayout::Rgba, ChannelLayout::OklabA) => vec![ConvertStep::LinearRgbaToOklaba],
(ChannelLayout::OklabA, ChannelLayout::Rgba) => vec![ConvertStep::OklabaToLinearRgba],
(ChannelLayout::Rgb, ChannelLayout::OklabA) => {
vec![ConvertStep::AddAlpha, ConvertStep::LinearRgbaToOklaba]
}
(ChannelLayout::OklabA, ChannelLayout::Rgb) => {
vec![ConvertStep::OklabaToLinearRgba, ConvertStep::DropAlpha]
}
(ChannelLayout::Oklab, ChannelLayout::Rgba) => {
vec![ConvertStep::OklabToLinearRgb, ConvertStep::AddAlpha]
}
(ChannelLayout::Rgba, ChannelLayout::Oklab) => {
vec![ConvertStep::DropAlpha, ConvertStep::LinearRgbToOklab]
}
(ChannelLayout::Bgra, ChannelLayout::OklabA) => {
vec![
ConvertStep::SwizzleBgraRgba,
ConvertStep::LinearRgbaToOklaba,
]
}
(ChannelLayout::OklabA, ChannelLayout::Bgra) => {
vec![
ConvertStep::OklabaToLinearRgba,
ConvertStep::SwizzleBgraRgba,
]
}
(ChannelLayout::Bgra, ChannelLayout::Oklab) => {
vec![
ConvertStep::SwizzleBgraRgba,
ConvertStep::DropAlpha,
ConvertStep::LinearRgbToOklab,
]
}
(ChannelLayout::Oklab, ChannelLayout::Bgra) => {
vec![
ConvertStep::OklabToLinearRgb,
ConvertStep::AddAlpha,
ConvertStep::SwizzleBgraRgba,
]
}
(ChannelLayout::Gray, ChannelLayout::Oklab) => {
vec![ConvertStep::GrayToRgb, ConvertStep::LinearRgbToOklab]
}
(ChannelLayout::Oklab, ChannelLayout::Gray) => {
vec![
ConvertStep::OklabToLinearRgb,
ConvertStep::RgbToGray {
coefficients: LumaCoefficients::Bt709,
},
]
}
(ChannelLayout::Gray, ChannelLayout::OklabA) => {
vec![ConvertStep::GrayToRgba, ConvertStep::LinearRgbaToOklaba]
}
(ChannelLayout::OklabA, ChannelLayout::Gray) => {
vec![
ConvertStep::OklabaToLinearRgba,
ConvertStep::RgbaToGray {
coefficients: LumaCoefficients::Bt709,
},
]
}
(ChannelLayout::GrayAlpha, ChannelLayout::OklabA) => {
vec![
ConvertStep::GrayAlphaToRgba,
ConvertStep::LinearRgbaToOklaba,
]
}
(ChannelLayout::OklabA, ChannelLayout::GrayAlpha) => {
vec![
ConvertStep::OklabaToLinearRgba,
ConvertStep::RgbaToGray {
coefficients: LumaCoefficients::Bt709,
},
ConvertStep::GrayToGrayAlpha,
]
}
(ChannelLayout::GrayAlpha, ChannelLayout::Oklab) => {
vec![ConvertStep::GrayAlphaToRgb, ConvertStep::LinearRgbToOklab]
}
(ChannelLayout::Oklab, ChannelLayout::GrayAlpha) => {
vec![
ConvertStep::OklabToLinearRgb,
ConvertStep::RgbToGray {
coefficients: LumaCoefficients::Bt709,
},
ConvertStep::GrayToGrayAlpha,
]
}
(ChannelLayout::Oklab, ChannelLayout::OklabA) => vec![ConvertStep::AddAlpha],
(ChannelLayout::OklabA, ChannelLayout::Oklab) => vec![ConvertStep::DropAlpha],
_ => Vec::new(), }
}
fn f32_linearize_step(tf: TransferFunction) -> Option<ConvertStep> {
match tf {
TransferFunction::Linear => None,
TransferFunction::Srgb => Some(ConvertStep::SrgbF32ToLinearF32),
TransferFunction::Bt709 => Some(ConvertStep::Bt709F32ToLinearF32),
TransferFunction::Pq => Some(ConvertStep::PqF32ToLinearF32),
TransferFunction::Hlg => Some(ConvertStep::HlgF32ToLinearF32),
TransferFunction::Gamma22 => Some(ConvertStep::Gamma22F32ToLinearF32),
TransferFunction::Unknown => None,
_ => None,
}
}
fn f32_encode_step(tf: TransferFunction) -> Option<ConvertStep> {
match tf {
TransferFunction::Linear => None,
TransferFunction::Srgb => Some(ConvertStep::LinearF32ToSrgbF32),
TransferFunction::Bt709 => Some(ConvertStep::LinearF32ToBt709F32),
TransferFunction::Pq => Some(ConvertStep::LinearF32ToPqF32),
TransferFunction::Hlg => Some(ConvertStep::LinearF32ToHlgF32),
TransferFunction::Gamma22 => Some(ConvertStep::LinearF32ToGamma22F32),
TransferFunction::Unknown => None,
_ => None,
}
}
fn f32_tf_pair_steps(from: TransferFunction, to: TransferFunction) -> Vec<ConvertStep> {
if from == to || from == TransferFunction::Unknown || to == TransferFunction::Unknown {
return Vec::new();
}
let mut steps = Vec::with_capacity(2);
if let Some(s) = f32_linearize_step(from) {
steps.push(s);
}
if let Some(s) = f32_encode_step(to) {
steps.push(s);
}
steps
}
fn to_f32_step(ct: ChannelType) -> ConvertStep {
match ct {
ChannelType::U8 => ConvertStep::NaiveU8ToF32,
ChannelType::U16 => ConvertStep::U16ToF32,
ChannelType::F16 => ConvertStep::F16ToF32,
_ => unreachable!("to_f32_step called with F32 or unsupported channel type"),
}
}
fn f32_to_depth_step(ct: ChannelType) -> ConvertStep {
match ct {
ChannelType::U8 => ConvertStep::NaiveF32ToU8,
ChannelType::U16 => ConvertStep::F32ToU16,
ChannelType::F16 => ConvertStep::F32ToF16,
_ => unreachable!("f32_to_depth_step called with F32 or unsupported channel type"),
}
}
fn depth_steps(
from: ChannelType,
to: ChannelType,
from_tf: TransferFunction,
to_tf: TransferFunction,
) -> Result<Vec<ConvertStep>, ConvertError> {
if from == to && from_tf == to_tf {
return Ok(Vec::new());
}
if from == to && from == ChannelType::F32 {
return Ok(f32_tf_pair_steps(from_tf, to_tf));
}
if from == to && from != ChannelType::F32 {
if from_tf == TransferFunction::Unknown || to_tf == TransferFunction::Unknown {
return Ok(Vec::new());
}
let mut steps = Vec::with_capacity(4);
steps.push(to_f32_step(from));
steps.extend(f32_tf_pair_steps(from_tf, to_tf));
steps.push(f32_to_depth_step(to));
return Ok(steps);
}
match (from, to) {
(ChannelType::U8, ChannelType::F32) => {
if from_tf == TransferFunction::Srgb && to_tf == TransferFunction::Linear {
Ok(vec![ConvertStep::SrgbU8ToLinearF32])
} else if from_tf == to_tf {
Ok(vec![ConvertStep::NaiveU8ToF32])
} else {
let mut steps = Vec::with_capacity(3);
steps.push(ConvertStep::NaiveU8ToF32);
steps.extend(f32_tf_pair_steps(from_tf, to_tf));
Ok(steps)
}
}
(ChannelType::F32, ChannelType::U8) => {
if from_tf == TransferFunction::Linear && to_tf == TransferFunction::Srgb {
Ok(vec![ConvertStep::LinearF32ToSrgbU8])
} else if from_tf == to_tf {
Ok(vec![ConvertStep::NaiveF32ToU8])
} else {
let mut steps = f32_tf_pair_steps(from_tf, to_tf);
steps.push(ConvertStep::NaiveF32ToU8);
Ok(steps)
}
}
(ChannelType::U16, ChannelType::F32) => {
match (from_tf, to_tf) {
(TransferFunction::Pq, TransferFunction::Linear) => {
Ok(vec![ConvertStep::PqU16ToLinearF32])
}
(TransferFunction::Hlg, TransferFunction::Linear) => {
Ok(vec![ConvertStep::HlgU16ToLinearF32])
}
(a, b) if a == b => Ok(vec![ConvertStep::U16ToF32]),
_ => {
let mut steps = Vec::with_capacity(3);
steps.push(ConvertStep::U16ToF32);
steps.extend(f32_tf_pair_steps(from_tf, to_tf));
Ok(steps)
}
}
}
(ChannelType::F32, ChannelType::U16) => {
match (from_tf, to_tf) {
(TransferFunction::Linear, TransferFunction::Pq) => {
Ok(vec![ConvertStep::LinearF32ToPqU16])
}
(TransferFunction::Linear, TransferFunction::Hlg) => {
Ok(vec![ConvertStep::LinearF32ToHlgU16])
}
(a, b) if a == b => Ok(vec![ConvertStep::F32ToU16]),
_ => {
let mut steps = f32_tf_pair_steps(from_tf, to_tf);
steps.push(ConvertStep::F32ToU16);
Ok(steps)
}
}
}
(ChannelType::U16, ChannelType::U8) => {
if from_tf == TransferFunction::Pq && to_tf == TransferFunction::Srgb {
Ok(vec![
ConvertStep::PqU16ToLinearF32,
ConvertStep::LinearF32ToSrgbU8,
])
} else if from_tf == TransferFunction::Hlg && to_tf == TransferFunction::Srgb {
Ok(vec![
ConvertStep::HlgU16ToLinearF32,
ConvertStep::LinearF32ToSrgbU8,
])
} else if from_tf == to_tf {
Ok(vec![ConvertStep::U16ToU8])
} else {
let mut steps = Vec::with_capacity(4);
steps.push(ConvertStep::U16ToF32);
steps.extend(f32_tf_pair_steps(from_tf, to_tf));
steps.push(ConvertStep::NaiveF32ToU8);
Ok(steps)
}
}
(ChannelType::U8, ChannelType::U16) => {
if from_tf == to_tf {
Ok(vec![ConvertStep::U8ToU16])
} else {
let mut steps = Vec::with_capacity(4);
steps.push(ConvertStep::NaiveU8ToF32);
steps.extend(f32_tf_pair_steps(from_tf, to_tf));
steps.push(ConvertStep::F32ToU16);
Ok(steps)
}
}
(ChannelType::F16, ChannelType::F32) => {
let mut steps = Vec::with_capacity(3);
steps.push(ConvertStep::F16ToF32);
if from_tf != to_tf {
steps.extend(f32_tf_pair_steps(from_tf, to_tf));
}
Ok(steps)
}
(ChannelType::F32, ChannelType::F16) => {
let mut steps = Vec::with_capacity(3);
if from_tf != to_tf {
steps.extend(f32_tf_pair_steps(from_tf, to_tf));
}
steps.push(ConvertStep::F32ToF16);
Ok(steps)
}
(ChannelType::F16, ChannelType::U8) => {
let mut steps = Vec::with_capacity(4);
steps.push(ConvertStep::F16ToF32);
if from_tf == TransferFunction::Linear && to_tf == TransferFunction::Srgb {
steps.push(ConvertStep::LinearF32ToSrgbU8);
} else if from_tf == to_tf {
steps.push(ConvertStep::NaiveF32ToU8);
} else {
steps.extend(f32_tf_pair_steps(from_tf, to_tf));
steps.push(ConvertStep::NaiveF32ToU8);
}
Ok(steps)
}
(ChannelType::U8, ChannelType::F16) => {
let mut steps = Vec::with_capacity(4);
if from_tf == TransferFunction::Srgb && to_tf == TransferFunction::Linear {
steps.push(ConvertStep::SrgbU8ToLinearF32);
} else if from_tf == to_tf {
steps.push(ConvertStep::NaiveU8ToF32);
} else {
steps.push(ConvertStep::NaiveU8ToF32);
steps.extend(f32_tf_pair_steps(from_tf, to_tf));
}
steps.push(ConvertStep::F32ToF16);
Ok(steps)
}
(ChannelType::F16, ChannelType::U16) => {
let mut steps = Vec::with_capacity(4);
steps.push(ConvertStep::F16ToF32);
if from_tf == TransferFunction::Linear && to_tf == TransferFunction::Pq {
steps.push(ConvertStep::LinearF32ToPqU16);
} else if from_tf == TransferFunction::Linear && to_tf == TransferFunction::Hlg {
steps.push(ConvertStep::LinearF32ToHlgU16);
} else if from_tf == to_tf {
steps.push(ConvertStep::F32ToU16);
} else {
steps.extend(f32_tf_pair_steps(from_tf, to_tf));
steps.push(ConvertStep::F32ToU16);
}
Ok(steps)
}
(ChannelType::U16, ChannelType::F16) => {
let mut steps = Vec::with_capacity(4);
if from_tf == TransferFunction::Pq && to_tf == TransferFunction::Linear {
steps.push(ConvertStep::PqU16ToLinearF32);
} else if from_tf == TransferFunction::Hlg && to_tf == TransferFunction::Linear {
steps.push(ConvertStep::HlgU16ToLinearF32);
} else if from_tf == to_tf {
steps.push(ConvertStep::U16ToF32);
} else {
steps.push(ConvertStep::U16ToF32);
steps.extend(f32_tf_pair_steps(from_tf, to_tf));
}
steps.push(ConvertStep::F32ToF16);
Ok(steps)
}
_ => Err(ConvertError::NoPath {
from: PixelDescriptor::new(from, ChannelLayout::Rgb, None, from_tf),
to: PixelDescriptor::new(to, ChannelLayout::Rgb, None, to_tf),
}),
}
}
pub(crate) struct ConvertScratch {
buf: Vec<u32>,
hdr: convert_kernels::HdrKernelScratch,
}
impl ConvertScratch {
pub(crate) fn new() -> Self {
Self {
buf: Vec::new(),
hdr: convert_kernels::HdrKernelScratch::default(),
}
}
fn ensure_capacity(&mut self, plan: &ConvertPlan, width: u32) {
let half_bytes = (width as usize) * plan.max_intermediate_bpp();
let total_u32 = (half_bytes * 2).div_ceil(4);
if self.buf.len() < total_u32 {
self.buf.resize(total_u32, 0);
}
}
}
impl core::fmt::Debug for ConvertScratch {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ConvertScratch")
.field("capacity", &self.buf.capacity())
.finish()
}
}
pub fn convert_row(plan: &ConvertPlan, src: &[u8], dst: &mut [u8], width: u32) {
let mut scratch = ConvertScratch::new();
convert_row_buffered(plan, src, dst, width, &mut scratch);
}
pub(crate) fn convert_row_buffered(
plan: &ConvertPlan,
src: &[u8],
dst: &mut [u8],
width: u32,
scratch: &mut ConvertScratch,
) {
if plan.is_identity() {
let len = min(src.len(), dst.len());
dst[..len].copy_from_slice(&src[..len]);
return;
}
if plan.steps.len() == 1 {
apply_step_u8(
&plan.steps[0],
src,
dst,
width,
plan.from,
plan.to,
plan.pq_anchor_scale,
&mut scratch.hdr,
);
return;
}
scratch.ensure_capacity(plan, width);
let ConvertScratch { buf, hdr } = scratch;
let buf_bytes: &mut [u8] = bytemuck::cast_slice_mut(buf.as_mut_slice());
let half = buf_bytes.len() / 2;
let (buf_a, buf_b) = buf_bytes.split_at_mut(half);
let num_steps = plan.steps.len();
let mut current_desc = plan.from;
for (i, step) in plan.steps.iter().enumerate() {
let is_last = i == num_steps - 1;
let next_desc = if is_last {
plan.to
} else {
intermediate_desc(current_desc, step)
};
let next_len = (width as usize) * next_desc.bytes_per_pixel();
let curr_len = (width as usize) * current_desc.bytes_per_pixel();
if i % 2 == 0 {
let input = if i == 0 { src } else { &buf_b[..curr_len] };
if is_last {
apply_step_u8(
step,
input,
dst,
width,
current_desc,
next_desc,
plan.pq_anchor_scale,
&mut *hdr,
);
} else {
apply_step_u8(
step,
input,
&mut buf_a[..next_len],
width,
current_desc,
next_desc,
plan.pq_anchor_scale,
&mut *hdr,
);
}
} else {
let input = &buf_a[..curr_len];
if is_last {
apply_step_u8(
step,
input,
dst,
width,
current_desc,
next_desc,
plan.pq_anchor_scale,
&mut *hdr,
);
} else {
apply_step_u8(
step,
input,
&mut buf_b[..next_len],
width,
current_desc,
next_desc,
plan.pq_anchor_scale,
&mut *hdr,
);
}
}
current_desc = next_desc;
}
}
fn fuse_matlut_patterns(steps: &mut Vec<ConvertStep>) {
let mut i = 0;
while i + 2 < steps.len() {
let rewrite = match (&steps[i], &steps[i + 1], &steps[i + 2]) {
(
ConvertStep::SrgbU8ToLinearF32,
ConvertStep::GamutMatrixRgbF32(m),
ConvertStep::LinearF32ToSrgbU8,
) => Some(ConvertStep::Fused {
kind: FusedKind::SrgbU8GamutRgb,
matrix: *m,
}),
(
ConvertStep::SrgbU8ToLinearF32,
ConvertStep::GamutMatrixRgbaF32(m),
ConvertStep::LinearF32ToSrgbU8,
) => Some(ConvertStep::Fused {
kind: FusedKind::SrgbU8GamutRgba,
matrix: *m,
}),
_ => None,
};
if let Some(fused) = rewrite {
steps[i] = fused;
steps.drain(i + 1..i + 3);
continue;
}
i += 1;
}
}
fn are_inverse(a: &ConvertStep, b: &ConvertStep) -> bool {
matches!(
(a, b),
(ConvertStep::SwizzleBgraRgba, ConvertStep::SwizzleBgraRgba)
| (ConvertStep::AddAlpha, ConvertStep::DropAlpha)
| (ConvertStep::SrgbF32ToLinearF32, ConvertStep::LinearF32ToSrgbF32)
| (ConvertStep::LinearF32ToSrgbF32, ConvertStep::SrgbF32ToLinearF32)
| (ConvertStep::PqF32ToLinearF32, ConvertStep::LinearF32ToPqF32)
| (ConvertStep::LinearF32ToPqF32, ConvertStep::PqF32ToLinearF32)
| (ConvertStep::HlgF32ToLinearF32, ConvertStep::LinearF32ToHlgF32)
| (ConvertStep::LinearF32ToHlgF32, ConvertStep::HlgF32ToLinearF32)
| (ConvertStep::Bt709F32ToLinearF32, ConvertStep::LinearF32ToBt709F32)
| (ConvertStep::LinearF32ToBt709F32, ConvertStep::Bt709F32ToLinearF32)
| (ConvertStep::Gamma22F32ToLinearF32, ConvertStep::LinearF32ToGamma22F32)
| (ConvertStep::LinearF32ToGamma22F32, ConvertStep::Gamma22F32ToLinearF32)
| (ConvertStep::StraightToPremul, ConvertStep::PremulToStraight)
| (ConvertStep::PremulToStraight, ConvertStep::StraightToPremul)
| (ConvertStep::LinearRgbToOklab, ConvertStep::OklabToLinearRgb)
| (ConvertStep::OklabToLinearRgb, ConvertStep::LinearRgbToOklab)
| (ConvertStep::LinearRgbaToOklaba, ConvertStep::OklabaToLinearRgba)
| (ConvertStep::OklabaToLinearRgba, ConvertStep::LinearRgbaToOklaba)
| (ConvertStep::NaiveU8ToF32, ConvertStep::NaiveF32ToU8)
| (ConvertStep::NaiveF32ToU8, ConvertStep::NaiveU8ToF32)
| (ConvertStep::U8ToU16, ConvertStep::U16ToU8)
| (ConvertStep::U16ToU8, ConvertStep::U8ToU16)
| (ConvertStep::U16ToF32, ConvertStep::F32ToU16)
| (ConvertStep::F32ToU16, ConvertStep::U16ToF32)
| (ConvertStep::F16ToF32, ConvertStep::F32ToF16)
| (ConvertStep::F32ToF16, ConvertStep::F16ToF32)
| (ConvertStep::SrgbU8ToLinearF32, ConvertStep::LinearF32ToSrgbU8)
| (ConvertStep::LinearF32ToSrgbU8, ConvertStep::SrgbU8ToLinearF32)
| (ConvertStep::PqU16ToLinearF32, ConvertStep::LinearF32ToPqU16)
| (ConvertStep::LinearF32ToPqU16, ConvertStep::PqU16ToLinearF32)
| (ConvertStep::HlgU16ToLinearF32, ConvertStep::LinearF32ToHlgU16)
| (ConvertStep::LinearF32ToHlgU16, ConvertStep::HlgU16ToLinearF32)
| (ConvertStep::SrgbF32ToLinearF32Extended, ConvertStep::LinearF32ToSrgbF32Extended)
| (ConvertStep::LinearF32ToSrgbF32Extended, ConvertStep::SrgbF32ToLinearF32Extended)
)
}
fn intermediate_desc(current: PixelDescriptor, step: &ConvertStep) -> PixelDescriptor {
match step {
ConvertStep::Identity => current,
ConvertStep::SwizzleBgraRgba => {
let new_layout = match current.layout() {
ChannelLayout::Bgra => ChannelLayout::Rgba,
ChannelLayout::Rgba => ChannelLayout::Bgra,
other => other,
};
PixelDescriptor::new(
current.channel_type(),
new_layout,
current.alpha(),
current.transfer(),
)
}
ConvertStep::AddAlpha => PixelDescriptor::new(
current.channel_type(),
ChannelLayout::Rgba,
Some(AlphaMode::Straight),
current.transfer(),
),
ConvertStep::RgbToBgra => PixelDescriptor::new(
current.channel_type(),
ChannelLayout::Bgra,
Some(AlphaMode::Straight),
current.transfer(),
),
ConvertStep::DropAlpha | ConvertStep::MatteComposite { .. } => PixelDescriptor::new(
current.channel_type(),
ChannelLayout::Rgb,
None,
current.transfer(),
),
ConvertStep::GrayToRgb => PixelDescriptor::new(
current.channel_type(),
ChannelLayout::Rgb,
None,
current.transfer(),
),
ConvertStep::GrayToRgba => PixelDescriptor::new(
current.channel_type(),
ChannelLayout::Rgba,
Some(AlphaMode::Straight),
current.transfer(),
),
ConvertStep::RgbToGray { .. } | ConvertStep::RgbaToGray { .. } => PixelDescriptor::new(
current.channel_type(),
ChannelLayout::Gray,
None,
current.transfer(),
),
ConvertStep::GrayAlphaToRgba => PixelDescriptor::new(
current.channel_type(),
ChannelLayout::Rgba,
current.alpha(),
current.transfer(),
),
ConvertStep::GrayAlphaToRgb => PixelDescriptor::new(
current.channel_type(),
ChannelLayout::Rgb,
None,
current.transfer(),
),
ConvertStep::GrayToGrayAlpha => PixelDescriptor::new(
current.channel_type(),
ChannelLayout::GrayAlpha,
Some(AlphaMode::Straight),
current.transfer(),
),
ConvertStep::GrayAlphaToGray => PixelDescriptor::new(
current.channel_type(),
ChannelLayout::Gray,
None,
current.transfer(),
),
ConvertStep::SrgbU8ToLinearF32
| ConvertStep::NaiveU8ToF32
| ConvertStep::U16ToF32
| ConvertStep::PqU16ToLinearF32
| ConvertStep::HlgU16ToLinearF32
| ConvertStep::PqF32ToLinearF32
| ConvertStep::HlgF32ToLinearF32
| ConvertStep::SrgbF32ToLinearF32
| ConvertStep::SrgbF32ToLinearF32Extended
| ConvertStep::Bt709F32ToLinearF32
| ConvertStep::Gamma22F32ToLinearF32 => PixelDescriptor::new(
ChannelType::F32,
current.layout(),
current.alpha(),
TransferFunction::Linear,
),
ConvertStep::LinearF32ToSrgbU8 | ConvertStep::NaiveF32ToU8 | ConvertStep::U16ToU8 => {
PixelDescriptor::new(
ChannelType::U8,
current.layout(),
current.alpha(),
TransferFunction::Srgb,
)
}
ConvertStep::U8ToU16 => PixelDescriptor::new(
ChannelType::U16,
current.layout(),
current.alpha(),
current.transfer(),
),
ConvertStep::F32ToU16 | ConvertStep::LinearF32ToPqU16 | ConvertStep::LinearF32ToHlgU16 => {
let tf = match step {
ConvertStep::LinearF32ToPqU16 => TransferFunction::Pq,
ConvertStep::LinearF32ToHlgU16 => TransferFunction::Hlg,
_ => current.transfer(),
};
PixelDescriptor::new(ChannelType::U16, current.layout(), current.alpha(), tf)
}
ConvertStep::LinearF32ToPqF32 => PixelDescriptor::new(
ChannelType::F32,
current.layout(),
current.alpha(),
TransferFunction::Pq,
),
ConvertStep::LinearF32ToHlgF32 => PixelDescriptor::new(
ChannelType::F32,
current.layout(),
current.alpha(),
TransferFunction::Hlg,
),
ConvertStep::LinearF32ToSrgbF32 | ConvertStep::LinearF32ToSrgbF32Extended => {
PixelDescriptor::new(
ChannelType::F32,
current.layout(),
current.alpha(),
TransferFunction::Srgb,
)
}
ConvertStep::LinearF32ToBt709F32 => PixelDescriptor::new(
ChannelType::F32,
current.layout(),
current.alpha(),
TransferFunction::Bt709,
),
ConvertStep::LinearF32ToGamma22F32 => PixelDescriptor::new(
ChannelType::F32,
current.layout(),
current.alpha(),
TransferFunction::Gamma22,
),
ConvertStep::StraightToPremul => PixelDescriptor::new(
current.channel_type(),
current.layout(),
Some(AlphaMode::Premultiplied),
current.transfer(),
),
ConvertStep::PremulToStraight => PixelDescriptor::new(
current.channel_type(),
current.layout(),
Some(AlphaMode::Straight),
current.transfer(),
),
ConvertStep::LinearRgbToOklab => PixelDescriptor::new(
ChannelType::F32,
ChannelLayout::Oklab,
None,
TransferFunction::Unknown,
)
.with_primaries(current.primaries),
ConvertStep::OklabToLinearRgb => PixelDescriptor::new(
ChannelType::F32,
ChannelLayout::Rgb,
None,
TransferFunction::Linear,
)
.with_primaries(current.primaries),
ConvertStep::LinearRgbaToOklaba => PixelDescriptor::new(
ChannelType::F32,
ChannelLayout::OklabA,
Some(AlphaMode::Straight),
TransferFunction::Unknown,
)
.with_primaries(current.primaries),
ConvertStep::OklabaToLinearRgba => PixelDescriptor::new(
ChannelType::F32,
ChannelLayout::Rgba,
current.alpha(),
TransferFunction::Linear,
)
.with_primaries(current.primaries),
ConvertStep::GamutMatrixRgbF32(_) => PixelDescriptor::new(
ChannelType::F32,
current.layout(),
current.alpha(),
TransferFunction::Linear,
),
ConvertStep::GamutMatrixRgbaF32(_) => PixelDescriptor::new(
ChannelType::F32,
current.layout(),
current.alpha(),
TransferFunction::Linear,
),
ConvertStep::Fused { kind, .. } => {
let (ch_type, transfer) = match kind {
FusedKind::SrgbU8GamutRgb | FusedKind::SrgbU8GamutRgba => {
(ChannelType::U8, TransferFunction::Srgb)
}
FusedKind::SrgbU16GamutRgb => (ChannelType::U16, TransferFunction::Srgb),
FusedKind::SrgbU8ToLinearF32Rgb => (ChannelType::F32, TransferFunction::Linear),
FusedKind::LinearF32ToSrgbU8Rgb => (ChannelType::U8, TransferFunction::Srgb),
};
PixelDescriptor::new(ch_type, current.layout(), current.alpha(), transfer)
}
ConvertStep::F16ToF32 => PixelDescriptor::new(
ChannelType::F32,
current.layout(),
current.alpha(),
current.transfer(),
),
ConvertStep::F32ToF16 => PixelDescriptor::new(
ChannelType::F16,
current.layout(),
current.alpha(),
current.transfer(),
),
#[cfg(feature = "hdr-experimental")]
ConvertStep::ToneMapBt2446A { .. } => current,
#[cfg(feature = "hdr-experimental")]
ConvertStep::SoftCompressOklch { .. } => current,
}
}
#[path = "convert_kernels.rs"]
mod convert_kernels;
use convert_kernels::apply_step_u8;
pub(crate) use convert_kernels::{hlg_eotf, hlg_oetf, pq_eotf, pq_oetf};
#[cfg(all(test, feature = "hdr-experimental"))]
mod hdr_plan_tests {
use super::*;
use crate::gamut::{apply_matrix_f32, conversion_matrix};
use crate::hdr::{Bt2446A, SoftCompress};
use crate::oklab;
fn reference_pipeline(input: [f32; 3]) -> [f32; 3] {
let mut px = [input];
for c in px[0].iter_mut() {
if !c.is_finite() || *c < 0.0 {
*c = 0.0;
}
}
let m_src = conversion_matrix(ColorPrimaries::Bt709, ColorPrimaries::Bt2020).unwrap();
for p in px.iter_mut() {
apply_matrix_f32(p, &m_src);
}
Bt2446A::new(1000.0, 100.0).map_strip_simd(&mut px);
let m_dst = conversion_matrix(ColorPrimaries::Bt2020, ColorPrimaries::Bt709).unwrap();
for p in px.iter_mut() {
apply_matrix_f32(p, &m_dst);
}
let m1 = oklab::rgb_to_lms_matrix(ColorPrimaries::Bt709).unwrap();
let m1_inv = oklab::lms_to_rgb_matrix(ColorPrimaries::Bt709).unwrap();
let compressor = SoftCompress::from_matrices(&m1, &m1_inv, 0.96);
compressor.apply_strip(&mut px);
for c in px[0].iter_mut() {
if !c.is_finite() {
*c = 0.0;
} else {
*c = c.clamp(0.0, 1.0);
}
}
px[0]
}
#[test]
fn pixel_buffer_hdr_convert_matches_reference_pipeline() {
use crate::PixelBufferHdrConvertExt;
use zenpixels::PixelBuffer;
let src = PixelDescriptor::new_full(
ChannelType::F32,
ChannelLayout::Rgb,
None,
TransferFunction::Linear,
ColorPrimaries::Bt709,
);
let to = PixelDescriptor::new_full(
ChannelType::F32,
ChannelLayout::Rgb,
None,
TransferFunction::Linear,
ColorPrimaries::Bt709,
);
let hdr = HdrConfig::for_source_peak(1000.0);
let inputs = [
[0.0_f32, 0.0, 0.0],
[0.18, 0.18, 0.18],
[1.0, 1.0, 1.0],
[0.5, 0.3, 0.1],
];
for inp in inputs {
let expected = reference_pipeline(inp);
let bytes: Vec<u8> = bytemuck::cast_slice(&inp).to_vec();
let buf = PixelBuffer::from_vec(bytes, 1, 1, src).unwrap();
let out = buf.convert_to_with_hdr_config(to, hdr).expect("convert");
let out_bytes = out.copy_to_contiguous_bytes();
let got: &[f32] = bytemuck::cast_slice(&out_bytes);
for k in 0..3 {
let diff = (expected[k] - got[k]).abs();
assert!(
diff < 5e-4,
"ext channel {k} for input {inp:?}: expected {} vs got {} (diff {})",
expected[k],
got[k],
diff,
);
}
}
}
#[test]
fn hdr_plan_matches_reference_pipeline_for_bt709_linear_targets() {
let src = PixelDescriptor::new_full(
ChannelType::F32,
ChannelLayout::Rgb,
None,
TransferFunction::Linear,
ColorPrimaries::Bt709,
);
let to = PixelDescriptor::new_full(
ChannelType::F32,
ChannelLayout::Rgb,
None,
TransferFunction::Linear,
ColorPrimaries::Bt709,
);
let hdr = HdrConfig::for_source_peak(1000.0);
let plan = ConvertPlan::new_with_hdr_config(src, to, hdr).expect("plan");
let inputs = [
[0.0_f32, 0.0, 0.0],
[0.18, 0.18, 0.18],
[1.0, 1.0, 1.0],
[0.5, 0.3, 0.1],
[0.9, 0.1, 0.05],
];
for inp in inputs {
let expected = reference_pipeline(inp);
let bytes: Vec<u8> = bytemuck::cast_slice(&inp).to_vec();
let mut out = vec![0u8; 12];
convert_row(&plan, &bytes, &mut out, 1);
let got_f: &[f32] = bytemuck::cast_slice(&out);
for k in 0..3 {
let diff = (expected[k] - got_f[k]).abs();
assert!(
diff < 5e-4,
"channel {k} for input {inp:?}: expected {} vs got {} (diff {})",
expected[k],
got_f[k],
diff,
);
}
}
}
}