use core::ops::Add;
use crate::{
AlphaMode, ChannelLayout, ChannelType, ColorPrimaries, PixelDescriptor, TransferFunction,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Provenance {
pub origin_depth: ChannelType,
pub origin_primaries: ColorPrimaries,
}
impl Provenance {
#[inline]
pub fn from_source(desc: PixelDescriptor) -> Self {
Self {
origin_depth: desc.channel_type(),
origin_primaries: desc.primaries,
}
}
#[inline]
pub const fn with_origin_depth(origin_depth: ChannelType) -> Self {
Self {
origin_depth,
origin_primaries: ColorPrimaries::Bt709,
}
}
#[inline]
pub const fn with_origin(origin_depth: ChannelType, origin_primaries: ColorPrimaries) -> Self {
Self {
origin_depth,
origin_primaries,
}
}
#[inline]
pub fn with_origin_primaries(desc: PixelDescriptor, primaries: ColorPrimaries) -> Self {
Self {
origin_depth: desc.channel_type(),
origin_primaries: primaries,
}
}
#[inline]
pub fn invalidate_primaries(&mut self, current: ColorPrimaries) {
self.origin_primaries = current;
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum ConvertIntent {
#[default]
Fastest,
LinearLight,
Blend,
Perceptual,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ConversionCost {
pub effort: u16,
pub loss: u16,
}
impl ConversionCost {
pub const ZERO: Self = Self { effort: 0, loss: 0 };
pub const fn new(effort: u16, loss: u16) -> Self {
Self { effort, loss }
}
}
impl Add for ConversionCost {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self {
effort: self.effort.saturating_add(rhs.effort),
loss: self.loss.saturating_add(rhs.loss),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct FormatOption {
pub descriptor: PixelDescriptor,
pub consumer_cost: ConversionCost,
}
impl FormatOption {
pub const fn with_cost(descriptor: PixelDescriptor, consumer_cost: ConversionCost) -> Self {
Self {
descriptor,
consumer_cost,
}
}
}
impl From<PixelDescriptor> for FormatOption {
fn from(descriptor: PixelDescriptor) -> Self {
Self {
descriptor,
consumer_cost: ConversionCost::ZERO,
}
}
}
pub fn best_match(
source: PixelDescriptor,
supported: &[PixelDescriptor],
intent: ConvertIntent,
) -> Option<PixelDescriptor> {
negotiate(
source,
Provenance::from_source(source),
supported.iter().map(|&d| FormatOption::from(d)),
intent,
)
}
pub fn best_match_with(
source: PixelDescriptor,
options: &[FormatOption],
intent: ConvertIntent,
) -> Option<PixelDescriptor> {
negotiate(
source,
Provenance::from_source(source),
options.iter().copied(),
intent,
)
}
pub fn negotiate(
source: PixelDescriptor,
provenance: Provenance,
options: impl Iterator<Item = FormatOption>,
intent: ConvertIntent,
) -> Option<PixelDescriptor> {
best_of(
source,
provenance,
options.map(|o| (o.descriptor, o.consumer_cost)),
intent,
)
}
pub fn ideal_format(source: PixelDescriptor, intent: ConvertIntent) -> PixelDescriptor {
match intent {
ConvertIntent::Fastest => source,
ConvertIntent::LinearLight => {
if source.channel_type() == ChannelType::F32
&& source.transfer() == TransferFunction::Linear
{
return source;
}
PixelDescriptor::new(
ChannelType::F32,
source.layout(),
source.alpha(),
TransferFunction::Linear,
)
}
ConvertIntent::Blend => {
let alpha = if source.layout().has_alpha() {
Some(AlphaMode::Premultiplied)
} else {
source.alpha()
};
if source.channel_type() == ChannelType::F32
&& source.transfer() == TransferFunction::Linear
&& source.alpha() == alpha
{
return source;
}
PixelDescriptor::new(
ChannelType::F32,
source.layout(),
alpha,
TransferFunction::Linear,
)
}
ConvertIntent::Perceptual => {
let tier = precision_tier(source);
match tier {
PrecisionTier::Sdr8 => {
if source.transfer() == TransferFunction::Srgb
|| source.transfer() == TransferFunction::Unknown
{
return source;
}
PixelDescriptor::new(
ChannelType::U8,
source.layout(),
source.alpha(),
TransferFunction::Srgb,
)
}
_ => PixelDescriptor::new(
ChannelType::F32,
source.layout(),
source.alpha(),
TransferFunction::Srgb,
),
}
}
}
}
#[must_use]
pub fn conversion_cost(from: PixelDescriptor, to: PixelDescriptor) -> ConversionCost {
conversion_cost_with_provenance(from, to, Provenance::from_source(from))
}
#[must_use]
pub fn conversion_cost_with_provenance(
from: PixelDescriptor,
to: PixelDescriptor,
provenance: Provenance,
) -> ConversionCost {
transfer_cost(from.transfer(), to.transfer())
+ depth_cost(
from.channel_type(),
to.channel_type(),
provenance.origin_depth,
)
+ layout_cost(from.layout(), to.layout())
+ alpha_cost(from.alpha(), to.alpha(), from.layout(), to.layout())
+ primaries_cost(from.primaries, to.primaries, provenance.origin_primaries)
}
pub(crate) fn score_target(
source: PixelDescriptor,
provenance: Provenance,
target: PixelDescriptor,
consumer_cost: ConversionCost,
intent: ConvertIntent,
) -> u32 {
let our_cost = conversion_cost_with_provenance(source, target, provenance);
let total_effort = our_cost.effort as u32 + consumer_cost.effort as u32;
let total_loss =
our_cost.loss as u32 + consumer_cost.loss as u32 + suitability_loss(target, intent) as u32;
weighted_score(total_effort, total_loss, intent)
}
fn best_of(
source: PixelDescriptor,
provenance: Provenance,
options: impl Iterator<Item = (PixelDescriptor, ConversionCost)>,
intent: ConvertIntent,
) -> Option<PixelDescriptor> {
let mut best: Option<(PixelDescriptor, u32)> = None;
for (target, consumer_cost) in options {
let score = score_target(source, provenance, target, consumer_cost, intent);
match best {
Some((_, best_score)) if score < best_score => best = Some((target, score)),
None => best = Some((target, score)),
_ => {}
}
}
best.map(|(desc, _)| desc)
}
pub(crate) fn weighted_score(effort: u32, loss: u32, intent: ConvertIntent) -> u32 {
match intent {
ConvertIntent::Fastest => effort * 4 + loss,
ConvertIntent::LinearLight | ConvertIntent::Blend => effort + loss * 4,
ConvertIntent::Perceptual => effort + loss * 3,
}
}
pub(crate) fn suitability_loss(target: PixelDescriptor, intent: ConvertIntent) -> u16 {
match intent {
ConvertIntent::Fastest => 0,
ConvertIntent::LinearLight => linear_light_suitability(target),
ConvertIntent::Blend => {
let mut s = linear_light_suitability(target);
if target.layout().has_alpha() && target.alpha() == Some(AlphaMode::Straight) {
s += 200;
}
s
}
ConvertIntent::Perceptual => perceptual_suitability(target),
}
}
#[allow(unreachable_patterns)] fn linear_light_suitability(target: PixelDescriptor) -> u16 {
if target.transfer() == TransferFunction::Linear {
match target.channel_type() {
ChannelType::F32 => 0,
ChannelType::F16 => 5, ChannelType::U16 => 5, ChannelType::U8 => 40, _ => 50,
}
} else {
120
}
}
fn perceptual_suitability(target: PixelDescriptor) -> u16 {
if target.channel_type() == ChannelType::F32 && target.transfer() == TransferFunction::Linear {
return 15;
}
if matches!(
target.transfer(),
TransferFunction::Pq | TransferFunction::Hlg
) {
return 10;
}
0
}
fn transfer_cost(from: TransferFunction, to: TransferFunction) -> ConversionCost {
if from == to {
return ConversionCost::ZERO;
}
match (from, to) {
(TransferFunction::Unknown, _) | (_, TransferFunction::Unknown) => {
ConversionCost::new(1, 0)
}
(TransferFunction::Srgb, TransferFunction::Linear)
| (TransferFunction::Linear, TransferFunction::Srgb) => ConversionCost::new(5, 0),
(TransferFunction::Bt709, TransferFunction::Srgb)
| (TransferFunction::Srgb, TransferFunction::Bt709)
| (TransferFunction::Bt709, TransferFunction::Linear)
| (TransferFunction::Linear, TransferFunction::Bt709) => ConversionCost::new(8, 0),
_ => ConversionCost::new(80, 300),
}
}
fn depth_cost(from: ChannelType, to: ChannelType, origin_depth: ChannelType) -> ConversionCost {
if from == to {
return ConversionCost::ZERO;
}
let effort = depth_effort(from, to);
let loss = depth_loss(to, origin_depth);
ConversionCost::new(effort, loss)
}
#[allow(unreachable_patterns)] fn depth_effort(from: ChannelType, to: ChannelType) -> u16 {
match (from, to) {
(ChannelType::U8, ChannelType::U16) | (ChannelType::U16, ChannelType::U8) => 10,
(ChannelType::U16, ChannelType::F32) | (ChannelType::F32, ChannelType::U16) => 25,
(ChannelType::U8, ChannelType::F32) | (ChannelType::F32, ChannelType::U8) => 40,
(ChannelType::F16, ChannelType::F32) | (ChannelType::F32, ChannelType::F16) => 15,
(ChannelType::F16, ChannelType::U8) | (ChannelType::U8, ChannelType::F16) => 30,
(ChannelType::F16, ChannelType::U16) | (ChannelType::U16, ChannelType::F16) => 25,
_ => 100,
}
}
#[allow(unreachable_patterns)] fn depth_loss(target_depth: ChannelType, origin_depth: ChannelType) -> u16 {
let target_bits = channel_bits(target_depth);
let origin_bits = channel_bits(origin_depth);
if target_bits >= origin_bits {
return 0;
}
match (origin_depth, target_depth) {
(ChannelType::U16, ChannelType::U8) => 10, (ChannelType::F32, ChannelType::U8) => 10, (ChannelType::F32, ChannelType::U16) => 5, (ChannelType::F32, ChannelType::F16) => 20, (ChannelType::F16, ChannelType::U8) => 8, (ChannelType::U16, ChannelType::F16) => 30, _ => 50,
}
}
#[allow(unreachable_patterns)] pub(crate) fn channel_bits(ct: ChannelType) -> u16 {
match ct {
ChannelType::U8 => 8,
ChannelType::F16 => 11, ChannelType::U16 => 16,
ChannelType::F32 => 32,
_ => 0,
}
}
fn primaries_cost(
from: ColorPrimaries,
to: ColorPrimaries,
origin: ColorPrimaries,
) -> ConversionCost {
if from == to {
return ConversionCost::ZERO;
}
if matches!(from, ColorPrimaries::Unknown) || matches!(to, ColorPrimaries::Unknown) {
return ConversionCost::new(1, 0);
}
if to.contains(from) {
return ConversionCost::new(10, 0);
}
if to.contains(origin) {
return ConversionCost::new(10, 5);
}
match (from, to) {
(ColorPrimaries::DisplayP3, ColorPrimaries::Bt709) => ConversionCost::new(15, 80),
(ColorPrimaries::Bt2020, ColorPrimaries::DisplayP3) => ConversionCost::new(15, 100),
(ColorPrimaries::Bt2020, ColorPrimaries::Bt709) => ConversionCost::new(15, 200),
_ => ConversionCost::new(15, 150),
}
}
fn layout_cost(from: ChannelLayout, to: ChannelLayout) -> ConversionCost {
if from == to {
return ConversionCost::ZERO;
}
match (from, to) {
(ChannelLayout::Bgra, ChannelLayout::Rgba) | (ChannelLayout::Rgba, ChannelLayout::Bgra) => {
ConversionCost::new(5, 0)
}
(ChannelLayout::Rgb, ChannelLayout::Rgba) | (ChannelLayout::Rgb, ChannelLayout::Bgra) => {
ConversionCost::new(10, 0)
}
(ChannelLayout::Rgba, ChannelLayout::Rgb) | (ChannelLayout::Bgra, ChannelLayout::Rgb) => {
ConversionCost::new(15, 50)
}
(ChannelLayout::Gray, ChannelLayout::Rgb) => ConversionCost::new(8, 0),
(ChannelLayout::Gray, ChannelLayout::Rgba) | (ChannelLayout::Gray, ChannelLayout::Bgra) => {
ConversionCost::new(10, 0)
}
(ChannelLayout::Rgb, ChannelLayout::Gray)
| (ChannelLayout::Rgba, ChannelLayout::Gray)
| (ChannelLayout::Bgra, ChannelLayout::Gray) => ConversionCost::new(30, 500),
(ChannelLayout::GrayAlpha, ChannelLayout::Rgba)
| (ChannelLayout::GrayAlpha, ChannelLayout::Bgra) => ConversionCost::new(15, 0),
(ChannelLayout::Rgba, ChannelLayout::GrayAlpha)
| (ChannelLayout::Bgra, ChannelLayout::GrayAlpha) => ConversionCost::new(30, 500),
(ChannelLayout::Gray, ChannelLayout::GrayAlpha) => ConversionCost::new(8, 0),
(ChannelLayout::GrayAlpha, ChannelLayout::Gray) => ConversionCost::new(10, 50),
(ChannelLayout::GrayAlpha, ChannelLayout::Rgb) => ConversionCost::new(12, 50),
(ChannelLayout::Rgb, ChannelLayout::Oklab) | (ChannelLayout::Oklab, ChannelLayout::Rgb) => {
ConversionCost::new(80, 0)
}
(ChannelLayout::Rgba, ChannelLayout::OklabA)
| (ChannelLayout::OklabA, ChannelLayout::Rgba) => ConversionCost::new(80, 0),
(ChannelLayout::Oklab, ChannelLayout::OklabA) => ConversionCost::new(10, 0),
(ChannelLayout::OklabA, ChannelLayout::Oklab) => ConversionCost::new(15, 50),
(ChannelLayout::Rgb, ChannelLayout::OklabA) => ConversionCost::new(90, 0),
(ChannelLayout::OklabA, ChannelLayout::Rgb) => ConversionCost::new(90, 50),
(ChannelLayout::Oklab, ChannelLayout::Rgba) => ConversionCost::new(90, 0),
(ChannelLayout::Rgba, ChannelLayout::Oklab) => ConversionCost::new(90, 50),
_ => ConversionCost::new(100, 500),
}
}
fn alpha_cost(
from_alpha: Option<AlphaMode>,
to_alpha: Option<AlphaMode>,
from_layout: ChannelLayout,
to_layout: ChannelLayout,
) -> ConversionCost {
if !to_layout.has_alpha() || !from_layout.has_alpha() || from_alpha == to_alpha {
return ConversionCost::ZERO;
}
match (from_alpha, to_alpha) {
(Some(AlphaMode::Straight), Some(AlphaMode::Premultiplied)) => ConversionCost::new(20, 5),
(Some(AlphaMode::Premultiplied), Some(AlphaMode::Straight)) => ConversionCost::new(25, 10),
_ => ConversionCost::ZERO,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum PrecisionTier {
Sdr8 = 0,
Sdr16 = 1,
LinearF32 = 2,
Hdr = 3,
}
#[allow(unreachable_patterns)] fn precision_tier(desc: PixelDescriptor) -> PrecisionTier {
if matches!(
desc.transfer(),
TransferFunction::Pq | TransferFunction::Hlg
) {
return PrecisionTier::Hdr;
}
match desc.channel_type() {
ChannelType::U8 => PrecisionTier::Sdr8,
ChannelType::U16 | ChannelType::F16 => PrecisionTier::Sdr16,
ChannelType::F32 => PrecisionTier::LinearF32,
_ => PrecisionTier::Sdr8,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_match_wins() {
let src = PixelDescriptor::RGB8_SRGB;
let supported = &[PixelDescriptor::RGBA8_SRGB, PixelDescriptor::RGB8_SRGB];
assert_eq!(
best_match(src, supported, ConvertIntent::Fastest),
Some(PixelDescriptor::RGB8_SRGB)
);
}
#[test]
fn empty_list_returns_none() {
let src = PixelDescriptor::RGB8_SRGB;
assert_eq!(best_match(src, &[], ConvertIntent::Fastest), None);
}
#[test]
fn prefers_same_depth_over_cross_depth() {
let src = PixelDescriptor::RGB8_SRGB;
let supported = &[PixelDescriptor::RGBF32_LINEAR, PixelDescriptor::RGBA8_SRGB];
assert_eq!(
best_match(src, supported, ConvertIntent::Fastest),
Some(PixelDescriptor::RGBA8_SRGB)
);
}
#[test]
fn bgra_rgba_swizzle_is_cheap() {
let src = PixelDescriptor::BGRA8_SRGB;
let supported = &[PixelDescriptor::RGB8_SRGB, PixelDescriptor::RGBA8_SRGB];
assert_eq!(
best_match(src, supported, ConvertIntent::Fastest),
Some(PixelDescriptor::RGBA8_SRGB)
);
}
#[test]
fn gray_to_rgb_preferred_over_rgba() {
let src = PixelDescriptor::GRAY8_SRGB;
let supported = &[PixelDescriptor::RGBA8_SRGB, PixelDescriptor::RGB8_SRGB];
assert_eq!(
best_match(src, supported, ConvertIntent::Fastest),
Some(PixelDescriptor::RGB8_SRGB)
);
}
#[test]
fn transfer_only_diff_is_cheap() {
let src = PixelDescriptor::new(
ChannelType::U8,
ChannelLayout::Rgb,
None,
TransferFunction::Unknown,
);
let target = PixelDescriptor::RGB8_SRGB;
let supported = &[target, PixelDescriptor::RGBF32_LINEAR];
assert_eq!(
best_match(src, supported, ConvertIntent::Fastest),
Some(target)
);
}
#[test]
fn conversion_cost_identity_is_zero() {
let cost = conversion_cost(PixelDescriptor::RGB8_SRGB, PixelDescriptor::RGB8_SRGB);
assert_eq!(cost, ConversionCost::ZERO);
}
#[test]
fn widening_has_zero_loss() {
let cost = conversion_cost(PixelDescriptor::RGB8_SRGB, PixelDescriptor::RGBF32_LINEAR);
assert_eq!(cost.loss, 0);
assert!(cost.effort > 0);
}
#[test]
fn narrowing_has_nonzero_loss() {
let cost = conversion_cost(PixelDescriptor::RGBF32_LINEAR, PixelDescriptor::RGB8_SRGB);
assert!(cost.loss > 0, "f32→u8 should report data loss");
assert!(cost.effort > 0);
}
#[test]
fn consumer_override_shifts_preference() {
let src = PixelDescriptor::RGBF32_LINEAR;
let options = &[
FormatOption::from(PixelDescriptor::RGB8_SRGB),
FormatOption::with_cost(PixelDescriptor::RGBF32_LINEAR, ConversionCost::new(5, 0)),
];
assert_eq!(
best_match_with(src, options, ConvertIntent::Fastest),
Some(PixelDescriptor::RGBF32_LINEAR)
);
}
}