Files
Valentin Klein bb78d55825 After Effects project import (.aep / .aepx); export follows (#2558)
Add After Effects project import with bounded parsing, explicit loss reports, and regression coverage.

Fetch the cleaned external test corpus at commit 97262276f625afdeeb61787e4cbb74b3f17af510 and verify each fixture against the pinned SHA-256 manifest.
2026-11-04 12:06:08 +01:00

152 lines
8.3 KiB
Rust

//! Effects of batch 5 (cases 409-450, 511-516): integer sliders, checkbox defaults, the stored
//! scale of each slider (its valid maximum), popup items, and the arbitrary data of Hue/Saturation
//! and Levels.
use crate::container::helpers::{first_comp, open};
use effectcraft_aep::{EffectParam, HueSaturationData, LevelsData, ParamKind, ParamValue, Project};
/// Runs `f` on the parameters of the first effect of layer L (the only layer).
fn with_params<R>(name: &str, f: impl FnOnce(&[EffectParam<'_, '_>], &effectcraft_aep::Effect<'_, '_>) -> R) -> Option<R> {
Some({
let bytes = crate::common::try_fixture(name)?;
let file = open(&bytes);
let project = Project::new(&file).expect("project");
let comp = first_comp(&project);
let layer = comp.layers().next().expect("L");
let fx = layer.effects().into_iter().next().expect("an effect");
let params = project.effect_params(&fx);
f(&params, &fx)
})
}
fn by_name<'p, 'f, 'a>(params: &'p [EffectParam<'f, 'a>], name: &str) -> &'p EffectParam<'f, 'a> {
params.iter().find(|p| p.name == name || p.items.as_ref().is_some_and(|i| i.first().is_some_and(|f| f == name))).unwrap_or_else(|| panic!("no {name}"))
}
#[test]
fn invert_channel_items_and_blend() {
skip!(with_params("410-fx-invert-channel-2.aep", |p, fx| {
assert_eq!(fx.match_name(), "ADBE Invert");
let ch = by_name(p, "Channel");
assert_eq!(ch.kind, ParamKind::Popup);
// The items include separators "(-" (one index each).
assert_eq!(ch.items.as_ref().map(|i| (i.len(), i[1].as_str(), i[4].as_str(), i[15].as_str())), Some((16, "Red", "(-", "Alpha")));
assert_eq!(ch.stored_value(), Some(ParamValue::Index(2)));
assert_eq!(by_name(p, "Blend With Original").valid_max(), Some(100.0));
}));
skip!(with_params("411-fx-invert-blend-50.aep", |p, _| assert_eq!(by_name(p, "Blend With Original").stored_value(), Some(ParamValue::Number(50.0)))));
}
#[test]
fn fast_box_blur_integer_slider_and_checkbox_default_on() {
skip!(with_params("413-fx-fastbox-radius-20.aep", |p, _| {
let r = by_name(p, "Blur Radius");
assert_eq!((r.kind, r.valid_max(), r.stored_value()), (ParamKind::FixedSlider, Some(30000.0), Some(ParamValue::Number(20.0))));
let it = by_name(p, "Iterations");
assert_eq!((it.kind, it.pard_default(), it.valid_max(), it.stored_value()), (ParamKind::IntSlider, Some(ParamValue::Number(3.0)), Some(50.0), None));
// Repeat Edge Pixels is on by default (AE read-back 1): the pard default, no record.
let rep = by_name(p, "Repeat Edge Pixels");
assert_eq!((rep.kind, rep.pard_default(), rep.value()), (ParamKind::Checkbox, Some(ParamValue::Bool(true)), Some(ParamValue::Bool(true))));
}));
skip!(with_params("414-fx-fastbox-iterations-1.aep", |p, _| assert_eq!(by_name(p, "Iterations").stored_value(), Some(ParamValue::Number(1.0)))));
skip!(with_params("416-fx-fastbox-repeatedge-off.aep", |p, _| {
let rep = by_name(p, "Repeat Edge Pixels");
assert_eq!((rep.stored_value(), rep.value(), rep.is_default()), (Some(ParamValue::Bool(false)), Some(ParamValue::Bool(false)), Some(false)));
}));
}
#[test]
fn gaussian_blur_dimensions_and_repeat_edge() {
skip!(with_params("418-fx-gaussian-dimensions-3.aep", |p, _| {
assert_eq!(by_name(p, "Blur Dimensions").stored_value(), Some(ParamValue::Index(3)));
assert_eq!(by_name(p, "Blurriness").pard_default(), Some(ParamValue::Number(25.0)));
assert_eq!(by_name(p, "Repeat Edge Pixels").value(), Some(ParamValue::Bool(true)));
}));
skip!(with_params("419-fx-gaussian-repeatedge-off.aep", |p, _| assert_eq!(by_name(p, "Repeat Edge Pixels").value(), Some(ParamValue::Bool(false)))));
}
#[test]
fn drop_shadow_opacity_is_stored_on_0_to_255() {
skip!(with_params("422-fx-dropshadow-opacity-204.aep", |p, _| {
let o = by_name(p, "Opacity");
assert_eq!((o.valid_max(), o.pard_default(), o.stored_value()), (Some(255.0), Some(ParamValue::Number(127.5)), Some(ParamValue::Number(204.0))));
assert_eq!(by_name(p, "Distance").valid_max(), Some(4000.0));
assert_eq!(by_name(p, "Shadow Only").pard_default(), Some(ParamValue::Bool(false)));
}));
skip!(with_params("426-fx-dropshadow-shadowonly-on.aep", |p, _| assert_eq!(by_name(p, "Shadow Only").stored_value(), Some(ParamValue::Bool(true)))));
}
#[test]
fn glow_defaults_items_and_scales() {
skip!(with_params("427-fx-glow-default.aep", |p, _| {
let popup = |n: &str| (by_name(p, n).pard_default(), by_name(p, n).items.as_ref().map(|i| i.len()));
assert_eq!(popup("Glow Based On"), (Some(ParamValue::Index(2)), Some(2)));
assert_eq!(popup("Composite Original"), (Some(ParamValue::Index(2)), Some(3)));
assert_eq!(popup("Glow Operation"), (Some(ParamValue::Index(3)), Some(25)));
assert_eq!(popup("Color Looping"), (Some(ParamValue::Index(3)), Some(4)));
let t = by_name(p, "Glow Threshold");
assert_eq!((t.valid_max(), t.pard_default()), (Some(255.0), Some(ParamValue::Number(153.0))));
let m = by_name(p, "A & B Midpoint");
assert_eq!(m.pard_default(), Some(ParamValue::Number(0.5)));
assert!(m.valid_max().is_some_and(|v| (v - 0.99).abs() < 0.001));
}));
skip!(with_params("429-fx-glow-threshold-76.5.aep", |p, _| assert_eq!(by_name(p, "Glow Threshold").stored_value(), Some(ParamValue::Number(76.5)))));
skip!(with_params("438-fx-glow-midpoint-0.25.aep", |p, _| assert_eq!(by_name(p, "A & B Midpoint").stored_value(), Some(ParamValue::Number(0.25)))));
}
#[test]
fn hue_saturation_master_values_are_arbitrary_data() {
for (name, master) in [("444-fx-huesat-hue-90.aep", [90, 0, 0]), ("446-fx-huesat-light-m30.aep", [0, 0, -30])] {
skip!(with_params(name, |p, fx| {
// The visible Master parameters keep no record.
assert!(by_name(p, "Master Hue").stored().is_none(), "{name}");
let range = by_name(p, "Channel Range");
assert_eq!(range.kind, ParamKind::ArbitraryData);
let data = fx.arbitrary_data(&range.match_name).expect("data");
let d = HueSaturationData::parse(data).expect("180 bytes");
assert_eq!(d.master, master, "{name}");
assert!(d.ranges_are_default(), "{name}");
}));
}
skip!(with_params("448-fx-huesat-colorizehue-120.aep", |p, fx| {
assert_eq!(by_name(p, "Colorize Hue").stored_value(), Some(ParamValue::Number(120.0)));
assert_eq!(by_name(p, "Colorize Saturation").pard_default(), Some(ParamValue::Number(25.0)));
let d = HueSaturationData::parse(fx.arbitrary_data(&by_name(p, "Channel Range").match_name).expect("data")).expect("parses");
assert_eq!(d.master, [0, 0, 0]);
}));
}
#[test]
fn levels_values_are_arbitrary_data() {
let f32r = |x: f64| f32::from_bits((x as f32).to_bits());
for (name, rgb) in [
("512-fx-levels-inblack-0.1.aep", [f32r(0.1), 1.0, 1.0, 0.0, 1.0]),
("514-fx-levels-gamma-2.aep", [0.0, 1.0, 2.0, 0.0, 1.0]),
("516-fx-levels-outwhite-0.9.aep", [0.0, 1.0, 1.0, 0.0, f32r(0.9)]),
] {
skip!(with_params(name, |p, fx| {
assert!(by_name(p, "Input Black").stored().is_none(), "{name}: the visible parameter keeps no record");
let h = by_name(p, "Histogram");
assert_eq!(h.kind, ParamKind::ArbitraryData);
let d = LevelsData::parse(fx.arbitrary_data(&h.match_name).expect("data")).expect("108 bytes");
assert_eq!(d.header, [0x000f_10a7, 1], "{name}");
assert_eq!(d.channels[0], rgb, "{name}");
for c in &d.channels[1..] {
assert_eq!(*c, LevelsData::DEFAULT_CHANNEL, "{name}");
}
let clip = by_name(p, "Clip To Output Black");
assert_eq!(clip.items.as_ref().map(|i| i.join("|")), Some("On|Off|Off for 32 bpc Color".to_string()));
// A borrowed popup (a later Levels of the combined file) has no reliable pard default.
let want = if clip.borrowed_definition { None } else { Some(ParamValue::Index(3)) };
assert_eq!(clip.pard_default(), want, "{name}");
}));
}
}
#[test]
fn hostile_arbitrary_data_is_none() {
assert_eq!(HueSaturationData::parse(&[0; 179]), None);
assert_eq!(LevelsData::parse(&[0; 200]), None);
assert!(HueSaturationData::parse(&[0xff; 180]).is_some());
}