Files
effectcraft/crates/aep
ca1ff9cfc2 .aep import: map the Cryptomatte plug-in to EffectCraft's Cryptomatte (#3109)
* AEP import: map the Cryptomatte plug-in to EffectCraft's Cryptomatte

The importer reported every instance of the free Cryptomatte plug-in
(match name `Cryptomatte`) as a lost effect. It now becomes
`ec.3d.cryptomatte`:

- CryptomatteData decodes the plug-in's Settings data (its own
  little-endian layout, documented in docs/aep-format.md 3.7): the
  Cryptomatte layer name and the selection text, both carried verbatim.
- Output maps to Display / Matte Only (Colors -> Colors, Matted RGBA ->
  Matte, Matte Only -> Matte Only). Matted Colors, Selection Mode when on,
  keys or expressions and anything else stored are reported, and
  unreadable data is reported as damaged.
- EffectCraft's Cryptomatte gains a Layer Name text parameter. A layer the
  footage has under exactly that name wins; otherwise the Layer popup
  decides as before. The CPU, GPU and pick paths share one resolver
  (AuxChannels::resolve_crypto_layer_named). Without it, plug-in layer
  names such as Crypto_Material would silently fall back to the first
  layer.

On a real production project this takes the effect losses from 132 to 44.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* AEP import: check Cryptomatte against an After Effects 2026 sample of our own

A sample made in After Effects 2026 (plug-in 2.6.6: a fresh effect,
Selection Mode on, each Output item, with read-backs and renders)
corrected three points of the mapping:

- Selection Mode on was dropped silently: After Effects writes it as a
  default-state record (tdsb 3) holding 1, which the general reader does
  not take. The Cryptomatte step reads that record itself and reports it.
- No Output record is now the plug-in's default Colors, also for an
  instance that borrows the project's definition (every instance after
  the first), which was reported as having no reliable default and fell
  back to EffectCraft's Preview. Keyed or unreadable Output values fall
  back to Colors too, still reported.
- Matted Colors imports as Display Matte (the same matte over the layer's
  own colours) instead of Preview, still reported as an approximation.

docs/aep-format.md 3.7 gets the sampled Output default, the tdsb 3
Selection Mode record and the real fresh Settings data (an empty text of
length 1 and its MurmurHash3, twice, not cry1 and zeros); docs/aep-import.md
the render comparison with After Effects (Matted RGBA matches; Matte Only
differs in alpha; Colors with no layer chosen shows another layer).
Tests: the exact fresh Settings bytes, tdsb 3 Selection Mode, borrowed
definitions, and an opt-in test against the sample once it is in the
corpus (aep/cryptomatte.aep, ignored until the corpus pin includes it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* AEP import: write Cryptomatte's popup items in the test as After Effects does

The injected Cryptomatte definition wrote `pdnm` and `fnam` as `LIST`
chunks. After Effects writes them as plain chunks whose body is chunks
(`NESTED_IDS`), and the reader only looks there, so the tests never read
Output's item names: they passed through the fallback for a definition
with four items and no `pdnm`, which uses the plug-in's own list.

The harness now writes both as After Effects does. New tests check that
the injected definition reads back with its items and instance name,
that Output is mapped by the item the stored index names (items in
another order, case, an unknown item), and that the fallback without a
`pdnm` still works. With the old `LIST` form, or with the mapping
ignoring the items, the new tests fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* AEP import: check Cryptomatte's layer and selection against presets of our own

The After Effects 2026 sample 1400 now has two preset compositions that choose a layer and
pick one object (box_red on CryptoObject) or one material (mat_paint on CryptoMaterial).
CryptomatteData reads both as they are; the picked tokens are the EXR manifest's hashes,
which EffectCraft's selection grammar reads the same as the names.

- The u32 at 108 is MurmurHash3 of the selection text with its NUL (three texts).
- A unit test pins the 127-byte preset data; the sample test now covers all eight
  compositions with their layer and selection.
- Render check: EffectCraft's matte covers exactly the pixels After Effects draws as
  selected, edge coverage within 0.0002. docs/aep-format.md 3.7 and docs/aep-import.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cryptomatte import: report damaged Settings and Selection Mode records

Review follow-ups for #3109:
- CryptomatteData refuses a selection text without its NUL and checks the zero byte
  at 112 for an empty text too.
- A Settings record without its data, and a Selection Mode record whose value cannot
  be read, are reported as damaged data, not taken as the default.
- Layer Name is the effect's last parameter, so the earlier parameters keep their
  indexes and upgraded instances have the same order as new ones.
- A Cryptomatte layer name is matched as given before it is trimmed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cryptomatte settings: require the selection's NUL to end its declared length

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cryptomatte: match rank planes by the exact layer name

crypto_rank_planes looked planes up with a case-insensitive whole-name match, so
layers Crypto_A and crypto_a shared planes. The layer prefix now matches
case-sensitively; component letters still match in either case.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Format

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* AEP: the exporter knows Extra::Cryptomatte; rustfmt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-06 19:36:42 +01:00
..

effectcraft-aep

After Effects projects (.aep, .aepx) in one crate, the way effectcraft-lottie handles Lottie:

  • container: reads and writes the container as a lossless chunk tree, with a typed, read-only view (below; its API is re-exported at the crate root);
  • import: maps the typed view into an EffectCraft project and reports everything it does not import (docs/aep-import.md);
  • export: writes an EffectCraft project as an .aep with every chunk built in code, and reports everything it does not export (docs/aep-export.md).

No file access: callers pass bytes, so the crate builds for wasm32.

Container

An .aep is a RIFX container (big-endian RIFF) with form type Egg!: chunks of a 4-byte id, a big-endian u32 size and a body padded to an even length; LIST chunks start with a 4-byte list type followed by chunks. parse builds the tree, write serialises it, and for every input that parses, write(parse(x)) == x byte for byte (container and chunk sizes are recomputed from the tree; pad bytes, odd bodies and the bytes after the container, an XMP packet, are kept).

The XML form of a project (.aepx) is read into the same tree by read_aepx (a small bounded XML tokenizer, no dependency); parse_any picks the reader by content, so the typed view and the importer take either form. The mapping is in docs/aep-format.md, section 1.

Bodies that look like lists but are not chunk sequences (for example LIST btdk, raw text) stay opaque rather than failing the parse. Input is untrusted: sizes are checked against the remaining input, nesting and chunk count are bounded (Limits), nothing is allocated from a declared size, and truncated or damaged files return Err (or, inside the container, an opaque body).

The container layer decodes no chunk body. On top of it, a typed view (Project, Item, Comp, Layer, Property, ...) reads the fields whose layout was verified against the values the test files were made with: layer and composition headers (switches, exact times and time stretch, size, frame rate, work area, shutter phase, display start, renderer), static transform values in After Effects' units with the "absent = default" rule, keyframe records (interpolation sides, ease per component, plain and spatial templates), Time Remapping, expressions with their enable switch, effect instances and parameter values, masks and bezier paths with tangents, shape layer groups, text documents (paragraph and style runs, box, justification, tracking, faux styles, caps, leading), markers with duration, strings and cue data, cameras and lights with their options, footage (stills, audio, image sequences), solids and precomp links. Every accessor returns Option/an empty list on damaged data and never panics. Which fields are verified, which are not decoded, and how it was found is in docs/aep-format.md.

Specifications

Public specifications used for parts the format embeds:

  • ICC.1:2010: the profile description of a working-space profile (a v2 desc or a v4 mluc tag), from which the working space's name is read (container/settings.rs).
  • RFC 4648 (base64): the profile data in the project's colour settings JSON (read, and written by the exporter).
  • The exporter's own colour profiles (export/icc.rs): ICC.1:2010 version 4.3 display profiles with ITU-R BT.709 primaries, D65 white, a D50 PCS and a Bradford adaptation, the IEC 61966-2-1 sRGB curve or a 2.4 power curve; the profile ID is the MD5 of RFC 1321.

Clean room

Everything here comes from our own observations: project files that we generated ourselves with After Effects, one change per file, compared byte by byte. No source code or format documentation of another .aep reader or writer was used, and no Adobe asset is used. The test fixtures are such generated files (own test data): a few combined projects, one composition per case (tests/fixtures/CASES.md; the generator is in tests/fixtures/tools/). They are not in this repository but in storytold/effectcraft-corpus, with their attribution, the generation notes and the inspection of their embedded content. cargo xtask aep-corpus fetches them at the commit pinned in tests/fixtures/corpus.lock, checks every file against the SHA-256 listed there and only then installs them in corpus/aep (ignored by git). The tests read them from the folder named by the EFFECTCRAFT_AEP_FIXTURES environment variable (opt-in; set EFFECTCRAFT_AEP_FIXTURES_REQUIRED=1 too, so that a missing project fails). The tests reach them only through tests/it/common/mod.rs, which hands a test its case as a project of its own cut from the combined file; a test whose fixture is missing (or with the variable not set) prints SKIPPED: fixture <name> missing and passes.

tests/fixtures/synthetic.aep (115 KB) is the one project in the repository: made in After Effects 2026 with the working space set to None (one composition, an image sequence of three 8x8 PNGs, a solid, a folder, a Levels and a Glow effect with expressions). Every After Effects project embeds the working space's ICC profile (HP sRGB IEC61966-2.1 for the default) in a JSON settings chunk; the one Utf8 chunk of this file that held a profile was replaced by {}, and nothing else was changed. The tests in tests/it/import/synthetic.rs damage copies of it and need no other file.

Tools

  • tests/fixtures/tools/: the generator of the test projects (regenerates the corpus with After Effects; its README says how).
  • examples/aepdump.rs: prints the chunk tree of a file (ids, sizes, short data, match names): cargo run -p effectcraft-aep --example aepdump -- file.aep.
  • examples/stubicc.rs: replaces every embedded ICC profile of a saved project by a stub that keeps only the profile's description: the JSON colorProfileData of the working space (any white space around the colon, \/ escapes, quotes as &quot;) in an .aep chunk or the .aepx text, and a raw profile of an imported image (an .aep chunk, or the bdata hex of an .aepx): cargo run -p effectcraft-aep --example stubicc -- in.aep out.aep. A profile it cannot replace stops it before anything is written. The importer reads the working space's name from the description only, so the project imports the same; no vendor colour data stays in the test project. Check the result independently, not by the tool's count (the corpus repository has tools/aep/inspect-embedded.py, which scans the raw bytes, base64 and hex).