mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-13 13:52:16 +00:00
* aep: a layer's markers and Source Text are read where they are stored, not searched for (#3276) A layer's marker list is a direct child of its root tdgp, and its Source Text record btds a direct child of the ADBE Text Properties group. Markers walked the whole layer subtree twice per layer (most layers have no marker list, so the walk always ran to the end), and the three text-document accessors walked it once each. * aep export: build the prototype template once, sort names with cached keys (#3277) The template every export clones from is immutable; it is now built by the first export of the process and kept (about 0.93 MB). The name sort builds each natural key once instead of on every comparison. Root solids move into the Solids folder instead of being copied. The two key-list rewrites share one emit-local helper and match names compare as bytes instead of a decoded String. Output is byte-identical. * aep container: size sibling lists, hex and write output once; borrow plain .aepx attributes (#3275) - read.rs: count the chunk headers of a sequence first and allocate the children Vec once (bounded by the input and by the chunk limit). - aepx.rs: decode_hex sizes its output once; attribute values without an entity are borrowed instead of copied through decode_entities. - typed.rs: PropGroup::get compares match names without building a String (same result as the lossy compare, invalid UTF-8 included). - write.rs: reserve the exact output size up front (try_reserve, so a hand-built tree cannot abort on the allocation). - read.rs, settings.rs, values.rs: use the shared bytes.rs readers. Output is identical on the corpus (tree, written bytes, import). * aep container: review fixes (#3275) - read.rs: reserve with try_reserve_exact, so a raised chunk limit on a small target cannot abort before parsing; the capped reservation and a deep, retry-heavy nest of broken lists are pinned by tests. - write.rs: a tree past the 32-bit container size fails with TooLarge before anything is serialised. - aepx.rs: drop an unneeded local and lifetime annotation. * aep export: note what the concurrency test covers and why a failed template build is not kept (#3277) * aep: text documents are read without a String per token, defaults built once (#3276) TextDocument::parse reads a borrowed tree: names, bare words and strings without escapes point into the body, keys are borrowed when they are /names, numbers parse from the borrowed bytes, and dictionaries and arrays are moved into vectors of their exact size when they close. The style and paragraph defaults are built once instead of per document. parse_text_dict keeps its public TextValue result (the borrowed tree turned owned). The old owning parser stays as a test-only oracle: a differential test compares both on 40 000 generated bodies (every key form the owned keys canonicalise, non-UTF-8 names, escapes, unclosed strings, non-finite numbers), every cut of a realistic document and nesting around the depth limit. * aep: text-dict test builds its prefix without repeat(1) (#3276) * aep: an escaped text string drops its byte order mark without a range drain (#3276) * aep export: one key-time loop, key-list rewrite, tdmn decoder and byte writer for the listed modules (#3278) In masks, shapes, styles, synth, fxsynth, base, camlight, skeleton, textanim and tree (effects.rs, plan.rs and emit.rs keep their copies for the follow-up work): - New export/keys.rs: the key-time loop (timed_keys), the key-list rewrite (fill_key_list, set_key_list, replace_cdat) and the spatial key record, shared by masks, shapes and styles. - One tdmn decoder (synth::tdmn_name, lossy UTF-8 up to the first NUL, as the importer reads it) and one encoder (synth::tdmn) instead of the copies in masks, styles, shapes, textanim, camlight and skeleton. - One byte writer: tree::put, which errors. base::bytes, synth::tdb4, synth::key_list, fxsynth::pard, masks::shph and the keyed path records write through it; their fixed layouts keep the infallible signatures behind tree::fixed, which logs, and a test builds every layout and requires no error. - camlight::put_group uses the shared group split (tree::split_group), which errors on a match name without a node instead of stopping quietly. Export output is byte-identical on the corpus and the synthetic fixtures. * aep export: pin the key-time loop's report, name the layout in the base::bytes log (#3278) * aep export: review fixes for #3278 - tree::Fixed replaces tree::fixed: a fixed layout writes every field that fits (as the old code did) and keeps the first error; finish() logs it once with the layout's description and counts it per thread, so the every-layout test cannot race with other tests. - base::bytes logs one line naming its length and fields. - masks::build_atom errors when the prototype has no Mask Shape instead of dropping the path of every mask quietly. - log sorted into the dependency list. * aep: review fixes for #3276 text documents and markers text_document reads the first btdk in the Source Text record, the documents text_document_count counts. The old slot checks (same, is_neutral, undecoded_fields) join the test oracle and are compared on 20 000 generated slot dictionaries; mixed dictionary/array chains and a document left open at the depth limit join the parser differential. An escaped string's byte order mark is drained without a copy. * aep: remove test-only container accessors, share report and time-base code (#3281) - Remove 35 public container accessors that only tests called (header, camera/light, keys, footage, masks, shapes, version) and the dead StyleMap::switch_name. Tests now check the importer's own reading of the same bytes, or the decoder it uses; three import tests gain the values the removed checks covered. - Import and export reports share one loss entry (Loss<C>) with record, total, count and summary; the JSON shape is pinned for both. - Collector::lose_with builds a detail only below the detail cap (callers switch later). - The export uses the container's TimeBase; one NTSC table serves import and export; TimeBase::frames saturates like units instead of returning 0. * aep: review fixes for #3281: NTSC table beside TimeBase, real-path NTSC round trip, light and camera defaults on import, doc wording * aep: drop a local check tool committed by mistake (#3281) * aep export: sort keys as one compact byte string, bounded by the name (#3277) The cached natural keys were Vecs of (bool, String, u128) segments: a 255-byte name of alternating letters and digits took about 14 KB, held for every item at once. Each key is now one byte string whose byte order is the natural order, at most 5 bytes per name byte. Order and output are unchanged. * aep container: check bdata digits and size before reserving (#3275) decode_hex reserved the decoded size before it knew the value decodes, so a large bdata with a bad digit reserved half its length first. It now refuses a value larger than a chunk's 32-bit size field (TooLarge), checks every digit in one vectorisable pass, and only then reserves, with try_reserve_exact so a value that does not fit in memory is an error instead of an abort. * aep export: the every-layout test counts from a baseline, not from zero (#3278) * aep export: correct the order the natural sort key doc gives for equal numbers (#3277) * aep: the format notes name the accessors that remain (#3281) * aep: the format notes name the 1 x 1 mask scale of text and shape layers (#3281) * aep: read imports through a flat tree index (#3300) Import reads an .aep through TreeIndex, one depth-first array of chunks that borrows the file's bytes, instead of building and freeing the owned Chunk tree (one Vec per list). The parser mirrors read::Parser step for step: the same chunk count order against max_chunks, the same depth limit and opaque fallbacks, the same errors. The typed view reads through Node, a handle to a chunk of either tree, so the accessors work unchanged on both; .aepx keeps its owned tree. Chunk, parse and write stay for export and editing. The array starts small and doubles, never past what the input can hold or the chunk limit allows; a failed allocation is TooLarge, a tree past u32 indices TooManyChunks. * aep export: the time remap and Transform keys use the shared key-list and match-name helpers; doc comments for the audit's functions (#3300) * #3300: import .aepx through the flat index The .aepx reader no longer builds the owned chunk tree element by element. It decodes every body (hex, text, alias JSON, numbers) into one buffer and records the chunks as depth-first entries whose bodies are ranges of that buffer; TreeIndex::from_drafts turns them into the index the importer reads, which borrows the buffer. read_aepx converts that index into the owned tree in one pass. The tree and the errors are those of the previous reader, which the tests keep as the reference: generated and damaged documents under seven limit sets, invalid UTF-8, hostile input, the fixture directory, and imports through both. Bound tests pin the chunk list to the input and the limit, and check that dropped content leaves nothing behind. * #3300: read_aepx keeps its own owned-tree reader read_aepx, read_aepx_with_limits and parse_any build the owned chunk tree element by element again, as before the .aepx index, instead of converting the index (which held the decoded buffer and the owned tree at the same time). Import keeps the index reader. The tests now compare the two production readers. * #3300: test keys that round to the same time * aep: compare the destination projects in the index-equivalence tests both() and both_aepx() kept only the ImportResult of each path; they now keep both destination Projects and assert structural and serialized equality, so a layer or property value regression cannot pass on equal ids and reports. --------- Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
346 lines
14 KiB
Rust
346 lines
14 KiB
Rust
//! The layer header (`ldta`) and the composition header (`cdta`, `PRin`), checked against the values
|
|
//! the synthetic corpus was made with (its MANIFEST): comp `C` 1920x1080, 25 fps, 5 s, background
|
|
//! black, shutter angle 180, one solid layer `L` (red, 1920x1080, label 1, Normal, Best, start 0,
|
|
//! in 0, out 5, stretch 100), then one change per file.
|
|
|
|
use crate::container::helpers::{base_layer, first_comp, open};
|
|
use effectcraft_aep::{BlendMode, LayerHeader, LayerType, Project, Quality, Rational};
|
|
|
|
const L: u32 = 0x1b;
|
|
|
|
/// The header of the solid layer `L` in a fixture.
|
|
fn l_header(name: &str) -> Option<LayerHeader> {
|
|
Some({
|
|
let bytes = crate::common::try_fixture(name)?;
|
|
let file = open(&bytes);
|
|
let project = Project::new(&file).unwrap();
|
|
let comp = first_comp(&project);
|
|
base_layer(&comp).header().unwrap()
|
|
})
|
|
}
|
|
|
|
/// The switches that the MANIFEST lists, so that a case can say which one differs from the base.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
struct Switches {
|
|
shy: bool,
|
|
locked: bool,
|
|
solo: bool,
|
|
three_d: bool,
|
|
motion_blur: bool,
|
|
collapse: bool,
|
|
adjustment: bool,
|
|
guide: bool,
|
|
}
|
|
|
|
fn switches(h: &LayerHeader) -> Switches {
|
|
Switches {
|
|
shy: h.is_shy(),
|
|
locked: h.is_locked(),
|
|
solo: h.is_solo(),
|
|
three_d: h.is_three_d(),
|
|
motion_blur: h.is_motion_blur(),
|
|
collapse: h.collapses_transformations(),
|
|
adjustment: h.is_adjustment(),
|
|
guide: h.is_guide(),
|
|
}
|
|
}
|
|
|
|
const NONE: Switches =
|
|
Switches { shy: false, locked: false, solo: false, three_d: false, motion_blur: false, collapse: false, adjustment: false, guide: false };
|
|
|
|
/// Everything the base layer and every one-switch case share.
|
|
fn assert_plain(h: &LayerHeader) {
|
|
// The id is the case's own (0x1b in the single-case files); the source is the solid `L`.
|
|
assert_eq!(h.layer_type, LayerType::Av);
|
|
assert_eq!(h.label, 1);
|
|
// No name was set, so the copy of the name at byte 64 is empty (the layer shows its source name).
|
|
assert_eq!(h.name_copy, "");
|
|
assert!(!h.is_null_object());
|
|
assert_eq!(h.parent_id, None);
|
|
assert_eq!(h.source_id, Some(0x1a));
|
|
}
|
|
|
|
#[test]
|
|
fn base_layer_header() {
|
|
let h = skip!(l_header("000-base.aep"));
|
|
assert_plain(&h);
|
|
assert_eq!(h.quality, Quality::Best);
|
|
assert_eq!(h.blend_mode, BlendMode::Normal);
|
|
assert_eq!((h.stretch, h.stretch_den), (1, 1));
|
|
assert_eq!(switches(&h), NONE);
|
|
// start 0 s, in 0 s, out 5 s; denominator = the composition timescale.
|
|
assert_eq!(h.start, Rational { num: 0, den: 25600 });
|
|
assert_eq!(h.in_relative, Rational { num: 0, den: 25600 });
|
|
assert_eq!(h.out_relative, Rational { num: 128_000, den: 25600 });
|
|
assert_eq!(h.in_point(), Rational::new(0, 1));
|
|
assert_eq!(h.out_point(), Rational::new(5, 1));
|
|
}
|
|
|
|
#[test]
|
|
fn one_switch_per_case() {
|
|
// (file, the switches that case turns on)
|
|
let cases = [
|
|
("012-layer-shy.aep", Switches { shy: true, ..NONE }),
|
|
("013-layer-locked.aep", Switches { locked: true, ..NONE }),
|
|
("014-layer-solo.aep", Switches { solo: true, ..NONE }),
|
|
("015-layer-3d.aep", Switches { three_d: true, ..NONE }),
|
|
("017-layer-motionblur.aep", Switches { motion_blur: true, ..NONE }),
|
|
("024-layer-collapse.aep", Switches { collapse: true, ..NONE }),
|
|
("025-layer-adjustment.aep", Switches { adjustment: true, ..NONE }),
|
|
("026-layer-guide.aep", Switches { guide: true, ..NONE }),
|
|
];
|
|
for (name, expected) in cases {
|
|
let h = skip!(l_header(name));
|
|
assert_eq!(switches(&h), expected, "{name}");
|
|
assert_plain(&h);
|
|
assert_eq!((h.quality, h.blend_mode, h.stretch), (Quality::Best, BlendMode::Normal, 1), "{name}");
|
|
assert_eq!(h.out_point(), Rational::new(5, 1), "{name}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn name_blend_mode_and_quality() {
|
|
let h = skip!(l_header("010-layer-name.aep"));
|
|
assert_eq!(h.name_copy, "Renamed");
|
|
let bytes = fixture!("010-layer-name.aep");
|
|
let file = open(&bytes);
|
|
let project = Project::new(&file).unwrap();
|
|
let layer = base_layer(&first_comp(&project));
|
|
// The layer name is the user's; the source footage keeps its own name.
|
|
assert_eq!(layer.name().as_deref(), Some("Renamed"));
|
|
assert_eq!(project.layer_source(&layer).and_then(|i| i.display_name()).as_deref(), Some("L"));
|
|
|
|
assert_eq!(skip!(l_header("016-layer-blend-multiply.aep")).blend_mode, BlendMode::Multiply);
|
|
assert_eq!(skip!(l_header("023-layer-quality-draft.aep")).quality, Quality::Draft);
|
|
}
|
|
|
|
#[test]
|
|
fn display_name_falls_back_to_the_source_name() {
|
|
let bytes = fixture!("000-base.aep");
|
|
let file = open(&bytes);
|
|
let project = Project::new(&file).unwrap();
|
|
let layer = base_layer(&first_comp(&project));
|
|
assert_eq!(layer.name(), None);
|
|
assert_eq!(project.layer_source(&layer).and_then(|i| i.display_name()).as_deref(), Some("L"));
|
|
assert_eq!(project.layer_source_size(&layer), Some((1920, 1080)));
|
|
let source = project.layer_source(&layer).unwrap();
|
|
assert_eq!(source.id(), Some(0x1a));
|
|
assert!(source.footage().unwrap().is_solid);
|
|
}
|
|
|
|
#[test]
|
|
fn time_stretch_and_the_in_out_start_convention() {
|
|
// 018: stretch 100 -> 200: the stored out point stays 5 s, the layer ends at 10 s.
|
|
let h = skip!(l_header("018-layer-stretch200.aep"));
|
|
assert_eq!(h.stretch, 2);
|
|
assert_eq!(h.stretch_ratio(), Rational::new(2, 1));
|
|
assert_eq!(h.out_relative, Rational { num: 128_000, den: 25600 });
|
|
assert_eq!(h.out_point(), Rational::new(10, 1));
|
|
assert_eq!(h.in_point(), Rational::new(0, 1));
|
|
|
|
// 019: inPoint 0 -> 1; AE moved the out point to 6 s and left the start at 0.
|
|
let h = skip!(l_header("019-layer-inpoint1.aep"));
|
|
assert_eq!(h.start.num, 0);
|
|
assert_eq!(h.in_relative, Rational { num: 25600, den: 25600 });
|
|
assert_eq!(h.in_point(), Rational::new(1, 1));
|
|
assert_eq!(h.out_point(), Rational::new(6, 1));
|
|
|
|
// 021: startTime 0 -> 0.5: in 0.5 s and out 5.5 s although the stored in point stays 0.
|
|
let h = skip!(l_header("021-layer-starttime0.5.aep"));
|
|
assert_eq!(h.start, Rational { num: 12800, den: 25600 });
|
|
assert_eq!(h.in_relative.num, 0);
|
|
assert_eq!(h.in_point(), Rational::new(1, 2));
|
|
assert_eq!(h.out_point(), Rational::new(11, 2));
|
|
}
|
|
|
|
#[test]
|
|
fn parent_link_and_null_object() {
|
|
// 022: a new null layer "Null" (source "Null 1", 100x100) on top; L is its child.
|
|
let bytes = fixture!("022-layer-parent-null.aep");
|
|
let file = open(&bytes);
|
|
let project = Project::new(&file).unwrap();
|
|
let comp = first_comp(&project);
|
|
let layers: Vec<_> = comp.layers().collect();
|
|
assert_eq!(layers.len(), 2);
|
|
let null = &layers[0];
|
|
let nh = null.header().unwrap();
|
|
assert_eq!(null.name().as_deref(), Some("Null"));
|
|
assert!(nh.is_null_object());
|
|
assert_eq!(nh.layer_type, LayerType::Av);
|
|
assert_eq!(nh.parent_id, None);
|
|
assert_eq!(project.layer_source(null).and_then(|i| i.display_name()).as_deref(), Some("Null 1"));
|
|
assert_eq!(project.layer_source_size(null), Some((100, 100)));
|
|
let lh = layers[1].header().unwrap();
|
|
assert_ne!(lh.id, nh.id);
|
|
assert_eq!(lh.parent_id, Some(nh.id));
|
|
assert!(!lh.is_null_object());
|
|
}
|
|
|
|
#[test]
|
|
fn layer_header_is_none_when_ldta_is_too_short() {
|
|
assert!(LayerHeader::parse(&[0; 139]).is_none());
|
|
assert!(LayerHeader::parse(&[]).is_none());
|
|
assert!(LayerHeader::parse(&[0; 140]).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn rational_is_exact() {
|
|
assert_eq!(Rational::new(12800, 25600), Some(Rational { num: 1, den: 2 }));
|
|
assert_eq!(Rational::new(-3, -6), Some(Rational { num: 1, den: 2 }));
|
|
assert_eq!(Rational::new(3, -6), Some(Rational { num: -1, den: 2 }));
|
|
assert_eq!(Rational::new(0, 7), Some(Rational { num: 0, den: 1 }));
|
|
assert_eq!(Rational::new(1, 0), None);
|
|
assert_eq!(Rational { num: 1, den: 0 }.to_f64(), None);
|
|
assert_eq!(Rational::new(i64::MIN, 1), None);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------- composition
|
|
|
|
use effectcraft_aep::CompHeader;
|
|
|
|
fn comp_header(name: &str) -> Option<(CompHeader, Option<String>, Option<(String, String)>)> {
|
|
Some({
|
|
let bytes = crate::common::try_fixture(name)?;
|
|
let file = open(&bytes);
|
|
let project = Project::new(&file).unwrap();
|
|
let comp = first_comp(&project);
|
|
(comp.header().unwrap(), comp.name(), comp.renderer().map(|r| (r.match_name, r.display_name)))
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn base_comp_header() {
|
|
let (h, name, renderer) = skip!(comp_header("000-base.aep"));
|
|
assert_eq!(name.as_deref(), Some("C"));
|
|
assert_eq!((h.width, h.height), (1920, 1080));
|
|
assert_eq!((h.time_base.frame_duration, h.time_base.timescale), (1024, 25600));
|
|
assert_eq!(h.frame_rate(), Some(25.0));
|
|
assert_eq!(h.frame_rate_fixed, 25 << 16);
|
|
assert_eq!(h.duration_units, 128_000);
|
|
assert_eq!(h.duration_seconds(), Some(5.0));
|
|
assert_eq!(h.background, [0, 0, 0]);
|
|
assert_eq!(h.shutter_angle, 180);
|
|
assert_eq!(h.work_area_start_units, 0);
|
|
assert_eq!(h.work_area_end_units, None);
|
|
assert_eq!(h.pixel_aspect, Rational { num: 1, den: 1 });
|
|
assert!(!h.motion_blur);
|
|
// The manifest calls the classic renderer `ADBE Advanced 3d`; the file stores `ADBE Escher`
|
|
// and the display name `Classic 3D`.
|
|
assert_eq!(renderer, Some(("ADBE Escher".to_string(), "Classic 3D".to_string())));
|
|
}
|
|
|
|
#[test]
|
|
fn one_comp_change_per_case() {
|
|
let (base, _, _) = skip!(comp_header("000-base.aep"));
|
|
|
|
let (h, _, _) = skip!(comp_header("030-comp-size-1280x720.aep"));
|
|
assert_eq!((h.width, h.height), (1280, 720));
|
|
assert_eq!(CompHeader { width: 1920, height: 1080, ..h }, base);
|
|
|
|
// 031: frameRate 25 -> 30: the frame duration stays 1024, the timescale becomes 30720; the
|
|
// duration stays 5 s (153 600 units).
|
|
let (h, _, _) = skip!(comp_header("031-comp-fps30.aep"));
|
|
assert_eq!((h.time_base.frame_duration, h.time_base.timescale), (1024, 30720));
|
|
assert_eq!(h.frame_rate(), Some(30.0));
|
|
assert_eq!(h.frame_rate_fixed, 30 << 16);
|
|
assert_eq!(h.duration_units, 153_600);
|
|
assert_eq!(h.duration_seconds(), Some(5.0));
|
|
|
|
// 032: duration 5 -> 10 s; the work area stays the whole composition.
|
|
let (h, _, _) = skip!(comp_header("032-comp-duration10.aep"));
|
|
assert_eq!(h.duration_units, 256_000);
|
|
assert_eq!(h.duration_seconds(), Some(10.0));
|
|
assert_eq!(h.work_area_end_units, None);
|
|
|
|
// 033: bgColor [0,0,0] -> [0,1,0].
|
|
let (h, _, _) = skip!(comp_header("033-comp-bg-green.aep"));
|
|
assert_eq!(h.background, [0, 255, 0]);
|
|
|
|
// 034: shutterAngle 180 -> 90.
|
|
let (h, _, _) = skip!(comp_header("034-comp-shutter90.aep"));
|
|
assert_eq!(h.shutter_angle, 90);
|
|
assert_eq!(CompHeader { shutter_angle: 180, ..h }, base);
|
|
|
|
// 035: workAreaStart 0 -> 1, workAreaDuration 5 -> 2: the end is start + duration = 3 s.
|
|
let (h, _, _) = skip!(comp_header("035-comp-workarea-1-2.aep"));
|
|
assert_eq!(h.work_area_start_units, 25600);
|
|
assert_eq!(h.work_area_end_units, Some(76_800));
|
|
}
|
|
|
|
#[test]
|
|
fn one_comp_change_per_case_aspect_blur_renderer() {
|
|
let (base, _, base_renderer) = skip!(comp_header("000-base.aep"));
|
|
|
|
// 037: pixelAspect 1 -> 2.
|
|
let (h, _, _) = skip!(comp_header("037-comp-pixelaspect2.aep"));
|
|
assert_eq!(h.pixel_aspect, Rational { num: 2, den: 1 });
|
|
|
|
// 038: comp motion blur on.
|
|
let (h, _, _) = skip!(comp_header("038-comp-motionblur.aep"));
|
|
assert!(h.motion_blur);
|
|
assert_eq!(CompHeader { motion_blur: false, ..h }, base);
|
|
|
|
// 039: renderer -> Advanced 3D.
|
|
let (h, _, renderer) = skip!(comp_header("039-comp-renderer-advanced3d.aep"));
|
|
assert_eq!(renderer, Some(("ADBE Calder".to_string(), "Advanced 3D".to_string())));
|
|
assert_ne!(renderer, base_renderer);
|
|
assert_eq!(h, base);
|
|
}
|
|
|
|
#[test]
|
|
fn comp_header_is_none_when_cdta_is_too_short() {
|
|
assert!(CompHeader::parse(&[0; 175]).is_none());
|
|
assert!(CompHeader::parse(&[0; 176]).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn precomp_item_ids_and_labels() {
|
|
// 124: L precomposed into "Pre"; the layer in C is a comp-sourced layer labelled 15.
|
|
let bytes = fixture!("124-precomp-Pre.aep");
|
|
let file = open(&bytes);
|
|
let project = Project::new(&file).unwrap();
|
|
let comps: Vec<_> = project.all_items().into_iter().filter_map(|i| i.comp().map(|c| (i, c))).collect();
|
|
let names: Vec<_> = comps.iter().map(|(_, c)| c.name().unwrap()).collect();
|
|
assert_eq!(names, ["C", "Pre"]);
|
|
let (c_item, c) = &comps[0];
|
|
let (pre_item, pre) = &comps[1];
|
|
let layers: Vec<_> = c.layers().collect();
|
|
assert_eq!(layers.len(), 1);
|
|
let layer = &layers[0];
|
|
let h = layer.header().unwrap();
|
|
assert_eq!(h.source_id, pre_item.id());
|
|
assert_eq!(project.layer_source(layer).unwrap().id(), pre_item.id());
|
|
assert_eq!(h.label, 15);
|
|
assert_eq!(layer.name(), None);
|
|
assert_eq!(project.layer_source(layer).and_then(|i| i.display_name()).as_deref(), Some("Pre"));
|
|
assert_eq!(project.layer_source_size(layer), Some((1920, 1080)));
|
|
// The precomp layer's start time has the denominator 1; the Pre comp is 1920x1080, 25 fps, 5 s.
|
|
assert_eq!(h.start.den, 1);
|
|
let ph = pre.header().unwrap();
|
|
assert_eq!((ph.width, ph.height, ph.frame_rate(), ph.duration_seconds()), (1920, 1080, Some(25.0), Some(5.0)));
|
|
// C records that it uses the comp Pre (chunk idpi, a little-endian u32).
|
|
assert_eq!(crate::common::used_item_ids(c_item), [pre_item.id().unwrap()]);
|
|
assert!(crate::common::used_item_ids(pre_item).is_empty());
|
|
assert_eq!(pre_item.label(), Some(15));
|
|
// The layer inside Pre is a new layer made from the solid L (moving the layers into a precomp
|
|
// creates new layers).
|
|
let inner: Vec<_> = pre.layers().collect();
|
|
assert_eq!(inner.len(), 1);
|
|
let ih = inner[0].header().unwrap();
|
|
assert_ne!(ih.id, L);
|
|
assert_eq!(ih.source_id, Some(0x1a));
|
|
assert_eq!(project.layer_source(&inner[0]).and_then(|i| i.display_name()).as_deref(), Some("L"));
|
|
}
|
|
|
|
#[test]
|
|
fn a_negative_stretch_is_a_reversed_layer() {
|
|
// 981: Time Reverse (stretch -100 %, start 5 s): numerator -1 (`0xffffffff`), the stored in
|
|
// point 0 and out point 5 s relative, so the layer runs from 5 s back to 0 s; 983: -50 % = -1/2.
|
|
let h = skip!(l_header("981-layer-stretch-minus100-start5.aep"));
|
|
assert_eq!((h.stretch, h.stretch_den), (-1, 1));
|
|
assert_eq!((h.in_point(), h.out_point()), (Rational::new(5, 1), Rational::new(0, 1)));
|
|
let h = skip!(l_header("983-layer-stretch-minus50-opacity-keys.aep"));
|
|
assert_eq!((h.stretch, h.stretch_den), (-1, 2));
|
|
assert_eq!((h.in_point(), h.out_point()), (Rational::new(5, 1), Rational::new(5, 2)));
|
|
}
|