mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 20:41:25 +00:00
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.
414 lines
16 KiB
Rust
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:?}");
|
|
}
|
|
}
|