mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
Export: apply crop once when calculating resize dimensions
Calculate frame_size from the original rendered dimensions and effective render scale before cropping, so locked-aspect resize does not apply the crop margins a second time in its dimension calculation. Add dimension and decoded-PNG regressions for crop, resize, padding, scaled ROI, both interpolation qualities, and zero or extreme requested sizes. AI assistance: OpenAI Codex (GPT-6).
This commit is contained in:
@@ -431,12 +431,13 @@ impl<'a> Cx<'a> {
|
||||
fn crop_resize(&self, img: Image) -> Image {
|
||||
let om = self.job.output;
|
||||
let scale = self.comp().map(|c| self.job.settings.effective_scale(c)).unwrap_or_else(|| self.job.settings.resolution.clamp(0.01, 4.0));
|
||||
// frame_size applies crop itself: use the original frame and the same scale as edges.
|
||||
let (w, h) = om.frame_size(img.width, img.height, scale);
|
||||
let (t, l, b, r) = om.crop.edges(img.width, img.height, scale);
|
||||
let img = if (t, l, b, r) == (0, 0, 0, 0) { img } else { crop(&img, t, l, b, r) };
|
||||
if !om.resize.enabled {
|
||||
return img;
|
||||
}
|
||||
let (w, h) = om.frame_size(img.width, img.height, 1.0);
|
||||
if (w, h) == (img.width, img.height) {
|
||||
return img;
|
||||
}
|
||||
@@ -1062,6 +1063,87 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crop_resize_applies_margins_once() {
|
||||
use effectcraft_project::render_queue::{Crop, OutputModule, Resize};
|
||||
let output = OutputModule {
|
||||
crop: Crop { enabled: true, left: 480, right: 480, ..Default::default() },
|
||||
resize: Resize { enabled: true, width: 1280, lock_aspect: true, quality: ResizeQuality::Low, ..Default::default() },
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(output.frame_size(1920, 1080, 1.0), (1280, 1440));
|
||||
let img = run_crop_resize(Image::new(1920, 1080), &output, 1.0);
|
||||
assert_eq!((img.width, img.height), (1280, 1440));
|
||||
assert_eq!(img.data.len(), 1280 * 1440);
|
||||
}
|
||||
|
||||
fn run_crop_resize(img: Image, output: &effectcraft_project::render_queue::OutputModule, scale: f64) -> Image {
|
||||
use effectcraft_project::render_queue::RenderSettings;
|
||||
let project = Project::default();
|
||||
let settings = RenderSettings { resolution: scale, ..Default::default() };
|
||||
let job = Job {
|
||||
project: &project,
|
||||
footage: &effectcraft_render::NoFootage,
|
||||
expr: None,
|
||||
accel: None,
|
||||
comp: effectcraft_project::ItemId(0),
|
||||
settings: &settings,
|
||||
output,
|
||||
path: "unused.png",
|
||||
sink: None,
|
||||
nested_switches: true,
|
||||
options: Default::default(),
|
||||
};
|
||||
Cx::new(&job).crop_resize(img)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crop_resize_dimensions_match_the_output_module() {
|
||||
use effectcraft_project::render_queue::{Crop, OutputModule, Resize};
|
||||
let edges = Crop { enabled: true, left: 2, right: 2, ..Default::default() };
|
||||
let padding = Crop { enabled: true, left: -2, bottom: -2, ..Default::default() };
|
||||
let roi = |rect| Crop { enabled: true, use_roi: true, roi: Some(rect), ..Default::default() };
|
||||
for quality in [ResizeQuality::Low, ResizeQuality::High] {
|
||||
let resize = |width| Resize { enabled: true, width, height: 1, lock_aspect: true, quality };
|
||||
for (crop, resize, scale, expected) in [
|
||||
(Crop::default(), Resize::default(), 1.0, (8, 6)),
|
||||
(edges, Resize::default(), 1.0, (4, 6)),
|
||||
(Crop::default(), resize(4), 1.0, (4, 3)),
|
||||
(edges, resize(8), 1.0, (8, 12)),
|
||||
(padding, resize(5), 1.0, (5, 4)),
|
||||
(roi([4.0, 2.0, 8.0, 6.0]), resize(8), 0.5, (8, 6)),
|
||||
(roi([1.0, 0.5, 2.0, 1.5]), resize(8), 2.0, (8, 6)),
|
||||
(edges, Resize { lock_aspect: false, height: 2, ..resize(5) }, 1.0, (5, 2)),
|
||||
] {
|
||||
let output = OutputModule { crop, resize, ..Default::default() };
|
||||
assert_eq!(output.frame_size(8, 6, scale), expected);
|
||||
let img = run_crop_resize(Image::filled(8, 6, [0.25, 0.0, 0.0, 0.5]), &output, scale);
|
||||
assert_eq!((img.width, img.height), expected, "{crop:?}, {resize:?}, scale {scale}");
|
||||
assert_eq!(img.data.len(), expected.0 as usize * expected.1 as usize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crop_resize_zero_and_extreme_sizes_stay_bounded() {
|
||||
use effectcraft_project::render_queue::{Crop, OutputModule, Resize};
|
||||
for quality in [ResizeQuality::Low, ResizeQuality::High] {
|
||||
for (size, crop, width, height, expected) in [
|
||||
((8, 6), Crop::default(), 0, 0, (1, 1)),
|
||||
((8, 1), Crop::default(), u32::MAX, 1, (30_000, 1)),
|
||||
((1, 8), Crop::default(), 1, u32::MAX, (1, 30_000)),
|
||||
((8, 6), Crop { enabled: true, left: 20_000, right: 20_000, ..Default::default() }, 2, 3, (2, 3)),
|
||||
((0, 0), Crop { enabled: true, left: 1, top: 1, ..Default::default() }, 2, 3, (2, 3)),
|
||||
] {
|
||||
let output = OutputModule { crop, resize: Resize { enabled: true, width, height, lock_aspect: false, quality }, ..Default::default() };
|
||||
let img = run_crop_resize(Image::new(size.0, size.1), &output, 1.0);
|
||||
assert_eq!((img.width, img.height), expected);
|
||||
assert_eq!(img.data.len(), expected.0 as usize * expected.1 as usize);
|
||||
assert!(img.data.iter().all(|p| *p == [0.0; 4]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crop_and_pad() {
|
||||
let mut img = Image::new(4, 3);
|
||||
|
||||
@@ -174,6 +174,29 @@ fn crop_roi_and_resize() {
|
||||
assert_eq!((z.width(), z.height()), (32, 24));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crop_resize_preserves_pixels_when_the_cropped_size_is_requested() {
|
||||
let (p, cid) = project(FrameRate::new(25, 1), |_, _| {});
|
||||
for (crop, scale, expected) in [
|
||||
(Crop { enabled: true, left: 16, right: 16, ..Default::default() }, 1.0, (32, 48)),
|
||||
(Crop { enabled: true, left: -4, top: -2, ..Default::default() }, 1.0, (68, 50)),
|
||||
(Crop { enabled: true, use_roi: true, roi: Some([0.0, 24.0, 32.0, 24.0]), ..Default::default() }, 0.5, (16, 12)),
|
||||
(Crop { enabled: true, use_roi: true, roi: Some([0.0, 24.0, 32.0, 24.0]), ..Default::default() }, 2.0, (64, 48)),
|
||||
] {
|
||||
let settings = RenderSettings { resolution: scale, ..custom(0.4, 0.44) };
|
||||
let mut output = OutputModule { crop, ..OutputModule::for_format(OutputFormat::PngSequence) };
|
||||
let reference = png(&run(&p, cid, &settings, &output, "/c/reference_[#####].png"), 0);
|
||||
assert_eq!(reference.dimensions(), expected);
|
||||
assert!(reference.pixels().any(|p| p.0[0] > 150), "the reference contains visible content");
|
||||
for quality in [ResizeQuality::Low, ResizeQuality::High] {
|
||||
output.resize = Resize { enabled: true, width: expected.0, height: 1, lock_aspect: true, quality };
|
||||
let actual = png(&run(&p, cid, &settings, &output, "/c/resized_[#####].png"), 0);
|
||||
assert_eq!(actual.dimensions(), expected);
|
||||
assert!(actual == reference, "crop {crop:?}, scale {scale}, quality {quality:?}: pixels changed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alpha_only_and_premultiplied() {
|
||||
let (p, cid) = project(FrameRate::new(25, 1), |p, comp| {
|
||||
|
||||
Reference in New Issue
Block a user