Files
effectcraft/crates/aep/tests/it/container/helpers.rs
T
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

414 lines
16 KiB
Rust

//! Helpers shared by the container tests.
#![allow(dead_code)]
/// The generated fixtures (own test data, see ATTRIBUTION.md).
pub const FIXTURES: &[&str] = &[
"000-base.aep",
"001-empty.aep",
"010-layer-name.aep",
"012-layer-shy.aep",
"013-layer-locked.aep",
"014-layer-solo.aep",
"015-layer-3d.aep",
"016-layer-blend-multiply.aep",
"017-layer-motionblur.aep",
"018-layer-stretch200.aep",
"019-layer-inpoint1.aep",
"021-layer-starttime0.5.aep",
"022-layer-parent-null.aep",
"023-layer-quality-draft.aep",
"024-layer-collapse.aep",
"025-layer-adjustment.aep",
"026-layer-guide.aep",
"030-comp-size-1280x720.aep",
"031-comp-fps30.aep",
"032-comp-duration10.aep",
"033-comp-bg-green.aep",
"034-comp-shutter90.aep",
"035-comp-workarea-1-2.aep",
"037-comp-pixelaspect2.aep",
"038-comp-motionblur.aep",
"039-comp-renderer-advanced3d.aep",
"040-static-position-100-200.aep",
"041-static-scale-50.aep",
"042-static-rotation-45.aep",
"043-static-opacity-25.aep",
"044-static-anchor-10-20.aep",
"052-key-position-2key-ease.aep",
"053-key-position-2key-hold-key1.aep",
"054-key-position-2key-spatial-bezier.aep",
"055-key-position-3key-inserted-t2.5.aep",
"056-key-position-3key-roving-key2.aep",
"057-key-position-3key-label9-key2.aep",
"058-key-opacity-2key.aep",
"060-expr-opacity-wiggle.aep",
"071-fx-slider-value77.aep",
"073-fx-color-0.2-0.6-1.aep",
"077-fx-dropdown-default.aep",
"080-fx-dropshadow-default.aep",
"090-mask-rect.aep",
"091-mask-rect-props.aep",
"093-mask-path-2keys.aep",
"110-text-A.aep",
"113-text-Hello-size60-fillorange.aep",
"122-marker-comp-1s.aep",
"123-marker-layer-1s.aep",
"124-precomp-Pre.aep",
"126-footage-png-layer.aep",
"202-key-out-hold-in-linear.aep",
"203-key-in-hold-out-linear.aep",
"204-key-ease-asymmetric.aep",
"205-key-opacity-ease-asymmetric.aep",
"206-key-scale-2key-linear.aep",
"208-key-mask-feather-2key-linear.aep",
"210-key-fill-color-2key-linear.aep",
"211-anchor-100x100-solid-10-20.aep",
"212-anchor-400x300-solid-100-50.aep",
"213-expr-opacity-wiggle-disabled.aep",
"214-fx-gaussianblur-default.aep",
"215-fx-fill-default.aep",
"216-fx-gaussianblur-40.aep",
"217-fx-checkbox-on.aep",
"218-fx-point-111-222.aep",
"219-fx-angle-90.aep",
"220-fx-layercontrol-to-layer1.aep",
"221-fx-slider-2key.aep",
"222-layer-video-off.aep",
"223-layer-audio-off.aep",
"224-layer-effects-off.aep",
"225-layer-blend-screen.aep",
"226-layer-blend-add.aep",
"227-layer-blend-overlay.aep",
"228-layer-blend-difference.aep",
"229-comp-fps-29.97.aep",
"230-comp-fps-23.976.aep",
"231-comp-bg-grey-0.5.aep",
"232-mask-ellipse.aep",
"233-mask-open-path.aep",
"234-mask-mode-intersect.aep",
"235-mask-mode-lighten.aep",
"236-mask-mode-darken.aep",
"237-mask-mode-difference.aep",
"238-shape-rect-rounded-corners.aep",
"239-shape-stroke-green-w8-round.aep",
"240-shape-custom-path-tangents.aep",
"241-shape-gradient-fill-linear.aep",
"242-shape-group-position-50-50.aep",
"243-text-multiline-A-CR-B.aep",
"245-text-box-400x200-Hello.aep",
"246-text-box-center-align.aep",
"247-text-tracking-100.aep",
"248-text-range-fill-red-1-3.aep",
"249-marker-layer-full.aep",
"251-camera-one-node.aep",
"252-light-point.aep",
"253-camera-position-100-200-m1000.aep",
"254-light-spot-intensity-50.aep",
"256-camera-two-node-default.aep",
"259-solid-blue-0-0-1.aep",
"299-save-noise.aep",
"300-fx-gaussianblur-0-explicit.aep",
"301-fx-gaussianblur-40-then-25.aep",
"302-fx-fill-opacity-100-explicit.aep",
"303-fx-fill-opacity-50-then-100.aep",
"304-fx-slider-0-explicit.aep",
"305-key-scale-bezier-ease.aep",
"306-key-maskfeather-bezier-ease.aep",
"307-key-fillcolor-bezier.aep",
"309-solid-100x100-mask-rect.aep",
"310-solid-100x100-pointcontrol.aep",
"311-position-default-explicit.aep",
"316-layer-stretch50.aep",
"317-layer-stretch150.aep",
"318-layer-inpoint-0.3333.aep",
"319-precomp-timeremap.aep",
"320-fx-layercontrol-self-explicit.aep",
"321-fx-layercontrol-none.aep",
"323-fx-dropdown-5items.aep",
"324-fx-dropdown-5items-select3.aep",
"325-comp-pixelaspect-0.9091.aep",
"326-comp-shutterphase-m90.aep",
"327-comp-displaystartframe-100.aep",
"328-mask-two-ellipse-rect.aep",
"329-mask-ellipse-mode-none.aep",
"330-shape-polystar-star-5-50-100.aep",
"331-shape-polystar-polygon-5-100.aep",
"332-shape-gradient-fill-radial.aep",
"333-shape-group-two-rects.aep",
"334-text-box-right-align.aep",
"335-text-point-fauxbold.aep",
"336-text-point-fauxitalic.aep",
"337-text-point-allcaps.aep",
"338-text-point-smallcaps.aep",
"339-text-point-leading60.aep",
"340-text-range-two-styles.aep",
"341-camera-zoom-2000.aep",
"342-camera-dof-800-30.aep",
"343-light-spot-color-red.aep",
"344-light-spot-coneangle-60.aep",
"345-light-spot-conefeather-30.aep",
"347-marker-layer-label3.aep",
"348-marker-layer-navigation-cue.aep",
"349-marker-layer-event-cue-params.aep",
"350-footage-wav-imported.aep",
"351-footage-wav-as-layer.aep",
"352-footage-pngseq-imported.aep",
"354-footage-pngseq-timeremap.aep",
"356-fx-tint-default.aep",
"357-fx-tint-black-0.2-0.4-0.6.aep",
"361-fx-tint-amount-0.aep",
"363-fx-tint-amount-2key.aep",
"364-fx-tint-white-2key.aep",
"366-fx-blur-then-tint.aep",
"368-fx-ramp-default.aep",
"369-fx-ramp-start-100-200.aep",
"371-fx-ramp-startcolor-red.aep",
"374-fx-ramp-end-2000-m100.aep",
"377-fx-ramp-shape-radial.aep",
"378-fx-ramp-scatter-12.aep",
"380-fx-ramp-blend-25pct.aep",
"382-fx-ramp-start-2key.aep",
"383-fx-ramp-on-100x100-start-10-20.aep",
"389-fx-exposure-default.aep",
"390-fx-exposure-channels-individual.aep",
"391-fx-exposure-master-1.5.aep",
"393-fx-exposure-offset-0.1.aep",
"394-fx-exposure-gamma-2.2.aep",
"395-fx-exposure-individual-red-1.aep",
"396-fx-exposure-bypass-on.aep",
"397-fx-exposure-master-2key.aep",
"403-matte-L-alpha-M.aep",
"404-matte-L-alphainv-M.aep",
"405-matte-L-luma-M.aep",
"406-matte-L-lumainv-M.aep",
"407-matte-M-alpha-L-below.aep",
"408-matte-L-alpha-M-visible.aep",
"509-marker-comp-two.aep",
"510-marker-layer-0.52-dur0.3.aep",
"451-shape-ellipse-400x300-fill.aep",
"457-shape-group-scale-50-80.aep",
"462-shape-layer-anchor-10-20.aep",
"464-shape-stroke-opacity-50.aep",
"467-shape-trim-end-70.aep",
"469-shape-trim-type-2.aep",
"471-shape-rect-roundness-30.aep",
"473-shape-star-points-6.aep",
"482-shape-two-groups.aep",
"483-shape-nested-group-rect.aep",
"519-shape-all-items-default.aep",
"410-fx-invert-channel-2.aep",
"411-fx-invert-blend-50.aep",
"413-fx-fastbox-radius-20.aep",
"414-fx-fastbox-iterations-1.aep",
"416-fx-fastbox-repeatedge-off.aep",
"418-fx-gaussian-dimensions-3.aep",
"419-fx-gaussian-repeatedge-off.aep",
"422-fx-dropshadow-opacity-204.aep",
"426-fx-dropshadow-shadowonly-on.aep",
"427-fx-glow-default.aep",
"429-fx-glow-threshold-76.5.aep",
"438-fx-glow-midpoint-0.25.aep",
"444-fx-huesat-hue-90.aep",
"446-fx-huesat-light-m30.aep",
"448-fx-huesat-colorizehue-120.aep",
"512-fx-levels-inblack-0.1.aep",
"514-fx-levels-gamma-2.aep",
"516-fx-levels-outwhite-0.9.aep",
"517-text-box-600x400-wrap.aep",
"526-text-point-HHHH-default.aep",
"527-text-point-HHHH-baseline-normal.aep",
"528-text-point-HHHH-baseline-subscript.aep",
"485-layer-3d-on.aep",
"486-layer-3d-rotx-30.aep",
"487-layer-3d-roty-45.aep",
"488-layer-3d-orientation-10-20-30.aep",
"529-fx-timewarp-default.aep",
"530-fx-timewarp-speed-25.aep",
"531-fx-timewarp-speed-keyed-50-200.aep",
"532-fx-timewarp-method-1.aep",
"533-fx-timewarp-sourceframe-12.aep",
"534-fx-slider-expression-time10.aep",
"535-camera-position-on-3d-rotx.aep",
"536-camera-one-node-position-on-3d-rotx.aep",
"537-camera-default-on-3d-rotx.aep",
"538-text-point-HHHH-normal-ref.aep",
"539-text-hscale-0.9.aep",
"540-text-vscale-1.5.aep",
"541-text-baselineshift-41.aep",
"542-text-autokern-none.aep",
"543-text-autokern-optical.aep",
"544-text-applystroke-on.aep",
"545-text-strokewidth-5.aep",
"546-text-applyfill-off.aep",
"548-text-linejoin-round.aep",
"550-text-strokeoverfill-off.aep",
"552-text-nobreak-on.aep",
"553-text-tsume-0.5.aep",
"554-text-kerning-50.aep",
"555-text-ligature-on.aep",
"556-text-autohyphenate-off.aep",
"558-text-hangingroman-on.aep",
"559-text-everyline-on.aep",
"560-text-firstlineindent-20.aep",
"561-text-startindent-30.aep",
"562-text-endindent-40.aep",
"563-text-spacebefore-10.aep",
"564-text-spaceafter-12.aep",
"565-text-justify-fullleft.aep",
"566-text-justify-fullcenter.aep",
"567-text-justify-fullright.aep",
"568-text-justify-fullall.aep",
"569-text-direction-rtl.aep",
"571-text-digitset-arabic.aep",
"572-text-leadingtype-japanese.aep",
"573-text-baselinedir-rotated.aep",
"575-text-vertical-rtl.aep",
"576-text-baselinedir-crossstream.aep",
"578-text-digitset-hindi.aep",
"595-text-stroke-scale-shift-combined.aep",
"596-text-combined-fill-over-stroke.aep",
"579-textanim-position-0-m50.aep",
"580-textanim-start-25.aep",
"581-textanim-end-75.aep",
"582-textanim-offset-10.aep",
"583-textanim-units-index.aep",
"584-textanim-basedon-3-words.aep",
"585-textanim-mode-2-subtract.aep",
"586-textanim-amount-50.aep",
"587-textanim-shape-2-rampup.aep",
"588-textanim-smoothness-50.aep",
"589-textanim-easehigh-30.aep",
"590-textanim-easelow-40.aep",
"591-textanim-randomize-seed-7.aep",
"592-textanim-many-props.aep",
"593-textanim-start-keyed-ease-0-100.aep",
"594-textanim-position-start-keyed-ease.aep",
"725-text-point-HHHH-normal-ref-S18.aep",
"727-text-reset-paragraph.aep",
"728-textanim-character-offset-3.aep",
"711-fx-directional-blur-30-40-on-M.aep",
"712-fx-eleven-effects-defaults-on-M.aep",
"713-fx-radial-blur-zoom-20-on-M.aep",
"714-fx-ccradial-type2-30-60-on-M.aep",
"715-fx-venetian-40-30-50-5-on-M.aep",
"716-fx-channel-blur-5-10-15-20-edge-horiz-on-M.aep",
"717-fx-blackwhite-set-tint-on-L.aep",
"718-fx-tritone-set-blend25-on-L.aep",
"719-fx-lumakey-darker-140-20-1-2.5-on-M.aep",
"720-fx-simplechoker-3.5-on-M.aep",
"721-fx-simplechoker-3.5-view-matte-on-M.aep",
"722-fx-mattechoker-set-on-M.aep",
"723-fx-transform-set-on-M.aep",
"597-key-anchor-ease-out50-40-in30-60.aep",
"598-key-ramp-point-ease-color-bezier.aep",
"599-key-mask-path-ease-infl75.aep",
"700-key-mask-path-ease-out20-in50.aep",
"701-light-parallel-int80-color.aep",
"702-light-ambient-int50-color.aep",
"703-key-orientation-0-to-10-20-30-linear.aep",
"704-light-environment-default.aep",
"705-mask-on-text-rect-40-m70-160-10.aep",
"706-mask-on-shape-rect-0-m150-100-150.aep",
"707-shape-merge-subtract-rect-ellipse.aep",
"708-shape-merge-intersect-rect-ellipse.aep",
"709-shape-repeater-3-pos150-60-rot10-scale90-80-op40.aep",
"710-shape-offset-paths-20-round.aep",
"730-key-anchor-ease-on-400x800-solid.aep",
"731-shape-merge-offset-repeater-defaults.aep",
"732-text-box-byteedit-para24-0.aep",
"733-text-box-byteedit-para35-0.aep",
"734-text-box-byteedit-style68-1.aep",
"828-shape-offset-paths-20-copies3-copyoffset1.5-stroke.aep",
"851-shape-key-trim-end-0-100-linear.aep",
"852-shape-key-trim-end-bezier-out0-60-in20-30.aep",
"853-shape-key-group-position-0-200-100-linear.aep",
"854-shape-key-fill-color-red-blue-linear.aep",
"855-shape-key-trim-offset-0-90-hold.aep",
"856-shape-trim-end-expression-time50.aep",
"857-shape-key-group-scale-100-50-150-linear.aep",
"900-mask-added-no-path.aep",
"901-mask-no-path-expansion-m100.aep",
"902-settings-rec709-16bpc-steps-png-grey-solid.aep",
"905-fx-24-effects-set-one-comp.aep",
"906-blendmodes-13-solids-on-steps.aep",
"963-curves-red-mid-down-on-S.aep",
"964-curves-rgb-s-curve-on-S.aep",
"951-styles-all-nine-defaults-on-B.aep",
"966-style-drop-shadow-opacity50-distance20.aep",
"970-style-stroke-size10-color-green.aep",
"974-style-gradient-overlay-angle45-scale150.aep",
"975-style-fill-opacity0-with-drop-shadow.aep",
"976-style-drop-shadow-opacity-keyed-100-20-linear.aep",
"977-styles-all-params-changed.aep",
"979-style-inner-glow-source-2.aep",
"981-layer-stretch-minus100-start5.aep",
"982-layer-stretch-minus100-opacity-keys.aep",
"983-layer-stretch-minus50-opacity-keys.aep",
"985-meshwarp-3x3-vertex-moved-on-S.aep",
"986-shape-fill-switched-off-green.aep",
"988-style-drop-shadow-switched-off.aep",
];
use effectcraft_aep::{AepFile, Comp, Layer, LayerType, Project, parse};
/// Parses a fixture; the bytes must outlive the file, so the caller keeps them.
pub fn open(bytes: &[u8]) -> AepFile<'_> {
parse(bytes).expect("fixture parses")
}
/// The first composition of a project (comp `C` in the corpus; in 124 the precomp `Pre` comes later).
pub fn first_comp<'f, 'a>(project: &Project<'f, 'a>) -> Comp<'f, 'a> {
project.all_items().into_iter().find_map(|i| i.comp()).expect("a composition")
}
/// The solid `L` of the base composition: the bottom layer (the first one made; id 0x1b in the
/// single-case files, another id in each case of a combined file).
pub fn base_layer<'f, 'a>(comp: &Comp<'f, 'a>) -> Layer<'f, 'a> {
comp.layers().last().expect("a layer")
}
/// The layer with this id in a composition.
pub fn layer_with_id<'f, 'a>(comp: &Comp<'f, 'a>, id: u32) -> Layer<'f, 'a> {
comp.layers().find(|l| l.id() == Some(id)).unwrap_or_else(|| panic!("no layer with id {id:#x}"))
}
/// The layer with this user-set name in a composition.
pub fn layer_named<'f, 'a>(comp: &Comp<'f, 'a>, name: &str) -> Layer<'f, 'a> {
comp.layers().find(|l| l.name().as_deref() == Some(name)).unwrap_or_else(|| panic!("no layer named {name}"))
}
/// The layer that shows this name: its own name, or its source's while it has none.
pub fn layer_shown_as<'f, 'a>(project: &Project<'f, 'a>, comp: &Comp<'f, 'a>, name: &str) -> Layer<'f, 'a> {
comp.layers().find(|l| project.layer_display_name(l).as_deref() == Some(name)).unwrap_or_else(|| panic!("no layer shown as {name}"))
}
/// The first layer of this type in a composition.
pub fn layer_of_type<'f, 'a>(comp: &Comp<'f, 'a>, layer_type: LayerType) -> Layer<'f, 'a> {
comp.layers().find(|l| l.header().is_some_and(|h| h.layer_type == layer_type)).unwrap_or_else(|| panic!("no {layer_type:?} layer"))
}
/// A copy of a layer's `LIST Layr` with its id (`ldta` bytes 0-3) set to 0, for comparing the
/// same layer in two cases of a combined file.
pub fn without_layer_id(layer: &effectcraft_aep::Chunk<'_>) -> effectcraft_aep::Chunk<'static> {
let mut c = layer.clone().into_owned();
if let effectcraft_aep::Body::List { children, .. } = &mut c.body
&& let Some(ldta) = children.iter_mut().find(|x| x.is(b"ldta"))
&& let effectcraft_aep::Body::Data(d) = &mut ldta.body
{
d.to_mut()[..4].fill(0);
}
c
}
/// Compares floats with a tolerance.
pub fn near(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
pub fn assert_near(got: &[f64], want: &[f64], tol: f64) {
assert_eq!(got.len(), want.len(), "{got:?} vs {want:?}");
for (g, w) in got.iter().zip(want) {
assert!(near(*g, *w, tol), "{got:?} != {want:?}");
}
}