#![warn(missing_docs)]
use std::sync::Arc;
pub mod assume_fps;
pub mod bit_depth;
pub mod colorspace;
pub mod contracts;
pub mod crop;
pub mod dither;
pub mod ffms2_source;
pub mod format;
mod options;
pub mod planes;
pub mod props;
pub mod resize;
pub mod select;
pub mod trim;
#[cfg(test)]
pub(crate) mod testkit;
use pixelflow_core::{
ErrorCategory, ErrorCode, FilterRegistry, Graph, MetadataSchema, NodeId, NodeKind,
PixelFlowError, RenderExecutorMap, Result,
};
pub use assume_fps::{ASSUME_FPS_CONTRACT, FILTER_ASSUME_FPS};
pub use assume_fps::{
AssumeFpsExecutor, AssumeFpsOptions, ValidatedAssumeFps, add_assume_fps_filter,
assume_fps_output_media,
};
pub use bit_depth::{CONVERT_BIT_DEPTH_CONTRACT, FILTER_CONVERT_BIT_DEPTH};
pub use bit_depth::{
ConvertBitDepthExecutor, ConvertBitDepthOptions, ValidatedConvertBitDepth,
add_convert_bit_depth_filter, convert_bit_depth_output_media,
};
pub use colorspace::{CONVERT_COLORSPACE_CONTRACT, FILTER_CONVERT_COLORSPACE};
pub use colorspace::{
ConvertColorspaceExecutor, ConvertColorspaceOptions, ValidatedConvertColorspace,
add_convert_colorspace_filter, convert_colorspace_output_media,
};
pub use contracts::{
DitherMode, OfficialFilterContract, STDLIB_PLUGIN, STDLIB_PUBLISHER, SupportedFormats,
};
pub use crop::{
CROP_CONTRACT, CropExecutor, CropOptions, FILTER_CROP, ValidatedCrop, add_crop_filter,
crop_output_media,
};
pub use format::{CONVERT_FORMAT_CONTRACT, FILTER_CONVERT_FORMAT};
pub use format::{
ConvertFormatExecutor, ConvertFormatOptions, ValidatedConvertFormat, add_convert_format_filter,
convert_format_output_media,
};
pub use planes::{FILTER_MERGE_PLANES, FILTER_SPLIT_PLANE, MERGE_PLANES_CONTRACT};
pub use planes::{
MergePlanesExecutor, MergePlanesOptions, SPLIT_PLANE_CONTRACT, SplitPlaneExecutor,
SplitPlaneOptions, ValidatedMergePlanes, ValidatedSplitPlane, add_merge_planes_filter,
add_split_plane_filter, merge_planes_output_media, split_plane_output_media,
};
pub use props::{
CLEAR_PROP_CONTRACT, COPY_PROPS_CONTRACT, ClearPropExecutor, ClearPropOptions,
CopyPropsExecutor, FILTER_CLEAR_PROP, FILTER_COPY_PROPS, FILTER_REQUIRE_PROP, FILTER_SET_PROP,
REQUIRE_PROP_CONTRACT, RequirePropExecutor, RequirePropOptions, SET_PROP_CONTRACT,
SetPropExecutor, SetPropOptions, add_clear_prop_filter, add_copy_props_filter,
add_require_prop_filter, add_set_prop_filter, copy_props_output_media,
};
pub use resize::{
BICUBIC_DEFAULT_B, BICUBIC_DEFAULT_C, FILTER_RESIZE, LANCZOS_DEFAULT_TAPS, RESIZE_CONTRACT,
ResizeExecutor, ResizeKernel, ResizeOptions, ValidatedResize, add_resize_filter,
resize_output_media,
};
pub use select::{FILTER_SELECT, SELECT_CONTRACT};
pub use select::{
SelectExecutor, SelectOptions, ValidatedSelect, add_select_filter, select_output_media,
};
pub use trim::{FILTER_TRIM, TRIM_CONTRACT};
pub use trim::{TrimExecutor, TrimOptions, ValidatedTrim, add_trim_filter, trim_output_media};
#[cfg(test)]
pub use colorspace::COLORSPACE_GOLDEN_TOLERANCE;
#[cfg(test)]
pub use pixelflow_test_support::{EXACT_GOLDEN_TOLERANCE, GoldenTolerance};
#[cfg(test)]
pub use resize::RESIZE_GOLDEN_TOLERANCE;
pub const FILTER_CRATE_NAME: &str = "pixelflow-filters";
type BuiltinExecutorFactory = fn(
&[pixelflow_core::ClipMedia],
&MetadataSchema,
&pixelflow_core::FilterOptions,
) -> Result<Arc<dyn pixelflow_core::FrameExecutor>>;
struct BuiltinFilter {
contract: OfficialFilterContract,
planner: pixelflow_core::FilterPlanner,
executor: BuiltinExecutorFactory,
}
const BUILTIN_FILTERS: &[BuiltinFilter] = &[
BuiltinFilter {
contract: ASSUME_FPS_CONTRACT,
planner: assume_fps::plan_assume_fps,
executor: assume_fps_executor,
},
BuiltinFilter {
contract: CLEAR_PROP_CONTRACT,
planner: props::plan_clear_prop,
executor: clear_prop_executor,
},
BuiltinFilter {
contract: CONVERT_BIT_DEPTH_CONTRACT,
planner: bit_depth::plan_convert_bit_depth,
executor: convert_bit_depth_executor,
},
BuiltinFilter {
contract: CONVERT_COLORSPACE_CONTRACT,
planner: colorspace::plan_convert_colorspace,
executor: convert_colorspace_executor,
},
BuiltinFilter {
contract: CONVERT_FORMAT_CONTRACT,
planner: format::plan_convert_format,
executor: convert_format_executor,
},
BuiltinFilter {
contract: COPY_PROPS_CONTRACT,
planner: props::plan_copy_props,
executor: copy_props_executor,
},
BuiltinFilter {
contract: CROP_CONTRACT,
planner: crop::plan_crop,
executor: crop_executor,
},
BuiltinFilter {
contract: MERGE_PLANES_CONTRACT,
planner: planes::plan_merge_planes,
executor: merge_planes_executor,
},
BuiltinFilter {
contract: REQUIRE_PROP_CONTRACT,
planner: props::plan_require_prop,
executor: require_prop_executor,
},
BuiltinFilter {
contract: RESIZE_CONTRACT,
planner: resize::plan_resize,
executor: resize_executor,
},
BuiltinFilter {
contract: SELECT_CONTRACT,
planner: select::plan_select,
executor: select_executor,
},
BuiltinFilter {
contract: SET_PROP_CONTRACT,
planner: props::plan_set_prop,
executor: set_prop_executor,
},
BuiltinFilter {
contract: SPLIT_PLANE_CONTRACT,
planner: planes::plan_split_plane,
executor: split_plane_executor,
},
BuiltinFilter {
contract: TRIM_CONTRACT,
planner: trim::plan_trim,
executor: trim_executor,
},
];
fn assume_fps_executor(
input_media: &[pixelflow_core::ClipMedia],
_schema: &MetadataSchema,
options: &pixelflow_core::FilterOptions,
) -> Result<Arc<dyn pixelflow_core::FrameExecutor>> {
Ok(Arc::new(assume_fps::assume_fps_executor_from_options(
input_media,
options,
)?))
}
fn clear_prop_executor(
input_media: &[pixelflow_core::ClipMedia],
schema: &MetadataSchema,
options: &pixelflow_core::FilterOptions,
) -> Result<Arc<dyn pixelflow_core::FrameExecutor>> {
Ok(Arc::new(props::clear_prop_executor_from_options(
input_media,
schema,
options,
)?))
}
fn convert_bit_depth_executor(
input_media: &[pixelflow_core::ClipMedia],
_schema: &MetadataSchema,
options: &pixelflow_core::FilterOptions,
) -> Result<Arc<dyn pixelflow_core::FrameExecutor>> {
Ok(Arc::new(
bit_depth::convert_bit_depth_executor_from_options(input_media, options)?,
))
}
fn convert_colorspace_executor(
input_media: &[pixelflow_core::ClipMedia],
_schema: &MetadataSchema,
options: &pixelflow_core::FilterOptions,
) -> Result<Arc<dyn pixelflow_core::FrameExecutor>> {
Ok(Arc::new(
colorspace::convert_colorspace_executor_from_options(input_media, options)?,
))
}
fn convert_format_executor(
input_media: &[pixelflow_core::ClipMedia],
_schema: &MetadataSchema,
options: &pixelflow_core::FilterOptions,
) -> Result<Arc<dyn pixelflow_core::FrameExecutor>> {
Ok(Arc::new(format::convert_format_executor_from_options(
input_media,
options,
)?))
}
fn copy_props_executor(
input_media: &[pixelflow_core::ClipMedia],
_schema: &MetadataSchema,
options: &pixelflow_core::FilterOptions,
) -> Result<Arc<dyn pixelflow_core::FrameExecutor>> {
Ok(Arc::new(props::copy_props_executor_from_options(
input_media,
options,
)?))
}
fn crop_executor(
input_media: &[pixelflow_core::ClipMedia],
_schema: &MetadataSchema,
options: &pixelflow_core::FilterOptions,
) -> Result<Arc<dyn pixelflow_core::FrameExecutor>> {
Ok(Arc::new(crop::crop_executor_from_options(
input_media,
options,
)?))
}
fn merge_planes_executor(
input_media: &[pixelflow_core::ClipMedia],
_schema: &MetadataSchema,
options: &pixelflow_core::FilterOptions,
) -> Result<Arc<dyn pixelflow_core::FrameExecutor>> {
Ok(Arc::new(planes::merge_planes_executor_from_options(
input_media,
options,
)?))
}
fn require_prop_executor(
input_media: &[pixelflow_core::ClipMedia],
schema: &MetadataSchema,
options: &pixelflow_core::FilterOptions,
) -> Result<Arc<dyn pixelflow_core::FrameExecutor>> {
Ok(Arc::new(props::require_prop_executor_from_options(
input_media,
schema,
options,
)?))
}
fn resize_executor(
input_media: &[pixelflow_core::ClipMedia],
_schema: &MetadataSchema,
options: &pixelflow_core::FilterOptions,
) -> Result<Arc<dyn pixelflow_core::FrameExecutor>> {
Ok(Arc::new(resize::resize_executor_from_options(
input_media,
options,
)?))
}
fn select_executor(
input_media: &[pixelflow_core::ClipMedia],
_schema: &MetadataSchema,
options: &pixelflow_core::FilterOptions,
) -> Result<Arc<dyn pixelflow_core::FrameExecutor>> {
Ok(Arc::new(select::select_executor_from_options(
input_media,
options,
)?))
}
fn set_prop_executor(
input_media: &[pixelflow_core::ClipMedia],
schema: &MetadataSchema,
options: &pixelflow_core::FilterOptions,
) -> Result<Arc<dyn pixelflow_core::FrameExecutor>> {
Ok(Arc::new(props::set_prop_executor_from_options(
input_media,
schema,
options,
)?))
}
fn split_plane_executor(
input_media: &[pixelflow_core::ClipMedia],
_schema: &MetadataSchema,
options: &pixelflow_core::FilterOptions,
) -> Result<Arc<dyn pixelflow_core::FrameExecutor>> {
Ok(Arc::new(planes::split_plane_executor_from_options(
input_media,
options,
)?))
}
fn trim_executor(
input_media: &[pixelflow_core::ClipMedia],
_schema: &MetadataSchema,
options: &pixelflow_core::FilterOptions,
) -> Result<Arc<dyn pixelflow_core::FrameExecutor>> {
Ok(Arc::new(trim::trim_executor_from_options(
input_media,
options,
)?))
}
fn builtin_executor_factory(name: &str) -> Option<BuiltinExecutorFactory> {
BUILTIN_FILTERS
.iter()
.find(|entry| entry.contract.name() == name)
.map(|entry| entry.executor)
}
pub fn register_builtin_filters(registry: &mut FilterRegistry) -> Result<()> {
for entry in BUILTIN_FILTERS {
registry.register_filter_planner(entry.contract.descriptor(), entry.planner)?;
}
Ok(())
}
pub fn build_builtin_executors(
graph: &Graph,
schema: &MetadataSchema,
executors: &mut RenderExecutorMap,
) -> Result<()> {
for node_id in graph.validation_plan()?.reachable_nodes() {
if executors.contains(*node_id) {
continue;
}
let node = graph.node(*node_id).ok_or_else(|| missing_node(*node_id))?;
let NodeKind::Filter {
name,
inputs,
options,
..
} = node.kind()
else {
continue;
};
let input_media = inputs
.iter()
.map(|clip| {
graph
.node(clip.node_id())
.map(|input| input.media().clone())
.ok_or_else(|| missing_node(clip.node_id()))
})
.collect::<Result<Vec<_>>>()?;
let executor = builtin_executor_factory(name).ok_or_else(|| unsupported_executor(name))?(
&input_media,
schema,
options,
)?;
executors.insert(*node_id, executor);
}
Ok(())
}
fn missing_node(node_id: NodeId) -> PixelFlowError {
PixelFlowError::new(
ErrorCategory::Graph,
ErrorCode::new("graph.invalid_clip"),
format!("reachable node {} is missing", node_id.index()),
)
}
fn unsupported_executor(name: &str) -> PixelFlowError {
PixelFlowError::new(
ErrorCategory::Core,
ErrorCode::new("render.unsupported_filter_executor"),
format!("filter '{name}' does not have a Phase 1 runtime executor"),
)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use pixelflow_core::{
ClipResolution, ErrorCategory, ErrorCode, FilterCompatibility, FilterOptionValue,
GraphBuilder, MetadataKind, RenderExecutorMap,
};
use crate::testkit::fixed_media;
use super::{
ASSUME_FPS_CONTRACT, CLEAR_PROP_CONTRACT, CONVERT_BIT_DEPTH_CONTRACT,
CONVERT_COLORSPACE_CONTRACT, CONVERT_FORMAT_CONTRACT, COPY_PROPS_CONTRACT, CROP_CONTRACT,
FILTER_ASSUME_FPS, FILTER_CLEAR_PROP, FILTER_CONVERT_BIT_DEPTH, FILTER_CONVERT_COLORSPACE,
FILTER_CONVERT_FORMAT, FILTER_COPY_PROPS, FILTER_CROP, FILTER_MERGE_PLANES,
FILTER_REQUIRE_PROP, FILTER_RESIZE, FILTER_SELECT, FILTER_SET_PROP, FILTER_SPLIT_PLANE,
FILTER_TRIM, FilterRegistry, MERGE_PLANES_CONTRACT, REQUIRE_PROP_CONTRACT, RESIZE_CONTRACT,
SELECT_CONTRACT, SET_PROP_CONTRACT, SPLIT_PLANE_CONTRACT, STDLIB_PLUGIN, TRIM_CONTRACT,
builtin_executor_factory, register_builtin_filters,
};
fn options(
entries: impl IntoIterator<Item = (&'static str, FilterOptionValue)>,
) -> BTreeMap<String, FilterOptionValue> {
entries
.into_iter()
.map(|(name, value)| (name.to_owned(), value))
.collect()
}
#[test]
fn register_builtin_filters_records_established_official_names() {
let mut registry = FilterRegistry::new();
register_builtin_filters(&mut registry).expect("filter registration should succeed");
assert_eq!(
registry.filter_names(),
[
FILTER_ASSUME_FPS,
FILTER_CLEAR_PROP,
FILTER_CONVERT_BIT_DEPTH,
FILTER_CONVERT_COLORSPACE,
FILTER_CONVERT_FORMAT,
FILTER_COPY_PROPS,
FILTER_CROP,
FILTER_MERGE_PLANES,
FILTER_REQUIRE_PROP,
FILTER_RESIZE,
FILTER_SELECT,
FILTER_SET_PROP,
FILTER_SPLIT_PLANE,
FILTER_TRIM,
]
);
}
#[test]
fn builtin_filter_table_has_unique_names_and_executor_factories() {
let mut names = std::collections::BTreeSet::new();
for entry in super::BUILTIN_FILTERS {
assert!(
names.insert(entry.contract.name()),
"duplicate built-in filter {}",
entry.contract.name()
);
assert!(builtin_executor_factory(entry.contract.name()).is_some());
}
}
#[test]
fn official_filter_namespace_is_stable() {
assert_eq!(STDLIB_PLUGIN, "std");
}
#[test]
fn crop_api_is_reachable_from_crate_root() {
let options = super::CropOptions::new(0, 0, 2, 2);
let _executor_type_name = std::any::type_name::<super::CropExecutor>();
assert_eq!(options, super::CropOptions::new(0, 0, 2, 2));
}
#[test]
fn assume_fps_api_is_reachable_from_crate_root() {
let mut options = super::AssumeFpsOptions::new();
options.insert("fps".to_owned(), FilterOptionValue::Int(24));
let _executor_type_name = std::any::type_name::<super::AssumeFpsExecutor>();
assert_eq!(options.len(), 1);
}
#[test]
fn resize_api_is_reachable_from_crate_root() {
let options = super::ResizeOptions::new(640, 360);
let _executor_type_name = std::any::type_name::<super::ResizeExecutor>();
assert_eq!(options.kernel(), super::ResizeKernel::Bilinear);
}
#[test]
fn convert_format_api_is_reachable_from_crate_root() {
let options = super::ConvertFormatOptions::new("yuv420p8");
let _executor_type_name = std::any::type_name::<super::ConvertFormatExecutor>();
assert_eq!(options.format(), "yuv420p8");
}
#[test]
fn copy_props_api_is_reachable_from_crate_root() {
let executor = super::CopyPropsExecutor::new();
let _executor_type_name = std::any::type_name::<super::CopyPropsExecutor>();
assert_eq!(executor, super::CopyPropsExecutor::new());
}
#[test]
fn select_api_is_reachable_from_crate_root() {
let mut options = super::SelectOptions::new();
options.insert(
"frames".to_owned(),
FilterOptionValue::String("0,2".to_owned()),
);
let _executor_type_name = std::any::type_name::<super::SelectExecutor>();
assert_eq!(options.len(), 1);
}
#[test]
fn trim_api_is_reachable_from_crate_root() {
let options = super::TrimOptions::new(None, Some(6));
let _executor_type_name = std::any::type_name::<super::TrimExecutor>();
assert_eq!(options, super::TrimOptions::new(None, Some(6)));
}
#[test]
fn split_plane_api_is_reachable_from_crate_root() {
let options = super::SplitPlaneOptions::new(1);
let _executor_type_name = std::any::type_name::<super::SplitPlaneExecutor>();
assert_eq!(options.plane(), 1);
}
#[test]
fn merge_planes_api_is_reachable_from_crate_root() {
let options = super::MergePlanesOptions::new("yuv420p8");
let _executor_type_name = std::any::type_name::<super::MergePlanesExecutor>();
assert_eq!(options.format(), "yuv420p8");
}
#[test]
fn build_builtin_executors_adds_reachable_filter_executors() {
let mut builder = GraphBuilder::new();
let source_media = fixed_media("yuv420p8", 4, 2);
let source = builder.source("input", source_media.clone());
let crop_options = options([
("left", FilterOptionValue::Int(0)),
("top", FilterOptionValue::Int(0)),
("width", FilterOptionValue::Int(2)),
("height", FilterOptionValue::Int(2)),
]);
let mut registry = FilterRegistry::new();
register_builtin_filters(&mut registry).expect("built-ins should register");
let plan = registry
.plan_filter(
FILTER_CROP,
std::slice::from_ref(&source_media),
&crop_options,
)
.expect("crop should plan");
let (media, compatibility, dependencies, concurrency) = plan.into_parts();
let crop = builder
.filter_with_schedule_and_options(
FILTER_CROP,
&[source],
media,
compatibility,
dependencies,
concurrency,
crop_options,
)
.expect("crop node should build");
builder.set_output(crop);
let graph = builder.build();
let mut executors = RenderExecutorMap::new();
super::build_builtin_executors(
&graph,
&pixelflow_core::MetadataSchema::core(),
&mut executors,
)
.expect("executors should build");
assert!(executors.contains(crop.node_id()));
}
#[test]
fn build_builtin_executors_errors_for_reachable_unknown_filter() {
let mut builder = GraphBuilder::new();
let media = fixed_media("yuv420p8", 4, 2);
let source = builder.source("input", media.clone());
let output = builder
.filter(
"third_party.blur",
&[source],
media,
FilterCompatibility::Custom,
)
.expect("filter node should build");
builder.set_output(output);
let graph = builder.build();
let mut executors = RenderExecutorMap::new();
let error = super::build_builtin_executors(
&graph,
&pixelflow_core::MetadataSchema::core(),
&mut executors,
)
.expect_err("unknown runtime filter should fail before render");
assert_eq!(error.code().as_str(), "render.unsupported_filter_executor");
}
#[test]
fn builtin_planners_plan_official_filters_through_registry() {
let mut registry = FilterRegistry::new();
register_builtin_filters(&mut registry).expect("filter registration should succeed");
registry
.register_metadata_key("acme/filter:enabled", MetadataKind::Bool)
.expect("metadata key should register");
let input = fixed_media("yuv420p8", 1920, 1080);
let assume_fps = registry
.plan_filter(
FILTER_ASSUME_FPS,
std::slice::from_ref(&input),
&options([(
"fps",
FilterOptionValue::Rational(pixelflow_core::Rational {
numerator: 30_000,
denominator: 1_001,
}),
)]),
)
.expect("assume_fps planner should succeed");
assert_eq!(
assume_fps.compatibility(),
ASSUME_FPS_CONTRACT.compatibility()
);
assert_eq!(assume_fps.output_media().format(), input.format());
assert_eq!(assume_fps.output_media().resolution(), input.resolution());
assert_eq!(assume_fps.output_media().frame_count(), input.frame_count());
assert_eq!(
assume_fps.output_media().frame_rate(),
pixelflow_core::FrameRate::Cfr(pixelflow_core::Rational {
numerator: 30_000,
denominator: 1_001,
})
);
assert_eq!(
assume_fps.dependencies(),
&pixelflow_core::DependencyPattern::same_frame()
);
let set_prop = registry
.plan_filter(
FILTER_SET_PROP,
std::slice::from_ref(&input),
&options([
(
"key",
FilterOptionValue::String("acme/filter:enabled".to_owned()),
),
("value", FilterOptionValue::Bool(true)),
]),
)
.expect("set_prop planner should succeed");
assert_eq!(set_prop.output_media(), &input);
assert_eq!(set_prop.compatibility(), SET_PROP_CONTRACT.compatibility());
let clear_prop = registry
.plan_filter(
FILTER_CLEAR_PROP,
std::slice::from_ref(&input),
&options([(
"key",
FilterOptionValue::String("acme/filter:enabled".to_owned()),
)]),
)
.expect("clear_prop planner should succeed");
assert_eq!(clear_prop.output_media(), &input);
assert_eq!(
clear_prop.compatibility(),
CLEAR_PROP_CONTRACT.compatibility()
);
let require_prop = registry
.plan_filter(
FILTER_REQUIRE_PROP,
std::slice::from_ref(&input),
&options([
(
"key",
FilterOptionValue::String("acme/filter:enabled".to_owned()),
),
("kind", FilterOptionValue::String("bool".to_owned())),
]),
)
.expect("require_prop planner should succeed");
assert_eq!(require_prop.output_media(), &input);
assert_eq!(
require_prop.compatibility(),
REQUIRE_PROP_CONTRACT.compatibility()
);
let crop = registry
.plan_filter(
FILTER_CROP,
std::slice::from_ref(&input),
&options([
("left", FilterOptionValue::Int(16)),
("top", FilterOptionValue::Int(8)),
("width", FilterOptionValue::Int(1280)),
("height", FilterOptionValue::Int(720)),
]),
)
.expect("crop planner should succeed");
assert_eq!(crop.compatibility(), CROP_CONTRACT.compatibility());
assert_eq!(crop.output_media().format(), input.format());
assert_eq!(
crop.output_media().resolution(),
&ClipResolution::Fixed {
width: 1280,
height: 720,
}
);
let resize = registry
.plan_filter(
FILTER_RESIZE,
std::slice::from_ref(&input),
&options([
("width", FilterOptionValue::Int(1280)),
("height", FilterOptionValue::Int(720)),
]),
)
.expect("resize planner should succeed");
assert_eq!(resize.compatibility(), RESIZE_CONTRACT.compatibility());
assert_eq!(resize.output_media().format(), input.format());
assert_eq!(
resize.output_media().resolution(),
&ClipResolution::Fixed {
width: 1280,
height: 720,
}
);
let convert_format = registry
.plan_filter(
FILTER_CONVERT_FORMAT,
std::slice::from_ref(&input),
&options([("format", FilterOptionValue::String("yuv444p8".to_owned()))]),
)
.expect("convert_format planner should succeed");
assert_eq!(
convert_format.compatibility(),
CONVERT_FORMAT_CONTRACT.compatibility()
);
assert!(matches!(
convert_format.output_media().format(),
pixelflow_core::ClipFormat::Fixed(format) if format.name() == "yuv444p8"
));
assert_eq!(
convert_format.output_media().resolution(),
input.resolution()
);
let prop_source = fixed_media("gray8", 320, 180);
let copy_props = registry
.plan_filter(
FILTER_COPY_PROPS,
&[prop_source, input.clone()],
&options([]),
)
.expect("copy_props planner should succeed");
assert_eq!(copy_props.output_media(), &input);
assert_eq!(
copy_props.compatibility(),
COPY_PROPS_CONTRACT.compatibility()
);
let convert_bit_depth = registry
.plan_filter(
FILTER_CONVERT_BIT_DEPTH,
std::slice::from_ref(&input),
&options([("bits", FilterOptionValue::Int(10))]),
)
.expect("convert_bit_depth planner should succeed");
assert_eq!(
convert_bit_depth.compatibility(),
CONVERT_BIT_DEPTH_CONTRACT.compatibility()
);
assert!(matches!(
convert_bit_depth.output_media().format(),
pixelflow_core::ClipFormat::Fixed(format) if format.name() == "yuv420p10"
));
assert_eq!(
convert_bit_depth.output_media().resolution(),
input.resolution()
);
let convert_colorspace = registry
.plan_filter(
FILTER_CONVERT_COLORSPACE,
std::slice::from_ref(&input),
&options([("range", FilterOptionValue::String("limited".to_owned()))]),
)
.expect("convert_colorspace planner should succeed");
assert_eq!(
convert_colorspace.compatibility(),
CONVERT_COLORSPACE_CONTRACT.compatibility()
);
assert_eq!(convert_colorspace.output_media(), &input);
let split_plane = registry
.plan_filter(
FILTER_SPLIT_PLANE,
std::slice::from_ref(&input),
&options([("plane", FilterOptionValue::Int(1))]),
)
.expect("split_plane planner should succeed");
assert_eq!(
split_plane.compatibility(),
SPLIT_PLANE_CONTRACT.compatibility()
);
assert!(matches!(
split_plane.output_media().format(),
pixelflow_core::ClipFormat::Fixed(format) if format.name() == "gray8"
));
assert_eq!(
split_plane.output_media().resolution(),
&ClipResolution::Fixed {
width: 960,
height: 540,
}
);
let bad_split_plane = registry
.plan_filter(
FILTER_SPLIT_PLANE,
std::slice::from_ref(&input),
&options([("plane", FilterOptionValue::String("u".to_owned()))]),
)
.expect_err("split_plane planner should reject string plane identifiers");
assert_eq!(bad_split_plane.category(), ErrorCategory::Format);
assert_eq!(
bad_split_plane.code(),
ErrorCode::new("filter.invalid_split_plane")
);
let merge_planes = registry
.plan_filter(
FILTER_MERGE_PLANES,
&[
fixed_media("gray8", 1920, 1080),
fixed_media("gray8", 960, 540),
fixed_media("gray8", 960, 540),
],
&options([("format", FilterOptionValue::String("yuv420p8".to_owned()))]),
)
.expect("merge_planes planner should succeed");
assert_eq!(
merge_planes.compatibility(),
MERGE_PLANES_CONTRACT.compatibility()
);
assert!(matches!(
merge_planes.output_media().format(),
pixelflow_core::ClipFormat::Fixed(format) if format.name() == "yuv420p8"
));
assert_eq!(
merge_planes.output_media().resolution(),
&ClipResolution::Fixed {
width: 1920,
height: 1080,
}
);
let trim = registry
.plan_filter(
FILTER_TRIM,
std::slice::from_ref(&input),
&options([
("start", FilterOptionValue::Int(2)),
("end", FilterOptionValue::Int(6)),
]),
)
.expect("trim planner should succeed");
assert_eq!(trim.compatibility(), TRIM_CONTRACT.compatibility());
assert_eq!(trim.output_media().format(), input.format());
assert_eq!(trim.output_media().resolution(), input.resolution());
assert_eq!(
trim.output_media().frame_count(),
pixelflow_core::FrameCount::Finite(4)
);
assert_eq!(trim.output_media().frame_rate(), input.frame_rate());
assert_eq!(
trim.dependencies(),
&pixelflow_core::DependencyPattern::frame_map(
pixelflow_core::DynamicDependencyBounds::any()
)
);
let select = registry
.plan_filter(
FILTER_SELECT,
std::slice::from_ref(&input),
&options([("every", FilterOptionValue::Int(2))]),
)
.expect("select planner should succeed");
assert_eq!(select.compatibility(), SELECT_CONTRACT.compatibility());
assert_eq!(select.output_media().format(), input.format());
assert_eq!(select.output_media().resolution(), input.resolution());
assert_eq!(
select.output_media().frame_count(),
pixelflow_core::FrameCount::Finite(12)
);
assert_eq!(
select.dependencies(),
&pixelflow_core::DependencyPattern::frame_map(
pixelflow_core::DynamicDependencyBounds::any()
)
);
}
#[test]
fn builtin_resize_planner_parses_kernel_options_outside_script_crate() {
let mut registry = FilterRegistry::new();
register_builtin_filters(&mut registry).expect("filter registration should succeed");
let plan = registry
.plan_filter(
FILTER_RESIZE,
&[fixed_media("gray8", 640, 480)],
&options([
("width", FilterOptionValue::Int(320)),
("height", FilterOptionValue::Int(240)),
("kernel", FilterOptionValue::String("bicubic".to_owned())),
("b", FilterOptionValue::Float(0.25)),
("c", FilterOptionValue::Float(0.5)),
]),
)
.expect("resize planner should accept kernel options");
assert_eq!(plan.compatibility(), RESIZE_CONTRACT.compatibility());
assert_eq!(
plan.output_media().resolution(),
&ClipResolution::Fixed {
width: 320,
height: 240,
}
);
}
#[test]
fn builtin_resize_planner_rejects_kernel_specific_option_combinations() {
let mut registry = FilterRegistry::new();
register_builtin_filters(&mut registry).expect("filter registration should succeed");
let error = registry
.plan_filter(
FILTER_RESIZE,
&[fixed_media("gray8", 640, 480)],
&options([
("width", FilterOptionValue::Int(320)),
("height", FilterOptionValue::Int(240)),
("kernel", FilterOptionValue::String("nearest".to_owned())),
("taps", FilterOptionValue::Int(3)),
]),
)
.expect_err("non-lanczos resize should reject taps option");
assert_eq!(error.category(), ErrorCategory::Format);
assert_eq!(
error.code(),
ErrorCode::new("filter.invalid_resize_parameter")
);
}
#[test]
fn builtin_colorspace_planner_reports_invalid_option_values_outside_script_crate() {
let mut registry = FilterRegistry::new();
register_builtin_filters(&mut registry).expect("filter registration should succeed");
let error = registry
.plan_filter(
FILTER_CONVERT_COLORSPACE,
&[fixed_media("gray8", 4, 1)],
&options([("matrix", FilterOptionValue::String("rec709".to_owned()))]),
)
.expect_err("invalid matrix should fail during planning");
assert_eq!(error.category(), ErrorCategory::Format);
assert_eq!(error.code(), ErrorCode::new("format.unsupported_matrix"));
}
#[test]
fn builtin_convert_format_planner_rejects_bit_depth_changes() {
let mut registry = FilterRegistry::new();
register_builtin_filters(&mut registry).expect("filter registration should succeed");
let error = registry
.plan_filter(
FILTER_CONVERT_FORMAT,
&[fixed_media("yuv420p8", 1920, 1080)],
&options([("format", FilterOptionValue::String("yuv420p10".to_owned()))]),
)
.expect_err("convert_format should not hide bit-depth conversion");
assert_eq!(error.category(), ErrorCategory::Format);
assert_eq!(
error.code(),
ErrorCode::new("filter.unsupported_format_conversion")
);
assert!(error.message().contains("convert_bit_depth"));
}
#[test]
fn builtin_convert_format_planner_requires_colorimetry_for_rgb_yuv_changes() {
let mut registry = FilterRegistry::new();
register_builtin_filters(&mut registry).expect("filter registration should succeed");
let error = registry
.plan_filter(
FILTER_CONVERT_FORMAT,
&[fixed_media("rgbp8", 1920, 1080)],
&options([("format", FilterOptionValue::String("yuv444p8".to_owned()))]),
)
.expect_err("convert_format should require explicit YUV target colorimetry");
assert_eq!(error.category(), ErrorCategory::Format);
assert_eq!(error.code(), ErrorCode::new("filter.missing_colorimetry"));
assert!(error.message().contains("matrix"));
}
}