use alloc::vec::Vec;
use crate::negotiate::channel_bits;
use crate::{AlphaMode, ChannelType, ConvertIntent, PixelDescriptor, TransferFunction};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum OpCategory {
Passthrough,
ResizeGentle,
ResizeSharp,
Blur,
Sharpen,
OklabSharpen,
Composite,
OklabAdjust,
ColorMatrix,
Tonemap,
IccTransform,
Quantize,
Arithmetic,
}
#[derive(Clone, Copy, Debug)]
pub struct OpRequirement {
pub category: OpCategory,
pub transfer: Option<TransferFunction>,
pub min_depth: Option<ChannelType>,
pub requires_float: bool,
pub alpha: Option<AlphaMode>,
}
impl OpCategory {
pub fn requirement(self) -> OpRequirement {
match self {
Self::Passthrough => OpRequirement {
category: self,
transfer: None,
min_depth: None,
requires_float: false,
alpha: None,
},
Self::ResizeGentle => OpRequirement {
category: self,
transfer: Some(TransferFunction::Linear),
min_depth: None,
requires_float: false,
alpha: None,
},
Self::ResizeSharp => OpRequirement {
category: self,
transfer: Some(TransferFunction::Linear),
min_depth: Some(ChannelType::F32),
requires_float: true,
alpha: None,
},
Self::Blur | Self::Sharpen => OpRequirement {
category: self,
transfer: None, min_depth: None,
requires_float: false,
alpha: None,
},
Self::OklabSharpen | Self::OklabAdjust => OpRequirement {
category: self,
transfer: Some(TransferFunction::Linear),
min_depth: Some(ChannelType::F32),
requires_float: true,
alpha: None,
},
Self::Composite => OpRequirement {
category: self,
transfer: Some(TransferFunction::Linear),
min_depth: Some(ChannelType::F32),
requires_float: true,
alpha: Some(AlphaMode::Premultiplied),
},
Self::ColorMatrix => OpRequirement {
category: self,
transfer: None, min_depth: None,
requires_float: false,
alpha: None,
},
Self::Tonemap => OpRequirement {
category: self,
transfer: Some(TransferFunction::Linear),
min_depth: Some(ChannelType::F32),
requires_float: true,
alpha: None,
},
Self::IccTransform => OpRequirement {
category: self,
transfer: Some(TransferFunction::Linear),
min_depth: Some(ChannelType::F32),
requires_float: true,
alpha: None,
},
Self::Quantize => OpRequirement {
category: self,
transfer: Some(TransferFunction::Srgb),
min_depth: Some(ChannelType::U8),
requires_float: false,
alpha: None,
},
Self::Arithmetic => OpRequirement {
category: self,
transfer: None,
min_depth: None,
requires_float: false,
alpha: None,
},
}
}
pub fn to_intent(self) -> ConvertIntent {
match self {
Self::Passthrough | Self::ColorMatrix | Self::Arithmetic | Self::Quantize => {
ConvertIntent::Fastest
}
Self::ResizeGentle
| Self::ResizeSharp
| Self::Blur
| Self::Tonemap
| Self::IccTransform => ConvertIntent::LinearLight,
Self::Composite => ConvertIntent::Blend,
Self::Sharpen | Self::OklabSharpen | Self::OklabAdjust => ConvertIntent::Perceptual,
}
}
pub fn candidate_working_formats(self, source: PixelDescriptor) -> Vec<PixelDescriptor> {
use crate::ChannelLayout;
let req = self.requirement();
let mut candidates = Vec::with_capacity(4);
if self == Self::Passthrough {
candidates.push(source);
return candidates;
}
if format_satisfies(source, &req) {
candidates.push(source);
}
let ideal_transfer = req.transfer.unwrap_or(source.transfer());
let ideal_depth = if req.requires_float {
ChannelType::F32
} else {
req.min_depth.unwrap_or(source.channel_type())
};
let ideal_alpha = match req.alpha {
Some(a) => Some(a),
None => source.alpha(),
};
let rgb_ideal = PixelDescriptor::new(ideal_depth, ChannelLayout::Rgb, None, ideal_transfer);
if !candidates.contains(&rgb_ideal) {
candidates.push(rgb_ideal);
}
if source.layout().has_alpha() || req.alpha.is_some() {
let rgba_ideal = PixelDescriptor::new(
ideal_depth,
ChannelLayout::Rgba,
ideal_alpha,
ideal_transfer,
);
if !candidates.contains(&rgba_ideal) {
candidates.push(rgba_ideal);
}
}
if matches!(self, Self::OklabSharpen | Self::OklabAdjust)
|| matches!(
source.layout(),
ChannelLayout::Oklab | ChannelLayout::OklabA
)
{
let oklab_ideal = PixelDescriptor::new(
ChannelType::F32,
ChannelLayout::Oklab,
None,
TransferFunction::Unknown,
);
if !candidates.contains(&oklab_ideal) {
candidates.push(oklab_ideal);
}
if source.layout().has_alpha() || req.alpha.is_some() {
let oklaba_ideal = PixelDescriptor::new(
ChannelType::F32,
ChannelLayout::OklabA,
ideal_alpha,
TransferFunction::Unknown,
);
if !candidates.contains(&oklaba_ideal) {
candidates.push(oklaba_ideal);
}
}
}
if source.is_grayscale() {
let gray_ideal =
PixelDescriptor::new(ideal_depth, ChannelLayout::Gray, None, ideal_transfer);
if !candidates.contains(&gray_ideal) {
candidates.push(gray_ideal);
}
if source.layout() == ChannelLayout::GrayAlpha {
let ga_ideal = PixelDescriptor::new(
ideal_depth,
ChannelLayout::GrayAlpha,
ideal_alpha,
ideal_transfer,
);
if !candidates.contains(&ga_ideal) {
candidates.push(ga_ideal);
}
}
}
candidates
}
}
fn format_satisfies(desc: PixelDescriptor, req: &OpRequirement) -> bool {
if let Some(tf) = req.transfer
&& desc.transfer() != tf
{
return false;
}
if req.requires_float && desc.channel_type() != ChannelType::F32 {
return false;
}
if let Some(min) = req.min_depth
&& channel_bits(desc.channel_type()) < channel_bits(min)
{
return false;
}
if let Some(alpha) = req.alpha
&& desc.layout().has_alpha()
&& desc.alpha() != Some(alpha)
{
return false;
}
true
}
#[cfg(test)]
mod tests {
use alloc::vec;
use super::*;
#[test]
fn passthrough_accepts_anything() {
let req = OpCategory::Passthrough.requirement();
assert!(req.transfer.is_none());
assert!(req.min_depth.is_none());
assert!(!req.requires_float);
assert!(req.alpha.is_none());
}
#[test]
fn resize_sharp_requires_float_linear() {
let req = OpCategory::ResizeSharp.requirement();
assert_eq!(req.transfer, Some(TransferFunction::Linear));
assert!(req.requires_float);
}
#[test]
fn composite_requires_premultiplied() {
let req = OpCategory::Composite.requirement();
assert_eq!(req.alpha, Some(AlphaMode::Premultiplied));
}
#[test]
fn passthrough_candidates_match_source() {
let src = PixelDescriptor::RGB8_SRGB;
let candidates = OpCategory::Passthrough.candidate_working_formats(src);
assert_eq!(candidates, vec![src]);
}
#[test]
fn resize_sharp_candidates_are_f32_linear() {
let src = PixelDescriptor::RGB8_SRGB;
let candidates = OpCategory::ResizeSharp.candidate_working_formats(src);
assert!(
candidates
.iter()
.all(|c| c.channel_type() == ChannelType::F32
&& c.transfer() == TransferFunction::Linear)
);
}
#[test]
fn composite_candidates_include_premul() {
let src = PixelDescriptor::RGBA8_SRGB;
let candidates = OpCategory::Composite.candidate_working_formats(src);
let has_premul = candidates
.iter()
.any(|c| c.alpha() == Some(AlphaMode::Premultiplied));
assert!(
has_premul,
"composite candidates must include premultiplied format"
);
}
#[test]
fn all_categories_have_intents() {
let categories = [
OpCategory::Passthrough,
OpCategory::ResizeGentle,
OpCategory::ResizeSharp,
OpCategory::Blur,
OpCategory::Sharpen,
OpCategory::OklabSharpen,
OpCategory::Composite,
OpCategory::OklabAdjust,
OpCategory::ColorMatrix,
OpCategory::Tonemap,
OpCategory::IccTransform,
OpCategory::Quantize,
OpCategory::Arithmetic,
];
for cat in &categories {
let _ = cat.to_intent();
let _ = cat.requirement();
}
assert_eq!(categories.len(), 13);
}
}