use pixelflow_core::{
ClipFormat, ClipMedia, ClipResolution, ErrorCategory, ErrorCode, FilterCompatibility,
FilterDescriptor, FormatDescriptor, FormatFamily, PixelFlowError, Result, SampleType,
};
pub const STDLIB_PUBLISHER: &str = "pixelflow";
pub const STDLIB_PLUGIN: &str = "std";
pub(crate) const ALL_PLANAR_FAMILIES: &[FormatFamily] = &[
FormatFamily::Gray,
FormatFamily::Yuv,
FormatFamily::PlanarRgb,
];
pub(crate) const ALL_SAMPLE_TYPES: &[SampleType] =
&[SampleType::U8, SampleType::U16, SampleType::F32];
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum DitherMode {
#[default]
Fruit,
None,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SupportedFormats {
families: &'static [FormatFamily],
sample_types: &'static [SampleType],
}
impl SupportedFormats {
#[must_use]
pub const fn new(
families: &'static [FormatFamily],
sample_types: &'static [SampleType],
) -> Self {
Self {
families,
sample_types,
}
}
#[must_use]
pub fn contains(self, format: &FormatDescriptor) -> bool {
(self.families.is_empty() || self.families.contains(&format.family()))
&& (self.sample_types.is_empty() || self.sample_types.contains(&format.sample_type()))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct OfficialFilterContract {
name: &'static str,
supported_formats: SupportedFormats,
compatibility: FilterCompatibility,
}
impl OfficialFilterContract {
#[must_use]
pub const fn new(
name: &'static str,
supported_formats: SupportedFormats,
compatibility: FilterCompatibility,
) -> Self {
Self {
name,
supported_formats,
compatibility,
}
}
#[must_use]
pub const fn name(self) -> &'static str {
self.name
}
#[must_use]
pub const fn compatibility(self) -> FilterCompatibility {
self.compatibility
}
#[must_use]
pub fn descriptor(self) -> FilterDescriptor {
FilterDescriptor::new(self.name, STDLIB_PUBLISHER, STDLIB_PLUGIN)
}
pub fn validate_input_media(self, media: &ClipMedia) -> Result<&FormatDescriptor> {
let ClipFormat::Fixed(format) = media.format() else {
return Err(filter_error(
"filter.variable_format",
format!("filter '{}' requires fixed input format", self.name),
));
};
match media.resolution() {
ClipResolution::Fixed { width, height } if *width > 0 && *height > 0 => {}
ClipResolution::Fixed { .. } => {
return Err(filter_error(
"filter.invalid_resolution",
format!("filter '{}' requires non-zero input resolution", self.name),
));
}
ClipResolution::Variable => {
return Err(filter_error(
"filter.variable_resolution",
format!("filter '{}' requires fixed input resolution", self.name),
));
}
}
if !self.supported_formats.contains(format) {
return Err(filter_error(
"filter.unsupported_format",
format!(
"filter '{}' does not support input format '{}'",
self.name,
format.name()
),
));
}
Ok(format)
}
}
fn filter_error(code: &'static str, message: impl Into<String>) -> PixelFlowError {
PixelFlowError::new(ErrorCategory::Format, ErrorCode::new(code), message)
}
#[cfg(test)]
mod tests {
use pixelflow_core::{
ClipFormat, ClipMedia, ClipResolution, ErrorCategory, ErrorCode, FilterCompatibility,
FormatFamily, FrameCount, FrameRate, Rational, SampleType,
};
use crate::testkit::fixed_media;
use crate::{
BICUBIC_DEFAULT_B, BICUBIC_DEFAULT_C, COLORSPACE_GOLDEN_TOLERANCE, LANCZOS_DEFAULT_TAPS,
RESIZE_CONTRACT, resize::RESIZE_GOLDEN_TOLERANCE,
};
use crate::EXACT_GOLDEN_TOLERANCE;
use super::{DitherMode, OfficialFilterContract, SupportedFormats};
#[test]
fn official_contract_rejects_variable_format_before_render() {
let media = ClipMedia::new(
ClipFormat::Variable,
ClipResolution::Fixed {
width: 8,
height: 6,
},
FrameCount::Finite(2),
FrameRate::Cfr(Rational {
numerator: 24,
denominator: 1,
}),
);
let error = RESIZE_CONTRACT
.validate_input_media(&media)
.expect_err("variable format should fail upfront");
assert_eq!(error.category(), ErrorCategory::Format);
assert_eq!(error.code(), ErrorCode::new("filter.variable_format"));
}
#[test]
fn official_contract_rejects_unsupported_format_before_render() {
let media = fixed_media("rgbpf32", 8, 6);
let contract = OfficialFilterContract::new(
"integer_yuv_only",
SupportedFormats::new(&[FormatFamily::Yuv], &[SampleType::U8]),
FilterCompatibility::Preserve,
);
let error = contract
.validate_input_media(&media)
.expect_err("contract should reject unsupported input format");
assert_eq!(error.category(), ErrorCategory::Format);
assert_eq!(error.code(), ErrorCode::new("filter.unsupported_format"));
assert!(error.message().contains("integer_yuv_only"));
assert!(error.message().contains("rgbpf32"));
}
#[test]
fn official_contract_builds_descriptor_with_standard_namespace() {
let descriptor = RESIZE_CONTRACT.descriptor();
assert_eq!(descriptor.name(), "resize");
assert_eq!(descriptor.publisher(), "pixelflow");
assert_eq!(descriptor.plugin(), "std");
}
#[test]
fn resize_and_dither_defaults_are_fixed_for_later_filter_tasks() {
assert_eq!(BICUBIC_DEFAULT_B, 1.0 / 3.0);
assert_eq!(BICUBIC_DEFAULT_C, 1.0 / 3.0);
assert_eq!(LANCZOS_DEFAULT_TAPS, 3);
assert_eq!(DitherMode::default(), DitherMode::Fruit);
}
#[test]
fn golden_tolerances_are_explicit() {
assert_eq!(EXACT_GOLDEN_TOLERANCE.u8_abs, 0);
assert_eq!(EXACT_GOLDEN_TOLERANCE.u16_abs, 0);
assert_eq!(EXACT_GOLDEN_TOLERANCE.f32_abs, 0.0);
assert_eq!(RESIZE_GOLDEN_TOLERANCE.u8_abs, 1);
assert_eq!(RESIZE_GOLDEN_TOLERANCE.u16_abs, 4);
assert_eq!(RESIZE_GOLDEN_TOLERANCE.f32_abs, 0.00001);
assert_eq!(COLORSPACE_GOLDEN_TOLERANCE.u8_abs, 1);
assert_eq!(COLORSPACE_GOLDEN_TOLERANCE.u16_abs, 8);
assert_eq!(COLORSPACE_GOLDEN_TOLERANCE.f32_abs, 0.0002);
}
}