use pixelflow_core::{
Clip, ClipMedia, ClipResolution, ErrorCategory, ErrorCode, FilterChangeSet,
FilterCompatibility, FilterPlan, FilterPlanRequest, FormatDescriptor, Frame, FrameExecutor,
FrameRequest, GraphBuilder, PixelFlowError, Result,
};
use crate::{
OfficialFilterContract, SupportedFormats,
contracts::{ALL_PLANAR_FAMILIES, ALL_SAMPLE_TYPES},
options::{optional_i64, required_i64, single_input_media, single_input_slice},
};
pub const FILTER_CROP: &str = "crop";
pub const CROP_CONTRACT: OfficialFilterContract = OfficialFilterContract::new(
FILTER_CROP,
SupportedFormats::new(ALL_PLANAR_FAMILIES, ALL_SAMPLE_TYPES),
FilterCompatibility::AllowChanges(FilterChangeSet {
format: false,
resolution: true,
frame_count: false,
frame_rate: false,
}),
);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CropOptions {
left: i64,
top: i64,
width: i64,
height: i64,
}
impl CropOptions {
#[must_use]
pub const fn new(left: i64, top: i64, width: i64, height: i64) -> Self {
Self {
left,
top,
width,
height,
}
}
}
#[cfg(test)]
impl CropOptions {
fn validate_for_tests(self, input: &ClipMedia) -> Result<ValidatedCrop> {
crop_output_media(input, self).map(|(crop, _media)| crop)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ValidatedCrop {
left: usize,
top: usize,
width: usize,
height: usize,
}
impl ValidatedCrop {
#[must_use]
pub const fn left(self) -> usize {
self.left
}
#[must_use]
pub const fn top(self) -> usize {
self.top
}
#[must_use]
pub const fn width(self) -> usize {
self.width
}
#[must_use]
pub const fn height(self) -> usize {
self.height
}
fn output_media(self, input: &ClipMedia) -> ClipMedia {
ClipMedia::new(
input.format().clone(),
ClipResolution::Fixed {
width: self.width,
height: self.height,
},
input.frame_count(),
input.frame_rate(),
)
}
}
pub fn crop_output_media(
input: &ClipMedia,
options: CropOptions,
) -> Result<(ValidatedCrop, ClipMedia)> {
let format = CROP_CONTRACT.validate_input_media(input)?;
let (input_width, input_height) = match input.resolution() {
ClipResolution::Fixed { width, height } => (*width, *height),
ClipResolution::Variable => unreachable!("crop contract already requires fixed resolution"),
};
let crop = validate_crop(format, options, input_width, input_height)?;
Ok((crop, crop.output_media(input)))
}
pub fn add_crop_filter(
builder: &mut GraphBuilder,
input: Clip,
input_media: &ClipMedia,
options: CropOptions,
) -> Result<(Clip, CropExecutor)> {
let (crop, output_media) = crop_output_media(input_media, options)?;
let output = builder.filter(
FILTER_CROP,
&[input],
output_media,
CROP_CONTRACT.compatibility(),
)?;
Ok((output, CropExecutor::new(crop)))
}
fn parse_crop_options(options: &pixelflow_core::FilterOptions) -> Result<CropOptions> {
let left = optional_i64(options, FILTER_CROP, "left", "filter.invalid_crop")?.unwrap_or(0);
let top = optional_i64(options, FILTER_CROP, "top", "filter.invalid_crop")?.unwrap_or(0);
let width = required_i64(options, FILTER_CROP, "width", "filter.invalid_crop")?;
let height = required_i64(options, FILTER_CROP, "height", "filter.invalid_crop")?;
Ok(CropOptions::new(left, top, width, height))
}
pub(crate) fn plan_crop(request: FilterPlanRequest<'_>) -> Result<FilterPlan> {
let input = single_input_media(request, FILTER_CROP)?;
let output_media = crop_output_media(input, parse_crop_options(request.options())?)?.1;
Ok(FilterPlan::new(output_media, CROP_CONTRACT.compatibility()))
}
pub(crate) fn crop_executor_from_options(
input_media: &[ClipMedia],
options: &pixelflow_core::FilterOptions,
) -> Result<CropExecutor> {
let input = single_input_slice(input_media, FILTER_CROP)?;
let (crop, _media) = crop_output_media(input, parse_crop_options(options)?)?;
Ok(CropExecutor::new(crop))
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CropExecutor {
crop: ValidatedCrop,
}
impl CropExecutor {
#[must_use]
pub const fn new(crop: ValidatedCrop) -> Self {
Self { crop }
}
#[must_use]
pub const fn crop(&self) -> ValidatedCrop {
self.crop
}
}
impl FrameExecutor for CropExecutor {
fn prepare(&self, request: FrameRequest<'_>) -> Result<Frame> {
let input = request.input_frame(0, request.frame_number())?;
input.view(
self.crop.left(),
self.crop.top(),
self.crop.width(),
self.crop.height(),
)
}
}
fn validate_crop(
format: &FormatDescriptor,
options: CropOptions,
input_width: usize,
input_height: usize,
) -> Result<ValidatedCrop> {
let left = non_negative_offset("left", options.left)?;
let top = non_negative_offset("top", options.top)?;
let width = positive_dimension("width", options.width)?;
let height = positive_dimension("height", options.height)?;
let right = left
.checked_add(width)
.ok_or_else(|| crop_bounds_error(left, top, width, height))?;
let bottom = top
.checked_add(height)
.ok_or_else(|| crop_bounds_error(left, top, width, height))?;
if right > input_width || bottom > input_height {
return Err(crop_error(
"filter.crop_out_of_bounds",
format!(
"filter '{FILTER_CROP}' crop rectangle {left},{top} {width}x{height} exceeds input {input_width}x{input_height}"
),
));
}
validate_plane_alignment(format, left, top, right, bottom)?;
Ok(ValidatedCrop {
left,
top,
width,
height,
})
}
fn non_negative_offset(name: &str, value: i64) -> Result<usize> {
usize::try_from(value).map_err(|_| {
crop_error(
"filter.invalid_crop",
format!("filter '{FILTER_CROP}' crop {name} must be non-negative"),
)
})
}
fn positive_dimension(name: &str, value: i64) -> Result<usize> {
let value = usize::try_from(value).map_err(|_| {
crop_error(
"filter.invalid_crop",
format!("filter '{FILTER_CROP}' crop {name} must be positive"),
)
})?;
if value == 0 {
return Err(crop_error(
"filter.invalid_crop",
format!("filter '{FILTER_CROP}' crop {name} must be positive"),
));
}
Ok(value)
}
fn validate_plane_alignment(
format: &FormatDescriptor,
left: usize,
top: usize,
right: usize,
bottom: usize,
) -> Result<()> {
for plane in format.planes() {
if !left.is_multiple_of(plane.width_divisor) || !right.is_multiple_of(plane.width_divisor) {
return Err(crop_error(
"filter.crop_chroma_alignment",
format!(
"filter '{FILTER_CROP}' crop horizontal edges must align to plane divisor {} for format '{}'",
plane.width_divisor,
format.name()
),
));
}
if !top.is_multiple_of(plane.height_divisor) || !bottom.is_multiple_of(plane.height_divisor)
{
return Err(crop_error(
"filter.crop_chroma_alignment",
format!(
"filter '{FILTER_CROP}' crop vertical edges must align to plane divisor {} for format '{}'",
plane.height_divisor,
format.name()
),
));
}
}
Ok(())
}
fn crop_bounds_error(left: usize, top: usize, width: usize, height: usize) -> PixelFlowError {
crop_error(
"filter.crop_out_of_bounds",
format!(
"filter '{FILTER_CROP}' crop rectangle {left},{top} {width}x{height} exceeds input dimensions"
),
)
}
fn crop_error(code: &'static str, message: impl Into<String>) -> PixelFlowError {
PixelFlowError::new(ErrorCategory::Format, ErrorCode::new(code), message)
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use pixelflow_core::{
ClipFormat, ClipResolution, ErrorCategory, ErrorCode, Frame, FrameCount, FrameExecutor,
FrameRate, FrameRequest, GraphBuilder, MetadataValue, Rational, RenderExecutorMap,
RenderOptions, WorkerPoolConfig,
};
use crate::EXACT_GOLDEN_TOLERANCE;
use crate::testkit::{
assert_plane_u8_near, fixed_media, synthetic_u8_frame, with_frame_number_metadata,
};
use super::{CropOptions, crop_output_media};
#[test]
fn crop_options_validates_output_media_and_preserves_timing() {
let input = fixed_media("yuv420p10", 1920, 1080);
let (crop, output) = crop_output_media(&input, CropOptions::new(16, 8, 1280, 720))
.expect("aligned crop should validate");
assert_eq!(crop.left(), 16);
assert_eq!(crop.top(), 8);
assert_eq!(crop.width(), 1280);
assert_eq!(crop.height(), 720);
assert_eq!(output.format(), input.format());
assert_eq!(
output.resolution(),
&ClipResolution::Fixed {
width: 1280,
height: 720,
}
);
assert_eq!(output.frame_count(), FrameCount::Finite(24));
assert_eq!(
output.frame_rate(),
FrameRate::Cfr(Rational {
numerator: 24_000,
denominator: 1_001,
})
);
}
#[test]
fn crop_options_accepts_planar_rgb_without_chroma_alignment_restrictions() {
let input = fixed_media("rgbp8", 7, 5);
let (crop, output) = crop_output_media(&input, CropOptions::new(1, 1, 5, 3))
.expect("RGB crop should validate on sample boundaries");
assert_eq!(
(crop.left(), crop.top(), crop.width(), crop.height()),
(1, 1, 5, 3)
);
assert!(matches!(output.format(), ClipFormat::Fixed(format) if format.name() == "rgbp8"));
}
#[test]
fn crop_options_rejects_negative_offsets_and_non_positive_dimensions() {
let input = fixed_media("gray8", 8, 6);
for options in [
CropOptions::new(-1, 0, 4, 4),
CropOptions::new(0, -1, 4, 4),
CropOptions::new(0, 0, 0, 4),
CropOptions::new(0, 0, 4, 0),
CropOptions::new(0, 0, -4, 4),
CropOptions::new(0, 0, 4, -4),
] {
let error = crop_output_media(&input, options).expect_err("invalid crop should fail");
assert_eq!(error.category(), ErrorCategory::Format);
assert_eq!(error.code(), ErrorCode::new("filter.invalid_crop"));
}
}
#[test]
fn crop_options_rejects_out_of_bounds_rectangles() {
let input = fixed_media("gray8", 8, 6);
for options in [
CropOptions::new(7, 0, 2, 1),
CropOptions::new(0, 5, 1, 2),
CropOptions::new(i64::MAX, 0, 1, 1),
] {
let error = crop_output_media(&input, options).expect_err("bounds check should fail");
assert_eq!(error.category(), ErrorCategory::Format);
assert_eq!(error.code(), ErrorCode::new("filter.crop_out_of_bounds"));
}
}
#[test]
fn crop_options_rejects_chroma_misaligned_yuv420_and_yuv422_rectangles() {
let yuv420 = fixed_media("yuv420p8", 8, 6);
let yuv422 = fixed_media("yuv422p8", 8, 6);
for (input, options) in [
(&yuv420, CropOptions::new(1, 0, 4, 4)),
(&yuv420, CropOptions::new(0, 1, 4, 4)),
(&yuv420, CropOptions::new(0, 0, 5, 4)),
(&yuv420, CropOptions::new(0, 0, 4, 5)),
(&yuv422, CropOptions::new(1, 0, 4, 4)),
(&yuv422, CropOptions::new(0, 0, 5, 4)),
] {
let error = crop_output_media(input, options).expect_err("misaligned crop should fail");
assert_eq!(error.category(), ErrorCategory::Format);
assert_eq!(error.code(), ErrorCode::new("filter.crop_chroma_alignment"));
}
}
#[test]
fn add_crop_filter_creates_filter_node_with_validated_output_media() {
let input_media = fixed_media("gray8", 8, 6);
let mut builder = GraphBuilder::new();
let source = builder.source("source", input_media.clone());
let (cropped, executor) = super::add_crop_filter(
&mut builder,
source,
&input_media,
CropOptions::new(1, 2, 4, 3),
)
.expect("crop node should build");
let graph = builder.build();
let node = graph
.node(cropped.node_id())
.expect("crop node should exist");
assert_eq!(executor.crop().left(), 1);
assert_eq!(executor.crop().top(), 2);
assert_eq!(
node.media().resolution(),
&ClipResolution::Fixed {
width: 4,
height: 3,
}
);
assert_eq!(node.media().frame_count(), input_media.frame_count());
assert_eq!(node.media().frame_rate(), input_media.frame_rate());
}
#[test]
fn crop_executor_returns_zero_copy_visible_region_and_metadata() {
struct RecordingSource {
last_frame: Arc<Mutex<Option<Frame>>>,
}
impl FrameExecutor for RecordingSource {
fn prepare(&self, request: FrameRequest<'_>) -> pixelflow_core::Result<Frame> {
let frame = synthetic_u8_frame("gray8", 5, 4, |_plane, x, y| {
u8::try_from(x + y * 10).expect("fixture sample fits u8")
})?;
let frame = with_frame_number_metadata(&frame, request.frame_number())?;
*self.last_frame.lock().expect("test mutex should lock") = Some(frame.clone());
Ok(frame)
}
}
let input_media = fixed_media("gray8", 5, 4);
let mut builder = GraphBuilder::new();
let source = builder.source("source", input_media.clone());
let (cropped, executor) = super::add_crop_filter(
&mut builder,
source,
&input_media,
CropOptions::new(1, 1, 3, 2),
)
.expect("crop node should build");
builder.set_output(cropped);
let graph = builder.build();
let last_frame = Arc::new(Mutex::new(None));
let mut executors = RenderExecutorMap::new();
executors.insert(
source.node_id(),
Arc::new(RecordingSource {
last_frame: last_frame.clone(),
}),
);
executors.insert(cropped.node_id(), Arc::new(executor));
let mut render = pixelflow_core::RenderEngine::new(WorkerPoolConfig::new(1))
.render_ordered(graph, executors, RenderOptions::new(2, Some(3)))
.expect("render should start");
let output = render
.next()
.expect("one frame should render")
.expect("crop should succeed");
let source_frame = last_frame
.lock()
.expect("test mutex should lock")
.clone()
.expect("source frame should be recorded");
assert_eq!(output.width(), 3);
assert_eq!(output.height(), 2);
assert!(source_frame.shares_plane_storage(&output, 0));
assert_eq!(
output.metadata().get("core:frame_number"),
Some(&MetadataValue::Int(2))
);
assert_plane_u8_near(
&output,
0,
&[&[11, 12, 13], &[21, 22, 23]],
EXACT_GOLDEN_TOLERANCE,
);
assert!(render.next().is_none());
}
#[test]
fn crop_executor_preserves_chroma_plane_alignment_for_yuv420() {
let input = synthetic_u8_frame("yuv420p8", 4, 4, |plane, x, y| {
u8::try_from(plane * 40 + x + y * 10).expect("fixture sample fits u8")
})
.expect("synthetic YUV frame should build");
let crop = CropOptions::new(2, 2, 2, 2)
.validate_for_tests(&fixed_media("yuv420p8", 4, 4))
.expect("aligned crop should validate");
let output = input
.view(crop.left(), crop.top(), crop.width(), crop.height())
.expect("validated crop should view");
assert!(input.shares_plane_storage(&output, 0));
assert!(input.shares_plane_storage(&output, 1));
assert!(input.shares_plane_storage(&output, 2));
assert_plane_u8_near(&output, 0, &[&[22, 23], &[32, 33]], EXACT_GOLDEN_TOLERANCE);
assert_plane_u8_near(&output, 1, &[&[51]], EXACT_GOLDEN_TOLERANCE);
assert_plane_u8_near(&output, 2, &[&[91]], EXACT_GOLDEN_TOLERANCE);
}
}