mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-09 03:49:12 +00:00
Merge upstream/main into aep-import-thirdparty
#3119 (the base of this branch) is merged. Conflicts with the Cryptomatte import (#3109) and the version report: Extra keeps Extractor, Identifier and Cryptomatte, arbitrary.rs keeps ExtractorChannels / identifier_channel_type next to CryptomatteData, the effect table counts 66, the docs keep both sides. The exporter from #2594 gets the Extractor and Identifier arms: like Lumetri they have no prototype, so CC Color Neutralizer, EXtractoR and IDentifier are reported, not written (new test imported_plug_in_effects_are_reported_not_written). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
# After Effects project export
|
||||
|
||||
> **Last reviewed:** 2026-10-19 · **Last updated:** 2026-11-05 · **Change:** a 3D layer's changed Material Options are reported, shape and text animator colours outside 0 to 1 are clamped and reported, and a popup expression is no longer reported where the option order is the same (2026-11-05); before that: composition settings the base writes at their defaults (Preserve Frame Rate / Resolution, motion blur samples, adaptive sample limit) and render queue items are reported, and a sequence's folder time is read only when its file passes the document-path policy (2026-11-04); before that: every chunk is built in code, no template is read (2026-10-29); before that: Curves and Mesh Warp are written (R53); before that: a switched-off shape item or layer style is written switched off (R52, 986-988); before that: a reversed layer (negative stretch) is written as After Effects stores it (R36, 981-983); before that: an explicit None (`NO_LAYER`) on Compound Blur, Displacement Map or Set Matte is written as None, and Layer Control on a named layer no longer reports a source choice; before that: layer styles are written (R51, 950-980; read back in After Effects: `export-tests\styles`); before that: an image sequence's folder modification time is written when the caller knows it (`sspc@118`, R50: without it After Effects rescans the folder on open and warns about every gap of a sparse sequence in a modal dialog); before that: an output space equal to the working space is not reported (After Effects renders in the working space, 902; the importer sets it so for Rec.709 Gamma 2.4); before that: one effect table for both directions again: the 27 effects of the importer round 3 and the Layer Control are written too (prototypes from 903 and 076, layer parameters by layer id; not read back in After Effects yet); before that: the importer round 3 is merged: Offset Paths' Copy Offset is written as EffectCraft's value + 1 (826-828, the importer's inverse); before that: sparse sequences checked (860, written as After Effects stores them); before that: the Rec.709 Gamma 2.4 working space is written with a profile authored here (R49, g5); before that: text animators with range selectors are written (R48, t8); before that: a footage layer running past its source is ended there on import, as After Effects does, and less than a frame past is not a loss (LayerTiming); before that: Anchor Point and mask path ease are written (R47); before that: shape operators (polystar, gradients, trim paths, round corners, merge / offset paths, repeater) and keys and expressions on shape properties are written (R46, o2); before that: every effect of the importer's table is written, through one shared table and records built per parameter type (R45, e2); before that: time remapping is written (319, 354, r1, R44); before that: the character and paragraph settings are read back in After Effects (t6) and right-to-left justification is written relative to the direction (t7, R43); before that: the character and paragraph settings the importer decodes are written (scales, baseline shift, kerning, ligatures, tsume, fill / stroke switches and width, justified variants, indents, spacing, composer, direction); before that: track mattes, markers, cameras, lights and shape layers (R39-R42), keys on stretched layers, conformed clips, tga / psd / ai / eps, project settings (620-634, R37), superscript / subscript (526-528); before: precomposition layers (124, 257, 258, 600-602), the effects Tint, Gradient Ramp and Exposure (356-397, 610), footage (351, 353, 603-607, 611-613), text layers (110, 245, t1-t4), the 3D switch (015) and time stretch (018, 316, 317) are exported · **Target:** Adobe After Effects 2026
|
||||
|
||||
File ▸ Export ▸ After Effects Project… (`file.exportAep`) writes the open project as an `.aep` that
|
||||
After Effects 2026 opens. It is deliberately narrow: what it writes is exact, tested and checked
|
||||
against After Effects itself; everything else is **reported** and never dropped silently. The
|
||||
importer ([aep-import.md](aep-import.md)) is the mirror image; both rest on the clean-room format
|
||||
notes in [aep-format.md](aep-format.md).
|
||||
|
||||
## Design: every chunk is built in code
|
||||
|
||||
The exporter reads no file After Effects wrote. Every chunk comes from code, in the layout of the
|
||||
chunks After Effects writes for the same content ([aep-format.md](aep-format.md) section 3):
|
||||
|
||||
- the project skeleton (`export/base.rs`): the top-level headers and settings, the render queue,
|
||||
the composition shell with its eleven view layers (empty property trees), the `Solids` folder,
|
||||
solid and null items;
|
||||
- the layer blocks (`export/skeleton.rs`, `export/tree_defs.rs`): AV, null, text, camera, light and
|
||||
shape layers as tables of their property groups and records, with only the groups the exporter
|
||||
writes into (After Effects adds the others);
|
||||
- the property records (`export/synth.rs`): the `tdb4` header from its decoded fields, the value or
|
||||
key list; effects from the effect table (`export/fx_defs.rs`, `export/fxsynth.rs`), layer styles
|
||||
from theirs (`export/style_defs.rs`), masks, markers and text animators from their layouts;
|
||||
- footage items per kind (`export/footage.rs`), naming our own colour profiles (`export/icc.rs`);
|
||||
- the text document (`export/textproto.rs`): the smallest dictionary After Effects reads back
|
||||
unchanged.
|
||||
|
||||
Some bytes After Effects needs have no decoded meaning. For those, the exporter writes the value
|
||||
the byte has in every After Effects file generated for the test cases, as a named constant in the
|
||||
code. Every other byte that is not decoded is zero. Each such choice was made by
|
||||
After Effects reading variants back (a field zeroed or a chunk left out, compared by After Effects'
|
||||
own project export and by renders), and the whole export was checked against the earlier
|
||||
template-based export case by case: no difference. The file ends with a neutral XMP packet written
|
||||
by the crate (After Effects accepts a file without one too).
|
||||
|
||||
```text
|
||||
EffectCraft Project --plan--> ProjectPlan (file units, losses reported)
|
||||
--emit--> chunk tree built in code --effectcraft_aep::write--> bytes
|
||||
```
|
||||
|
||||
Where it lives: the `export` module of `crates/aep` (`effectcraft_aep::export`, **L3**, next to the importer; no file
|
||||
access of its own, builds for wasm32). The command wrapper is in `crates/engine/src/cmd/aep.rs`.
|
||||
API: `export(&Project, &ExportOptions) -> Result<ExportResult { bytes, report }, AepExportError>` and
|
||||
the dry run `analyze(&Project) -> ExportReport`. A refused or failed export writes nothing.
|
||||
|
||||
## The command
|
||||
|
||||
`file.exportAep {path, allowLosses?, dryRun?}`
|
||||
|
||||
- Without `allowLosses` an export that would lose anything is **refused** (nothing is written) and
|
||||
the error names the categories, e.g. `exporting would lose effects: 3, text layers: 1; allow
|
||||
losses to export anyway`. With `allowLosses: true` it writes the file and returns the same report.
|
||||
- `dryRun: true` returns only the report. The result is `{path, bytes, report}`. The path is
|
||||
confined like every other file-writing command (`crates/automation/src/confine.rs`). From the
|
||||
shell: `effectcraft-cli exec file.exportAep '{"path":"out.aep","allowLosses":true}' in.ecproj --json`.
|
||||
|
||||
## What is exported
|
||||
|
||||
| Area | Exported |
|
||||
|---|---|
|
||||
| Project settings | colour depth (8 / 16 / 32 bpc), working space None, sRGB or Rec.709 Gamma 2.4 (R49), working gamma 2.2 / 2.4, Linearize Working Space, Blend Colors Using 1.0 Gamma, time display (timecode, frames, feet + frames 35 / 16 mm), frame count start 0 / 1 (R37) |
|
||||
| Project tree | Folders (nesting; empty ones too), compositions, solids (colour, size), footage items, item labels and names. Siblings are written in the order After Effects stores them (natural, case-insensitive name order) |
|
||||
| Composition | size, exact frame rate (the time base After Effects picks for the rate), duration (whole frames), work area (whole frames), background (8 bit), pixel aspect (exact fraction), shutter angle and phase, display start, composition motion blur switch |
|
||||
| Layers | solid, adjustment, null, precomposition, footage and text layers: name, in/out/start (exact units of the composition's time base), time stretch, enabled/audio/solo/shy/locked/effects/motion blur/collapse/guide/3D switches, draft quality, label, blend mode (Normal, Add, Multiply, Screen, Overlay, Difference and the 24 codes of 906 / 907, see `blend_modes` below), parent |
|
||||
| Transform | Anchor Point, Position, Scale, Rotation, Opacity: the value when it differs from the default; keyframes with time, linear/bezier/hold on each side, temporal ease (speed and influence, per dimension for Scale; an Anchor Point's speed in pixels per second over the source height, R47), spatial tangents of Position and Anchor Point (automatic ones are written as After Effects computes them), roving and key labels |
|
||||
| Expressions | on those five properties: source text and enable switch |
|
||||
| Masks | on solid, adjustment and null layers, in stack order: name, mode (all seven), inverted, path (vertices and tangents in layer pixels, open or closed), feather, opacity, expansion (each only when not at the default), path keys (time, linear, bezier or hold sides with their ease, label) with one shape per key, Mask Feather keys (time, interpolation, value, ease per dimension, label) |
|
||||
| Precompositions | a composition shown as a layer of another: the layer's source is the composition item, the using composition lists each composition it shows once (`idpi`); the anchor is a fraction of the composition's size; precompositions nest to any depth, a loop is reported |
|
||||
| Text | point and box text: the text (paragraphs, CR / LF / CRLF as After Effects' CR), per paragraph the justification (left, right, centre and the four justified variants, codes 3-6), first-line / start / end indent, space before / after, hanging punctuation, composer and direction (paragraph slots `/1`-`/5`, `/21`, `/29`, `/33`), per style run the font (PostScript name), size, fill and stroke colour, tracking, automatic or explicit leading, faux bold and italic, all caps and small caps, horizontal / vertical scale, baseline shift, automatic kerning (metrics, optical, none), ligatures, tsume, whether fill and stroke are drawn, stroke width and stroke over fill (style slots `/6`, `/7`, `/9`, `/11`, `/18`, `/36`, `/56`-`/58`, `/63`, as the importer decodes them, 538-569; written only where they differ from the prototype; read back in After Effects, R43), the justification of a right-to-left paragraph relative to its direction (R43), superscript and subscript (style slot `/13`: normal text is written 0, 527; the prototypes and After Effects' scripted default hold 1 = superscript, drawn at 58.3 % size, 526); the box; the anchor in pixels; the name only when it is not the text's first line; text animators: Anchor Point, Position, Scale, Skew, Skew Axis, Rotation, Opacity, Fill / Stroke Color, Stroke Width, Tracking, Line Spacing, Blur, Character Offset with keys and expressions, range selectors (Start / End / Offset in percent or index units, Units, Based On, Mode, Amount, Shape, Smoothness, Ease High / Low, Randomize Order, Random Seed; R48) |
|
||||
| Footage | png, jpeg, OpenEXR, TIFF, Targa, (flat) Photoshop, Illustrator and EPS stills, png, OpenEXR and jpeg sequences, mp4 and mov clips (also conformed to another rate, R38), WAV audio: the path (a sequence's folder and numbering), size, duration, native frame rate, the name (none stored when it is the file's, as After Effects does); After Effects re-reads everything else from the file when it opens the project |
|
||||
| Effects | every effect of the table the importer reads (`effectcraft_aep::effect_map`, one table for both directions): Tint, Gradient Ramp, Exposure, Invert, Fast Box Blur, Gaussian Blur, Drop Shadow, Timewarp, Glow, Fill, Hue/Saturation (master and colorize), Levels (RGB), Directional Blur, Radial Blur, CC Radial Blur, Venetian Blinds, Channel Blur, Black & White, Tritone, Luma Key, Simple Choker, Matte Choker, Transform, Brightness & Contrast, Turbulent Displace, Fractal Noise, Beam, CC Radial Fast Blur, Noise, Levels (Individual Controls), Corner Pin, Grid, Sharpen, Linear Color Key, Radial Wipe, Remove Color Matting, CC Bend It, CC Power Pin, 4-Color Gradient, Change Color, Extract, Noise HLS, Scatter, CC Light Sweep, Compound Blur, Displacement Map, Set Matte, Gaussian Blur (Legacy), Pixel Motion Blur and the Slider, Angle, Checkbox, Color, Point and Layer expression controls: the instance name, every parameter that differs from its default (colours, points, fixed, integer and float sliders, angles, popups by item name, checkboxes, layer parameters: the `tdpi` of their always-present record holds the owning layer's id by default, 0 for None (EffectCraft's unset Layer Control, or the explicit `NO_LAYER` of the other effects), or the named layer's id, 320 / 321 / 904; Layer Control's companion source choice is not reported (After Effects has none for it); the 27 effects of batch 6 (903-905) and the Layer Control are written from prototypes but not yet read back in After Effects), expressions on parameters, Hue/Saturation's master values and Levels' RGB values (arbitrary data), keys on colours, points and fixed / float sliders (time, linear / bezier / hold, the ease of sliders and points, labels), in stack order, several instances of a type per layer and per project (R24-R27, R45) |
|
||||
| Layer styles | the nine styles and Blending Options (Fill Opacity, channel switches, Blend Interior, Blend Ranges; the composition's Global Light on every styled layer) on solid, footage, precomposition, text and shape layers: a record for every parameter that differs from After Effects' default, keys on scalars, expressions (R51) |
|
||||
| Track mattes | alpha, alpha inverted, luma, luma inverted, with any exported layer of the composition as the matte (R39) |
|
||||
| Markers | composition and layer markers: time, duration, comment, chapter, URL, frame target, cue point (event or navigation, name, parameters), label (R40) |
|
||||
| Cameras | one- and two-node cameras: point of interest, position, zoom, depth of field, focus distance, aperture, blur level (R41) |
|
||||
| Lights | spot and point lights: point of interest (spot), position, intensity, colour, cone angle and feather (R41) |
|
||||
| Shape layers | groups (nested), rectangle (size, position, roundness), ellipse (size, position), polystar (type, points, position, rotation, radii, roundness) and freeform paths, fill (rule, colour, opacity), stroke (colour, opacity, width, line cap and join, miter limit), gradient fill and stroke (type, start, end, highlight; the default colour stops), trim paths (start, end, offset, mode), round corners, merge paths (mode), offset paths (amount, join, miter limit, copies, copy offset: After Effects' Copy Offset is EffectCraft's + 1, 826-828), repeater (copies, offset, composite, its transform), the group transform (anchor, position, scale, skew, skew axis, rotation, opacity), keys and expressions on all of them but popups (R46), item names (R42) |
|
||||
| Time remapping | an enabled Time Remap on a precomposition or footage layer: keys (time in layer time, value in source seconds, linear / bezier / hold, ease, labels), the expression; a remap without keys as one key holding its value; such a layer may run past its source's end (R44) |
|
||||
| Null layers | a null layer gets the hidden 100 x 100 white solid every null object has (`Null <id>` in the `Solids` folder) |
|
||||
|
||||
Units are the importer's inverse: Anchor Point is stored as a fraction of the source size, Scale and
|
||||
Opacity as fractions (ease speeds are scaled the same way), a property at its default has no record
|
||||
at all.
|
||||
|
||||
## What is reported
|
||||
|
||||
`ExportReport` (`serde`, JSON) has `exported` (counts), `losses` (per category: exact `count`, up to
|
||||
20 `details`) and `limitations` (what the exporter cannot write at all, listed in every report).
|
||||
|
||||
| Category | Counted |
|
||||
|---|---|
|
||||
| `effects` | an effect outside the table (each instance), a parameter without an After Effects counterpart that carries keys or an expression or a value other than a new instance's (a default that depends on the layer size is not compared), the effect's label colour, a colour parameter's opacity (After Effects' have none; Drop Shadow uses it), a fraction on an integer parameter (written whole), Curves points outside 0..1 (written at the limit), keys on a popup, checkbox, angle or integer slider (the first key's value is written), keys or expressions on Hue/Saturation's or Levels' data values, a fractional Hue/Saturation master value (written whole), a popup item After Effects does not have, keys or an expression on a layer parameter (its value is written), a layer parameter naming a layer that is not exported (the owning layer is written) or whose layer is taken with its masks and effects (After Effects takes the source) |
|
||||
| `markers` | a protected marker region (the switch is not decoded; the marker is written), a marker time out of range, markers on a footage item (not written) |
|
||||
| `masks` | masks on a text or shape layer (the path unit there is not verified; not written), a disabled mask or one without a usable path (not written), and per written mask: keys on Mask Opacity or Expansion (no keyed prototype; the static value is written), expressions on mask properties, the lock, mask motion blur other than Same As Layer, Linear feather falloff, RotoBezier, variable-width feather points |
|
||||
| `text_layers` | per text layer what its document cannot hold: wiggly and expression selectors, animator properties outside the list above (3D rotations, hue / saturation, Character Value, ...), a Tracking Type or Character Range / Alignment other than the default, Path and More Options other than the defaults, Source Text keys (the first key's text is written) and expressions, a manual kerning amount, OpenType features, variable font axes, a font style other than Regular, vertical text, a switched-off range selector (written on), a run without a font name (written in Times New Roman), a fill or stroke opacity (text colours have none), and a value beyond what the document holds (written at the limit): colour channels outside 0..1, font size, tracking, leading, scale, baseline shift, tsume, stroke width, paragraph indents and spacing, box position and size |
|
||||
| `shape_layers` | shape items outside the list above (wiggle, zig zag, pucker, twist, ...; not written), dashes / taper / wave, blend mode and composite order, the path direction, per-corner roundness of a rectangle (the uniform Roundness is written), gradient colour stops other than the default, keys on popups (the first key's value is written), keys and expressions on a path |
|
||||
| `cameras`, `lights` | keys or expressions on camera / light options (static value written), iris and highlight settings; parallel and ambient lights (not written), light falloff, radius, shadows; a camera's Auto-Orient Along Path or Towards Camera (written one-node), a spot light not oriented towards its point of interest (written oriented), a light colour channel outside 0..1 |
|
||||
| `model_layers` | each 3D model layer (skipped) |
|
||||
| `footage_items` | a footage item of a type without a prototype (`.avi`, `.mp3`, `.pdf`, ...), one layer of a layered file, a sequence with gaps, mixed names or no frame number, a sequence or clip at a fractional frame rate, an item without a path, a size larger than 30 000 pixels (written smaller), items in a folder loop or deeper than 64 levels, an item's proxy (not written), a solid's pixel aspect other than 1 (written as 1) |
|
||||
| `layer_timing` | an in or out point beyond the 32-bit range in a stretched layer's source time (written at the limit), a layer of a clip, sequence or sound that runs a composition frame or more past its source's end without a time remap (After Effects ends it there; the importer ends such layers at their source as After Effects does, so imported projects no longer have them) |
|
||||
| `unsupported_layers` | a layer of footage that is not written, a precomposition layer whose composition is not exported or that closes a loop, a solid layer whose solid is not exported |
|
||||
| `blend_modes` | a mode without a known code (Dissolve, Dancing Dissolve, Subtract, Divide, Lighter / Darker Color, Alpha Add, Luminescent Premul): the layer is written as Normal. The other 30 modes are written with the codes After Effects saves (906, 907; not read back in After Effects yet) |
|
||||
| `track_mattes` | a matte whose layer is not exported (written without matte) |
|
||||
| `time_stretch` | a stretch that is not an exact ratio with a denominator up to 1000 (written unstretched); bezier ease on a stretched layer is written and reported as `approximated_keyframes` (its speed unit there is not sampled) |
|
||||
| `three_d` | per-character 3D of a text layer, and a 3D layer's Material Options other than a new layer's (a changed value, a key or an expression; they are not written) (the layer's 3D switch is written) |
|
||||
| `transform_properties` | an X/Y rotation or orientation that is not at its default |
|
||||
| `approximated_keyframes` | a mask path key ease speed other than 0 (written as stored, its unit is not sampled), a bezier side of a colour key (written linear: only speed 0 / influence 0 is sampled, which After Effects interpolates linearly, 598), a roving mask or effect key, keys merged at the same time, a muted key (written as an active key), an auto-Bezier key (written with its stored ease, not the slope EffectCraft computes) |
|
||||
| `invalid_values` | NaN, infinity or a number beyond 1e9 (the default is written), a key speed or spatial tangent beyond 1e9 (written as 0), a time beyond the 32-bit range, a solid or effect colour channel outside 0..1 (written at the limit) |
|
||||
| `composition_settings` | a size outside 1 to 30 000, a duration outside 1 frame to 3 hours or that does not fit the time base, a frame rate that has no time base, a pixel aspect that is not an exact fraction, a background channel outside 0..1, a fractional shutter angle or phase (written whole), a start timecode that is negative or not a whole unit |
|
||||
| `composition_options` | guides, Essential Graphics, the Advanced 3D renderer (written as Classic 3D), and settings that change rendered frames but are written at After Effects' defaults: Preserve Frame Rate or Preserve Resolution switched on, motion blur samples per frame other than 16, an adaptive sample limit other than 128 (Draft 3D, Hide Shy, the viewer resolution, the poster time and the composition's frame blending switch are a listed limitation) |
|
||||
| `parent_links` | a parent that was not exported or that closes a loop |
|
||||
| `rounded_times` | a key, work area, layer start, in or out point time that is not a whole unit / frame of the time base, a duration that is not a whole number of frames, an in or out point that falls between two units in a stretched layer's source time |
|
||||
| `names` | a name longer than 255 bytes (cut), an item comment (not written) |
|
||||
| `project_settings` | a working space other than None, sRGB and Rec.709 Gamma 2.4 (written as sRGB: After Effects needs the profile data, `export-tests\settings\g4`, and the exporter has its own profiles for sRGB and Rec.709 Gamma 2.4 only, R49), the OCIO colour engine, HDR handling, an output space other than the working space (an equal one is After Effects' normal case: it renders in the working space, 902), a frame count start other than 0 / 1 |
|
||||
| `time_remapping` | a Time Remap on a layer After Effects cannot remap (solid, text, shape, null, camera, light: 319): not written |
|
||||
| `layer_styles` | the whole Layer Styles group switched off, an edited gradient, keys on a colour, point, popup or checkbox (the value is written), Knockout, styles on a layer that takes none |
|
||||
| `layer_switches` | per layer: Wireframe quality (written as Best), Bicubic sampling, frame blending, Preserve Transparency, Auto-Orient on a layer other than a camera or light, the environment layer setting, Lock Markers |
|
||||
| `render_queue` | each render queue item (the render queue is written empty) |
|
||||
|
||||
## Rules discovered (what After Effects needs for the file to open and read back right)
|
||||
|
||||
Evidence is the file name in the contributor's local `export-tests\` folder (not in this repository; generated by the contributor's local lab tools,
|
||||
opened in After Effects 2026 and read back through its scripting interface) or the reference
|
||||
projects made with After Effects (`ref1`, `ref-fps`, `ref-folders`, `ref-longname`). **AE re-save**
|
||||
means: the exported file was opened, saved again by After Effects and compared chunk by chunk with
|
||||
`tools/aep_diff.py`; only the noise listed under "not rules" differed, so every byte the exporter
|
||||
wrote was accepted as it is.
|
||||
|
||||
| # | Rule | Evidence |
|
||||
|---|---|---|
|
||||
| R1 | The XMP packet after the container is optional; a short neutral one is accepted. | `e0-noxmp`, `x1` |
|
||||
| R2 | Composition and layer names can be set freely (a renamed composition and layer open and read back; checked when the exporter still edited a saved project, and kept by the code-built skeleton). | `e0-rename` |
|
||||
| R3 | **Time base.** NTSC: time scale 23 976, frame duration 1000 / 800 / 400 / 200 for 23.976 / 29.97 / 59.94 / 119.88 fps. Every other rate: the frame duration is the smallest power of two for which `rate x frame duration >= 24 576` (25 fps: 1024 / 25 600; 12 fps: 2048 / 24 576; 12.5 fps: 2048 / 25 600; 1 fps: 32 768 / 32 768; 240 fps: 128 / 30 720). The 16.16 rate at `cdta@156` is the float32 of the typed rate. | `ref-fps` (13 compositions made by After Effects), `z1` (6 rates written by the exporter, read back) |
|
||||
| R4 | Composition duration is a whole number of frames (`frames x frame duration`); a 2 s composition at 29.97 fps lasts 2.002 s. | `x3`, `z1` |
|
||||
| R5 | `cdta` of a new composition: bytes 16, 24, 32 and 40 are 600, byte 48 is the time scale. **The work area is two fractions**: start `@28 / @32`, end `@36 / @40` (absolute end, not duration); once a work area is set the denominators are the time scale, while the whole composition is `0 / 600` and `0xffffffff / 600`. With 600 written as the denominator of a set work area After Effects read the start as 51.2 s. | `y1` (wrong), `y1b` (right), fixture `035` |
|
||||
| R6 | A composition item uses its own id plus the 11 ids after it for the built-in view layers (`DLay`, 6 x `SLay`, 3 x `CLay`, `SecL`, `ldta@0`); layer ids, item ids and the hidden null solids come from one counter; `head@12` is the next free id. | `x1`-`z1` |
|
||||
| R7 | **Every property's `tdb4@12` follows the composition's time scale, including those of the view layers.** | `y1`, `z1` (AE re-save identical) |
|
||||
| R8 | Items are stored in the order the project panel sorts them (natural order of the name, digit runs as numbers, case-insensitive). A composition item is followed by `FEE ` and the 7 f-records, a footage item by the 7 f-records, a folder by nothing. | `ref-folders`; AE accepted the same order in `x3`, `z1` |
|
||||
| R9 | The `Solids` folder (id 0x19, named by `sfnm/sfid`) holds root-level solids and the hidden solids of nulls; solids with a folder keep it. A root folder named `Solids` in the project **is** that folder (one folder, not two). It is written even when empty (an empty project opens). | `ref1`, `x0`, `x3`, `x7`, `z1` |
|
||||
| R10 | A **null layer** is an ordinary layer with `ldta@38` bit `0x80` whose source is a hidden 100 x 100 white solid; its Transform defaults are those of a 100 x 100 source. | `ref1`, `x3`, `z1` |
|
||||
| R11 | Transform properties exist in the file only at a non-default value; they sit in the order Anchor Point, Position, X/Y Position, Scale, Orientation, X/Y/Z Rotation, Opacity, Environment (the exporter inserts into that order). | `x4`, `x5` |
|
||||
| R12 | A keyed property is the keyed prototype with `lhd3` words 2, 3, 4, 7 and `ldat` rewritten (3, 5 and 20 keys); records are 128 bytes (Position / Anchor Point spatial, Scale plain), 48 bytes (Rotation, Opacity). | `x5`, `y2`, `y3` |
|
||||
| R13 | **Scale ease is per dimension** (slot `i` of each block belongs to dimension `i`): three different eases read back as three different eases. Upgrades the E mark of aep-format.md 3.5. | `y2` |
|
||||
| R14 | Automatic spatial tangents are written as the values After Effects computes (EffectCraft's Catmull-Rom); the flag at `@8` is 1. Roving is written as the flag; After Effects moves the key in time itself (4.525 s for the key at 4.5 s with neighbours at 3 s and 6 s). | `y2`, `z1` |
|
||||
| R15 | An expression is a `Utf8` chunk before `tdum`; `tdb4` bytes 76, 77 and 120 are 1, byte 119 is 1 when it is disabled. A property with an expression but a default value gets a static record with the default. | `x6`, `y2` |
|
||||
| R16 | Names: a layer's name is the `Utf8` child (80 characters, Japanese and umlauts read back unchanged); `ldta@64` holds the first 31 bytes. A **solid's name is in `opti@26`, up to 255 bytes** (a 140 character name saved by After Effects fills the field as far as it goes). Item names are the `Utf8` child. | `y1`, `z1`, `ref-longname` |
|
||||
| R17 | `pgui` (16 bytes in a footage item) is a GUID; the exporter writes distinct deterministic ones. | `z1` |
|
||||
| R18 | Folders clone the `Solids` folder item (`sfdt` 1); `idta@20..23` of a folder is 0 on export and set to 1 by After Effects on save (a UI state, not needed). | `x3`, `z1` re-save |
|
||||
| R19 | **Masks** (from the fixtures, After Effects read-back pending: `export-tests\masks\`). `tdgp[ADBE Mask Parade]` sits between Time Remapping and the Transform group; per mask `tdmn ADBE Mask Atom`, `mkif`, the atom. `mkif`: inverted `u8 @0`, mode `u32 @4`, the layer's mask counter `u32 @8` (1..n, 328); bytes 12..47 copied from 091. Inside the atom the order is Shape, Feather, Opacity, Offset, each property only when not at its default (090, 091). Re-exporting the imported fixtures 090, 091 and 093 gives a mask parade byte-identical to the one After Effects saved; 208, 233 and 328 differ only in the session bytes of `mkif` (and 208 in Mask Shape `tdb4@24`, R21). | fixtures 090, 091, 093, 208, 233, 328 |
|
||||
| R20 | **Path points**: `shph` byte 3 bit `0x08` = open, the box (vertices and handles) as fractions of the layer source size, points `f32` normalised inside the box (a box without width or height stores 0); `lhd3` of the point list: word 2 the record count `3n`, words 3 and 7 the vertex count `n` and `4n`, word 4 = 8 (090: 4 / 16; 233: 3 / 12). | 090, 233 |
|
||||
| R21 | **Mask Shape `tdb4@24`** is written as the source width / height, like the Anchor Point's (090, 093, 232, 233, 306, 328, 329: 16 / 9 on 1920 x 1080; 309: 1 on 100 x 100). 208 stores 1.0 on a 1920 x 1080 layer, so this is **E** until the read-back. | listed |
|
||||
| R22 | **Path keys** are 64-byte records: time, interpolation, flags (`0x0001` plus label bits) as decoded; bytes 8..63 from two records built in code with 093's layout (the first key from the first, later keys from the second). Feather keys are the plain template with two components (88 bytes, 208, 306). | 093, 208, 306 |
|
||||
| R23 | **Precomposition layers** (After Effects read-back `export-tests\precomp\p1`: all values, 39 of 39 render points identical). The layer block is the solid layer's with `ldta@40` = the composition's item id: AE's own precomposition layer in 258 (`Small`, added through the API) differs from the solid layer `L` of the same file only in the id, the name, the label and the time remapping maximum, and re-exporting 258 gives a layer subtree identical to AE's apart from the ids. The using composition lists each composition it shows **once** (602: the same composition twice gives one entry) in a little-endian `idpi` right after `idpc`, and `idpc` starts with **their number** as a little-endian `u32` (124, 257, 258, 600: 1; 601: 2; the AE re-save of p1, written with `idpc` 1, rewrote C2's `idpc` to 2). **The order of the `idpi` carries no meaning:** After Effects reverses the list on every re-save (p1: `1c 28` -> `28 1c`; p1b, written `28 1c`: -> `1c 28`, same layers); the exporter writes ascending ids, the order After Effects created in 601. The inert `ADBE Time Remapping` record's `tduM` is the source's duration in seconds (600: 3 for a 3 s composition in a 5 s one, 601: 2; 124, 257, 258: 5; 1 for the 1 s WAV of 351, 0.1 for the 3-frame sequence of 353, 0 on solids). The anchor divides by the composition's size (3.4). After Effects stores a precomposition layer's out point in the **source's** time scale (600: 92 160 / 30 720 for 3 s at 30 fps) and the precompose operation the start denominator 1 (124, 257); the exporter writes the composition's time scale for all three, as a composition added as a layer at the same rate does (258), and After Effects reads them back unchanged (p1: out 3, 1, 2). | 124, 257, 258, 351, 353, 600-602, p1 |
|
||||
| R24 | **Effects** (After Effects read-back `export-tests\effects\e1`). An instance is the default instance of its case (356 Tint, 368 Gradient Ramp, 389 Exposure: `tdmn` + `sspc` with `fnam`, `parT`, the value group and `pgui`), inserted in the layer's `ADBE Effect Parade` right before the Transform group (after the masks: 357, 398). The input-layer record `-0000` holds the owning layer's id in `tdpi` (320). A parameter that differs from its default gets the value record After Effects wrote when that parameter was changed alone (Tint 357, 359, 362; Ramp 369, 371, 373, 375, 377, 378, 380; Exposure 390, 391, 393, 394, 395, 396), inserted in index order; Exposure's Red / Green / Blue sliders share the master's `pard` (389) and take the master's record (Red Exposure from 395) with their own `tdmn` and `tdsn` (**E**). Values: colour ARGB 0..255 with alpha 255, point fractions of the layer size with `tdb4@24` = width / height (369: 16 / 9, 383: 1), fixed sliders in their stored scale, float sliders as float32, popups 1-based (also written into `pard@56`, 377, 390), checkbox 1.0. Re-exporting every effect fixture gives the same value records as After Effects wrote (`tests/effects.rs`; the layer subtrees differ only in the ids, the stale `pard@7` UI flags and the arc length of keyed records). | 356-397, 320, 398, e1 |
|
||||
| R25 | **Keyed effect parameters**: the parameter's static record with `cdat` replaced by the key list of its kind's keyed record (fixed slider 363, colour 364, point 382, float slider 397: `lhd3` words 2, 3, 4, 7 rewritten as R12) and `tdb4` bytes 5 (static bit), 68 (has keys) and 79 taken from that keyed record: static and keyed records of the same parameter differ in exactly those bytes (362 / 363 and 391 / 397: 5 and 68; 359 / 364 and 369 / 382: 5, 68, 79). Records: sliders plain n = 1 (48 bytes), colours spatial n = 4 (152 bytes, flags `0x0001`), points spatial n = 2 (104 bytes, flags `0x0007`, `u32@8` the automatic tangent switch); the arc length / colour distance `f64@16` is written 0. | 359-397, e1 |
|
||||
| R26 | **Effect instance names**: `fnam` keeps the effect's own name; a name the user gave the instance is the `tdsn` of the instance's value group, which holds the placeholder `-_0_/-` otherwise (610 against 609). The exporter wrote the name into `fnam` first; After Effects showed the effect's name (e1) and its re-save reset `fnam`. | 609, 610, e1 |
|
||||
| R27 | **Effect definitions**: the project lists every effect type in `Pefl` (one `pjef` with the match name each, sorted by name: 365, 366 store Blur before Tint whatever the stack order) and keeps one `EfDf` copy per type in `LIST EfdG` right after `Pefl` (the `EfDC` count little-endian before them; in order of first use), the copy cloned from the default case. The first instance of a type in the file keeps the full `parT`; every later one has an empty `LIST parT` and borrows the copy's definitions (383); in e1 After Effects read all parameters of both Tints. After Effects fills the copies with one instance's value records on save (e1 re-save; the importer never reads values from them, 383). | 365, 366, 383, e1 |
|
||||
| R28 | **Footage items** (After Effects read-back `export-tests\footage\f1`: all eight items found their files, names, sizes, durations and rates as written). An item is the prototype of its kind (png 607, jpeg 605, OpenEXR 604, TIFF 613, mp4 606, QuickTime mov 612, WAV 351, png sequence 353, OpenEXR sequence 603, jpeg sequence 611) with: `iide` / `idta` id and label; the name in the item's `Utf8` only when it is not the file's (After Effects stores none then, 126, 603-607; the WAV keeps it, 351); `sspc` width @30, height @34, duration `num @38 / den @42` (stills 0 over png 1, jpeg and TIFF 600, OpenEXR 100; sequences frames / rate; mp4 frames / rate, 606: 25 / 25; WAV samples / 48 000, 351), native rate @56 and @150 (353, 603, 606), the sequence numbering @172 / @176 / @180 and the two `Utf8` around the number (353, 603); png `opti` width / height @20 / @24 and the (first) file name @58 (126, 353, 607); a fresh `pgui`. Footage sits in its folder; root-level footage stays at the root (126), unlike solids. A footage layer is the solid layer block with the footage's id (126, 351, 353, 603-607 differ from `L` only in the source, the label and the out point's time base) and the source's duration as the Time Remapping maximum (351: 1, 353: 0.1). | 126, 351, 353, 603-607, f1 |
|
||||
| R29 | **The alias** (`Als2/alas`) is JSON with `fullpath` (the file; a sequence's folder with `target_is_folder` true), `ascendcount_base` 1 and `ascendcount_target` 1 (2 for a folder, as 353), `platform` 1, and empty `server_name` / `server_volume_name`: After Effects resolved all eight paths of f1 and filled the two names on its re-save (13 bytes per item). Quotes and backslashes are escaped, other characters written as UTF-8. | f1 |
|
||||
| R30 | **After Effects re-reads a footage file's metadata when it opens the project**: f1's `test8x8.png` was written with the 64 x 48 RGB png prototype (bits per pixel 24, colour type 2, another file size and time); After Effects showed and rendered the 8 x 8 RGBA file correctly, and its re-save had `sspc` @63 (32), @73, @119-121 (time), @208 (size) and `opti` @37 (colour type 6) as for 126. The exporter therefore needs the right prototype per file type and the decoded fields above, not the file's encoding details. | f1 re-save, 126 |
|
||||
| R31 | After Effects ends a layer of a clip, sequence or sound at its source's end: f1 wrote all layers 5 s long; the mp4, the sequences and the WAV read back 1, 0.1, 0.1 and 1 s long. Reported as `layer_timing`. | f1 |
|
||||
| R32 | **Text layers** (After Effects read-back `export-tests\text\t4`: seven text layers, every text, font, size, fill, justification, tracking, leading and box read back). The layer block is the point (110) or box (245) text layer's (the two differ only in their document and `btgu/pgui`; layer type `ldta@128` = 3 and source id 0: writing type 0 made After Effects show AV layers without a source that it refused to save). The document (`btdk`, 3.10) is the prototype's, parsed into a tree and written back with After Effects' spacing, with: the text `RUN/0`; one paragraph run per paragraph cloned from the prototype's (`RUN/5/0[n]/1` the characters including the CR, `.../0/0/5/0` the justification 0 / 1 / 2: 245, 246, 334); one style run per change of style cloned from the prototype's (`RUN/6/0[n]/1`; slots `/0` font index, `/1` size, `/2` / `/3` faux bold / italic, `/4` automatic leading with `/5` 0.01 or `/4` false with `/5` the leading (400, 339), `/8` tracking (247), `/12` caps (337, 338), `/13` 0 normal / 1 superscript / 2 subscript (526-528), `/53` and `/54` fill and stroke `[A R G B]`: 113, 248, 340); for box text the frame `/0/8/0[0]/0/1/0` (16 corner points, 245). Re-exporting all 18 text fixtures gives documents equal to After Effects' in every decoded field. | 110, 113, 243-248, 334-340, 385, 386, 400, t4 |
|
||||
| R33 | **After Effects lays text out again on open**: the composed layout `/1/1[0]/1/2` (lines, glyphs, advances) need not match the text. t1 (113 with `Hello` replaced by `World`, layout stale), t2 (`Hello World`, runs 6 -> 12, layout stale) and t3 (the same with the layout list emptied) all showed and rendered the new text; t3's re-save had the layout rebuilt. The exporter writes the layout empty. The per-paragraph list `/1/1[0]/1/1` (present only with several paragraphs, 243, 386) is left as the single-paragraph prototype has it (absent); t4's three-paragraph layer laid out right. | t1-t3, t4 |
|
||||
| R34 | **Fonts**: the prototype's font list (`/0/1/0`: Times New Roman, then three internal entries the rest of the document refers to by index) keeps its order; another font is appended as a copy of the first entry with its PostScript name and `/2` 0 and without the version. After Effects resolved `ArialMT` (family Arial) and, on its re-save, rebuilt the list as it would (the used font first, with its own `/2` and version), and for faux bold / italic added the synthesized first entry (`/3` 2 / 1) of 335 / 336 itself. A text layer's anchor is pixels with the origin as its default (385: `tdb4@24` = 1). | 335, 336, 385, t4 |
|
||||
| R35 | **The 3D switch** is `ldta@38` bit 0x04 (015); the layer keeps the AV layer block's inert Orientation / X / Y Rotation and Material Options records, which After Effects replaces with its 3D set on save (d1 re-save, as 015 shows). Position z is written as usual. Read back and rendered identically to EffectCraft with the default camera (`export-tests\threed\d1`: 15 of 15 points). | 015, d1 |
|
||||
| R36 | **Time stretch** is the exact ratio `ldta@8 / @108` (018: 2 / 1, 316: 1 / 2, 317: 3 / 2; a reversed layer -1 / 1 or -1 / 2 with the stored in point at the later end and the composition's time scale, 981-983); in and out points relative to the start are written in source time (absolute = start + relative x stretch), and the layer's property time scale (`tdb4@12` of every record of the layer) is the composition's x stretch for 100 % or more (018: 51 200, 317: 38 400) and the composition's below (316: 25 600). Re-exporting 316 and 317 gives layers identical to After Effects' apart from ids and the name; 018 the same stretch, times and time scales. **Keys** on a stretched layer count source time in the layer's time scale (641: 200 %, keys at comp 1 / 2 s stored as 25 600 / 51 200 over 51 200; 642: 50 %, 51 200 / 102 400 over 25 600; 643: the same from a start at 1 s), i.e. EffectCraft's layer time in the layer's units; re-exporting 641-643 gives the key records After Effects wrote. After Effects read back 200 % and 50 % precomposition layers and played them exactly as EffectCraft (`export-tests\stretch\s1`: the moving square at 4 times, both halves, same pixel spans). | 018, 316, 317, s1 |
|
||||
| R37 | **Project settings** (aep-format.md 3.12a, cases 620-634) are written into the header chunks: `nhed` bytes 15 (depth 0 / 1 / 2), 8 (frames), 11 (feet + frames), 13 (frames per foot 16 / 40), 14 (count start: 2 = 0, 1 = 1) and the same fields of `nnhd` at 24, 8, 11, 19, 20; `dwga` byte 0 (gamma 2.4); one-byte `lnrb` then `lnrp` after `cpid`; for no working space `cpid` = 16 x `0xff` and the JSON after `PwCs` `{}`. Read back exactly by After Effects (`export-tests\settings\g1`-`g3`; g1 re-save: only noise). A working space named without its profile data is refused by After Effects ("Unable to read project color settings", g4). Before, every export was 32 bpc sRGB (the template's settings), which colour-manages footage that EffectCraft (no working space by default) does not: re-exporting `footage\f1` with the settings written, After Effects now renders the jpeg and mp4 within one level of EffectCraft where round 1 measured 11-13 levels (that gap was the sRGB working space, not the decoder), the other footage unchanged; `effects\e1` moved by at most one level. | 620-634, g1-g4 |
|
||||
| R38 | **Conformed clips and four more still types.** A clip conformed to another rate keeps its native frames and rate in `sspc` @38 / @42 and @56 and stores the conformed rate at @146 and @150 (645 against 644); a conformed sequence stores the new rate as its own (@42, @56, @150; 615), which is what the exporter writes for a sequence anyway. Targa (650), flat Photoshop (651), Illustrator (652) and EPS (653) stills are their cases' items (`opti` holds no file name; the duration 0 / 600); Illustrator and EPS share the type tag `TEXT`. After Effects read back `export-tests\footage\f3` (all five files found, the clip 0.8333 s, at 0.5 s it shows the clip's frame 15 = 30 fps) and rendered tga, psd and ai identical to EffectCraft (0 / 0 at 2 times), the EPS within 51 levels at the edges of its square (rasteriser), the clip: EffectCraft shows frame 12 at 0.5 s = it plays the clip at its native 25 fps (the renderer does not apply a conformed rate; not an export problem). | 615, 644, 645, 650-653, f3 |
|
||||
| R39 | **Track mattes** (IMPORTER cases 402-408): the target layer's `ldta@107` holds the mode (1 alpha, 2 alpha inverted, 3 luma, 4 luma inverted) and `ldta@160` the matte layer's id (any layer, also one below); the matte layer's video switch is written as EffectCraft has it (After Effects 2026 does not hide it by itself). o1: luma matte rendered identically to EffectCraft. | 402-408, o1 |
|
||||
| R40 | **Markers**: the marker group of 249 (`tdmn ADBE Marker` + `LIST mrst`) is inserted as the first property of the layer's root group or of the composition's `Markers` view layer (`SecL`); one 16-byte record per marker (time in the owner's time units, bytes 4 / 5 = 3, the rest written 0 where After Effects leaves session bytes) with `lhd3` words 2, 3, 7 as for keys, and one `Nmrd` per marker (`NmHd`: cue type 4 / 5, parameter count, duration in 1/600 s, 600, label; five strings and the parameter pairs). Layer markers count layer time in the layer's units. Re-exporting 122, 123, 249, 347-349 gives the markers After Effects wrote; o1 read back every field. EffectCraft's default marker label is Red (index 1), After Effects' is None. | 122, 123, 249, 347-349, 509, 510, o1 |
|
||||
| R41 | **Cameras and lights** are the layer blocks of 256 (two-node camera), 254 (spot) and 252 (point) with the layer header fields set (type 2 / 1, source 0 / `0xffffffff`, the light type at 136, the auto-orient bit 0x40 for a two-node camera and every light, the 3D bit), the point of interest as the Anchor Point in pixels and the Position as an explicit value (the camera / light Position record has the layout of a 3D layer's, 253 = 045); every camera option is written (Zoom and Blur Level with the plain-double layout of Focus Distance, 342; After Effects read them back), light options only where they differ (254, 343-345), in After Effects' order. o1: all values read back. | 251-256, 342-345, o1 |
|
||||
| R42 | **Shape layers**: the shape layer block of 519 with its contents emptied, items cloned from 519 (all defaults, no value records) and 240 (path), value records from the one-change cases (451-471, 239, 242) only where a value differs from After Effects' default, in After Effects' order (fill rule before colour, 455; stroke colour, opacity, width, cap, join, miter, 464 / 239; group transform anchor, position, scale, skew, skew axis, rotation, opacity); colours `[A, R, G, B]` 0..255, opacities and the group scale in percent, popups 1-based, the path as a mask path with a box in layer pixels (the box of 240 to the bit). Rectangle Position takes the record layout of Ellipse Position (no case; o1 read it back). o1: every value read back, render identical to EffectCraft but for edge pixels. | 238-242, 451-471, 519, o1 |
|
||||
| R43 | **Character and paragraph slots** (After Effects read-back `export-tests\text\t6`): the scales `/6` `/7` (factors), baseline shift `/9`, automatic kerning `/11` (0 none, 1 metrics, 2 optical), ligatures `/18`, tsume `/36` (fraction), fill / stroke switches `/56` `/57`, stroke over fill `/58`, stroke width `/63`; paragraph indents and spacing `/1`-`/5`, hanging punctuation `/21`, composer `/29`, direction `/33`, justification 3-6: importing After Effects' re-save of t6 gives the same documents as importing the export, in all 15 layers; text.measure and the render agree with EffectCraft (baseline shift, indents, spacing, justified last lines, scales, stroke). EffectCraft's default composer is every-line (`/29` true, as a new text from After Effects' Paragraph panel; scripted texts store false, 538); After Effects reads it as every-line and breaks lines as EffectCraft. **In a right-to-left paragraph the stored justification is relative to the direction** (t7): 0 and 3 are laid out flush right, 1 and 4 flush left, 2, 5, 6 as in left-to-right; EffectCraft's justification is visual, so the exporter and the importer swap 0 / 1 and 3 / 4 there. | 538-569, t6, t7 |
|
||||
| R44 | **Time remapping** (After Effects read-back `export-tests\remap\r1`, `r1b`): the layer's `ADBE Time Remapping` record with `tdsb` 1, `tdb4` byte 5 bit 0 cleared and byte 68 = 1, and a key list of plain 48-byte one-component records (value in seconds at 8) in place of `cdat`; `tduM` stays the source's duration. Re-exporting 319 and 354 gives the record After Effects wrote, byte for byte. Keys count layer time in the layer's time scale like any key (a 200 % layer's key at layer 2 s reads back at comp 4 s). A record with `tdsb` 1 but a static value and no keys reads back as remapping **off** (r1 Freeze), so a remap without keys is written as one key. Read back: hold, bezier ease, labels, an expression, reverse; renders equal to EffectCraft at 9 times but for a bezier-out / linear-in segment (EffectCraft's interpolation, 9-12 px at 2.5 / 3 s). | 319, 354, r1, r1b |
|
||||
| R45 | **Effect value records by parameter type** (all effect fixtures; After Effects read-back `export-tests\effects\e2`). Over the whole synthetic corpus every static value record of a parameter type has the same `tdsb` and `tdb4` but for the time scale (`@12`) and a point's layer aspect (`@24`, with the constant `6.25e-12` at `@16`); a slider's `tdum` / `tduM` repeat its slider range from `pard` (float sliders `f32` @112 / @116, fixed sliders 16.16 and integer sliders `i32` @132 / @136; angles have none), and `tdsn` is the parameter's `pard` name (a checkbox's `pdnm` label). So one record per type (`fxkd`: integer 414, fixed 361, angle 219, checkbox 217, colour 357, point 369, popup 377, float 071) and the parameter's definition build every record: re-exporting every effect fixture rebuilds every static record of a mapped parameter byte for byte (`every_static_value_record_is_rebuilt_byte_for_byte`). **Records are named by the parameter's match name, which is not its position in `parT`** (Fill: position 2 is `ADBE Fill-0007`; Hue/Saturation's position 8 is `-0009`). An expression on an effect parameter sets only `tdb4@120` (534; bytes 76 / 77 stay 0, unlike the Transform properties). A point key's ease speed is pixels per second over the layer height (598). Default instances come from our cases at the defaults (or a one-change case with that record removed: 073, 711). After Effects' save of a project it opened stores **point defaults in percent** (50 / 50 instead of 0.5 / 0.5: e1 and e2 re-saves) and can leave **0 in a popup's default slot** (Luma Key, e2 re-save): the reader and the importer read both (`resaved_defaults.rs`). e2 (27 compositions, one per effect): every non-default value, the renamed `Speed` slider and both expressions read back; importing After Effects' re-save gives the same 54 effects as importing the export (0 differences). | 070-075, 210-221, 307, 356-397, 409-448, 511-534, 598, 711-723, e1, e2 |
|
||||
| R46 | **Shape records by property type, keys and expressions** (all shape fixtures, 851-857; After Effects read-back `export-tests\objects\o2`). Over the corpus every shape value record of a type has the same bytes but for its match name, the time scale and a slider's range in `tdum` / `tduM` (0..100 for every slider but Skew -85..85 and Highlight Length -100..100; sizes and scales -32 000..32 000): one-component sliders take the record of Fill Opacity (454), angles of Rotation (458), popups of Fill Rule (455), points of Ellipse Position (453), sizes and scales of Scale (457), colours of Fill Color (451); every static record of every shape fixture is rebuilt byte for byte (`every_shape_value_record_is_written_as_after_effects_wrote_it`; After Effects also keeps records set to their default, 239 Miter Limit 4, 332 Highlight Angle 0, and the inner radius of a polygon, 331, which the exporter leaves out: absent = default). The items not in 519 at their defaults come from 731 (merge, offset, repeater). A keyed shape property clears `tdb4` byte 5 bit 0 and sets byte 68 (points and colours also clear byte 79) and holds plain records (48 bytes one component, 88 bytes a size: 851, 852, 855, 857) or spatial ones (104 bytes a point, flags 7: 853; 152 bytes a colour, flags 1: 854); the 7 key fixtures are written as After Effects wrote them but for the arc length, automatic tangents and the unused ease before the first / after the last key. The reader takes a shape colour's keys as spatial by their record size although `tdb4` byte 7 is 0 (854). An expression sets `tdb4` 76, 77, 120 as on the Transform properties (856). | 238-242, 331, 332, 451-477, 519, 707-710, 731, 851-857, o2 |
|
||||
| R47 | **Ease the importer decodes is written back** (597, 598, 599, 700, 730): an Anchor Point stored as a fraction of its source has its spatial speed in pixels per second divided by the source height (597 on 1920 x 1080, 730 on 400 x 800; an effect point the same, 598); a mask path key holds its ease at bytes 24..55 of its 64-byte record (in speed, in influence, out speed, out influence; 599, 700). Re-exporting the five fixtures gives the key records After Effects wrote in time, interpolation and the used ease sides (`tests/ease.rs`). | 597-599, 700, 730 |
|
||||
| R48 | **Text animators** (114, 579-594, 728; After Effects read-back `export-tests\text\t8`). The layer's `ADBE Text Animators` group (after More Options) is 579's, every animator its first animator with one copy of its range selector per EffectCraft selector; an animator property is written whenever the animator has it (its entry is what Animate adds), a selector property only when it differs from the default or carries keys or an expression; every record is the one After Effects wrote for that property (579-592, 728), keyed as on shape properties (593, 594). With Units = Index the range is `Index Start / End / Offset` (583; After Effects also keeps the percent range it had, which the export leaves out), and After Effects writes Smoothness 100 explicitly with Ramp Up (587, absent = default). Re-exporting the 18 animator fixtures gives After Effects' structure and records byte for byte (but for unused ease sides and arc lengths). t8: every value read back, the re-save imports equal to the export, three of four texts render equal to EffectCraft. | 114, 579-594, 728, t8 |
|
||||
| R49 | **Rec.709 Gamma 2.4 working space** (624; After Effects read-back `export-tests\settings\g5`). After Effects keeps the working space's ICC profile in the project: `cpid` is the profile's 16-byte ID (ICC.1:2010 7.2.18, the MD5 of the profile with the flags, rendering intent and ID fields zeroed) and the `Utf8` after `PwCs` is `{"baseColorProfile":{"colorProfileData":<base64>,"colorProfileName":...},"baseProfileType":2}`. The exporter copies no profile: for this working space it writes one of its own (and one for sRGB), authored from the standards (`crates/aep/src/export/icc.rs`): ICC v4 display class, BT.709 primaries, D65 white adapted to the D50 PCS with Bradford, a pure 2.4 power curve (BT.1886 with black 0), description `Rec.709 Gamma 2.4 (EffectCraft)`. After Effects opens it with that working space and renders g5 identical (0 levels) to the same project switched to its built-in Rec.709 Gamma 2.4; the importer maps both names to Rec. 709. Other working spaces are still written as sRGB (reported) | 624, g5 |
|
||||
| R50 | **Sequence folder time** (860; After Effects opens `export-tests\sparse860\exp` x1-x3 through the startup argument): a sequence item's `sspc@118` (u32, seconds since 1904-01-01 UTC) is the modification time of the sequence's folder when After Effects last scanned it. While it equals the folder's, After Effects keeps the stored numbering; otherwise it scans the folder on open and raises a modal "missing frame" warning for every gap. The folder's time opens 860's export without the dialog; the time of the first or of the last file does not (x1, x2), the file size at @208 does not matter (b-size). `export_with` asks the caller with each sequence's first file as the project stores it and writes the answer only for sequences (`file.exportAep` follows that file through the document-path policy first, as the footage check does: for a refused path, such as one on another computer stored in an imported project, the folder is not looked at and the time stays 0; otherwise it reads the folder's metadata, nothing else); `export` and unknown folders get 0 (then the folder is scanned again, harmless for a sequence without gaps). | 860, x1-x3 |
|
||||
| R51 | **Layer styles** (950-980; `export-tests\styles`). The `ADBE Layer Styles` group of every layer gets Layer Styles, Blending Options and the applied styles' switch groups set to `tdsb` 1; a parameter whose stored value differs from After Effects' default (the 951 read-back, not EffectCraft's: Use Global Light is off there) gets the record After Effects wrote for it in 977 (the table `style_defs`; Green / Blue Channel Blend are Red's record renamed) with the value replaced, in After Effects' property order; scalar keys take 976's keyed form; expressions as on effect parameters. Re-exporting 951, 966, 970, 974, 975, 976, 977, 979 gives After Effects' group byte for byte; s1 (made here) reads back every value in After Effects, s2 (977) renders identical to the original (0 / 0). A style switched off is written with its switch group's `tdsb` 0 (988). Not written, reported: the whole Layer Styles group switched off (not sampled), edited gradients (`GCst`), keys on colours, points, popups and checkboxes, Knockout. | 950-980, s1, s2 |
|
||||
| R52 | **Shape item switch** (986, 987): bit 0 of a shape item group's `tdsb` is its eye switch (1 on; a Fill at 0 is not drawn by After Effects, 986, and drawn once set, 987, both saved by After Effects). A switched-off item is written with the bit cleared; the importer reads it into the item's `enabled` (before, a switched-off fill was drawn, e.g. under trim paths). | 986, 987 |
|
||||
| R53 | **Curves and Mesh Warp** (961-965, 985). Their arbitrary-data parameter always has a record and an `aRbs` (the prototypes of 961 / 965 keep it). Curves: EffectCraft's points become whole 0..255 points (at most 16 per curve, reported otherwise), the five lookup tables are the natural spline through them, the `i32` after the point count is -1 for an untouched curve and count - 2 otherwise (as in every sample); re-exporting 963 / 964 gives After Effects' 1644 bytes exactly. Mesh Warp: Rows / Columns / Quality as integer sliders, the mesh as rows + 1, columns + 1 (`export-tests\mesh`: only this order renders as made), the stored 12 bytes, and per vertex the position (fraction of the layer) with its default handles moved along (985) plus each handle's own move, which the importer keeps as eight extra numbers of the vertex (EffectCraft draws the mesh bilinear); 985 re-exports within 1e-5. Not written: keys on the curves or the mesh. | 961-965, 985 |
|
||||
|
||||
**Not rules (noise After Effects changes by itself on save):** `head` save counter, `nhed` /
|
||||
`nnhd` session ids, `fdta`, `ppSn`, item modification times, `idta@23` of folders,
|
||||
and the cached arc length `f64 @16` of a spatial
|
||||
key (0 as written, filled by After Effects when it saves a path with a curve).
|
||||
|
||||
## After Effects read-back
|
||||
|
||||
Each file was opened with `tools/open-in-ae.ps1` in After Effects 2026 and read back through its
|
||||
scripting interface (comp size, rate, duration, work area, layer names / flags / timing / parents,
|
||||
property values and keyframes with interpolation and ease); the last column says whether an
|
||||
AE re-save of the same file differed.
|
||||
|
||||
| File | Content | Read back | AE re-save |
|
||||
|---|---|---|---|
|
||||
| `e0-noxmp`, `e0-rename` | an export without XMP; renamed composition and layer | accepted, names read back | not run |
|
||||
| `x1` | `C` 1920x1080, 25 fps, 5 s, red solid `L` | identical to the base fixture `000` | not run |
|
||||
| `x2` | `Comp 2` 1280x720, 30 fps, 4 s, green 400x300 solid, layer `Layer X`; shutter phase -90, motion blur on | all read back | not run |
|
||||
| `x3` | folder `Folder A` with a composition and a solid, a 29.97 fps composition, a null layer | tree, parents, 29.97 fps (2.002 s), null 100x100 | not run |
|
||||
| `x4` | all five Transform values | Anchor [100,50], Position [300,200], Scale [50,60,100], Rotation 45, Opacity 25 | not run |
|
||||
| `x5` | two linear keys on all five properties | 10 keys read back with values and times | not run |
|
||||
| `x6` | bezier / hold sides with ease, 3 keys on Opacity and Position, an expression | interpolation names, influence 16.67 / 25 / 33.33 / 50 %, speed 100, expression `time*20` | not run |
|
||||
| `y1`, `y1b` | adjustment, locked / shy / solo, hidden, guide, blends, label, draft, parents, timing, bg, pixel aspect 10/11, shutter, display start, work area, 80 character and Japanese names | everything read back; `y1` exposed R5 | identical (only noise) |
|
||||
| `y2` | 5 Position keys (labels, manual tangents, roving), per-dimension Scale ease, Anchor keys, hold, disabled expression, 24 fps | all read back | only `ldat` arc lengths and the roving time |
|
||||
| `y3` | 300 layers x 20 keys, 50 fps | 300 layers | not run |
|
||||
| `x0` | empty project | opens, the `Solids` folder only | not run |
|
||||
| `x7` | no layers, an empty folder, duplicate names, a project folder named `Solids` with two solids, a one-frame composition | 8 items, one `Solids` folder with the two solids and the hidden null solid, duplicate names kept | not run |
|
||||
| `cli1` | written by `effectcraft-cli run --empty comp.new layer.newSolid layer.newNull prop.addKey ... file.exportAep` (the command path itself) | solid + null layer, Position and Rotation keys, expression `wiggle(1,20)` | not run |
|
||||
| `z1` | 6 rates, 6 compositions, folder, 100 character and umlaut solid names, nulls, keys, expressions | 6 rates, durations, 16 items | identical (only noise) |
|
||||
| `objects\o1` | shape layer (groups, rect, ellipse, path, fills, stroke, group transforms), luma track matte, layer and composition markers, two-node camera, spot and point light | every value read back (READBACK.md there); shapes render identical to EffectCraft but for edges, the lit 3D card the same geometry with EffectCraft a little brighter | camera / light transform sets rebuilt, marker session bytes filled |
|
||||
| `stretch\s2` | three solids moving with two linear Position keys each, stretched 200 %, 50 % and 150 % from 0.4 s | key times in comp time 0 / 4, 0 / 2, 1.9 / 4.9 s; renders identical to EffectCraft at 1, 2, 3, 4.5 s (max 1 level) | not run |
|
||||
| `footage\f3` | tga, psd, ai, eps stills and an mp4 conformed from 25 to 30 fps | all found; clip 0.8333 s; tga / psd / ai renders identical to EffectCraft, EPS edges within 51, the conformed clip a different frame in EffectCraft (renderer ignores the conformed rate) | not compared |
|
||||
| `settings\g1`-`g4` | project settings | g1-g3 every setting read back; g4 refused (R37) | g1: only noise |
|
||||
| `settings\g5` | Rec.709 Gamma 2.4 working space, 16 bpc: a black-to-white Gradient Ramp, a mid-grey and a red solid | the working space shown as `Rec.709 Gamma 2.4 (EffectCraft)`; render identical (0 levels) to the project switched to the built-in Rec.709 Gamma 2.4 (R49); EffectCraft's Rec. 709 renders about 7 levels darker (renderer) | import Rec. 709 |
|
||||
| `footage\860` | a sparse png sequence (files 1, 2, 4, 5 of 1-5, imported by After Effects as one sequence) | After Effects stores a sparse sequence as the folder (alias with `target_is_folder`), the prefix and extension `Utf8`s and the numbering range in `sspc` @172 / @176 (1 / 5), nothing per file; the export writes the same and opens with no missing footage; scripted (`project.open`) with no dialog, but through the startup argument (the GUI path) only with the folder's modification time (R50; before it, a "missing frame 3" warning); renders of comp S at frames 0-4 equal the original's (After Effects holds frame 2 for the missing 3); the P1 export (the round-2 dialog) now also opens with 0 missing items and its 36 ranges equal the original's. `sspc` @208 (the files' total size, 628 in 860) keeps the template's value: After Effects does not check it (b-size) | import: 5 frames, 1-5 |
|
||||
| `objects\o2` | four shape layers: star + radial gradient fill + stroke; rounded rectangle with keyed trim paths; merged (subtract) rectangles with offset paths -10 and a keyed fill colour; a repeater (5 copies by expression `3 + Math.floor(time * 2)`, rotation, end opacity) in a group with keyed position | every value, key and expression (R46); renders at 0 / 0.5 / 1 / 1.5 / 2.5 s: star and gradient and the repeater equal to EffectCraft (mean 0.14 / 0.6); EffectCraft differs (renderer) on trim paths with Start above End and the start point of a rounded rectangle, and on Offset Paths with Copy Offset 0 (After Effects draws no offset, EffectCraft shrinks the shape; EffectCraft's default Copy Offset is 0, After Effects' 1) | not run |
|
||||
| `effects\e2` | 27 compositions, one per mapped effect, on a ramp | every value and expression (R45); renders: Invert, Fast Box Blur, Drop Shadow, Hue/Saturation (both), Levels, CC Radial Blur, Channel Blur, Tritone and the five controls within 2 levels, Gaussian within 9 (edges); EffectCraft differs (renderer) on Fill (After Effects makes the layer the colour at the opacity, EffectCraft mixes it with the original), Timewarp (After Effects retimes the source without the effects above), Glow, Venetian Blinds, Directional / Radial Blur, Black & White, Luma Key, the chokers, Transform, and the `effect("Speed")("Slider")` expression (After Effects rotates 10 degrees at 1 s, EffectCraft 0) | import of the re-save = import of the export |
|
||||
| `remap\r1`, `r1b` | five remapped precomposition layers (reverse, hold + ease, no keys, expression, 200 % stretch) | every key, interpolation, ease, expression; the keyless remap read back off in r1, on in r1b (R44) | not run |
|
||||
| `text\t8` | four texts with animators (keyed reveal, words, ramped colour, index units + subtract) | every animator, property and selector; re-save import = export import (R48); renders equal but for tracking with index units (renderer) | import equal |
|
||||
| `styles\s1`, `styles\s2-977` | s1: a solid with five styles made in EffectCraft (an expression on a colour, a Hue stroke at Center, Inner Glow Center, Emboss, a Multiply overlay, Fill Opacity, Global Light 60); s2: 977 imported and exported | every value read back (s1); render of s2 = 977 (0 / 0); s1 EffectCraft vs After Effects mean 0.7 (glow, bevel and Hue in EffectCraft's renderer) | import equal |
|
||||
| `text\t6`, `text\t7` | character / paragraph slots; right-to-left justification | every slot via the re-save import, measures and renders (R43) | t6: the import equals the export's |
|
||||
| `text\t5` | normal, superscript, subscript HHHH | widths 288.9 / 168.4 / 168.4, baselines 0 / -33.3 / +33.3 (= 527 / 526 / 528) | not run |
|
||||
| `stretch\s1` | a precomposition with a moving square shown at 200 % and at 50 % from 1 s | stretch 200 / 50, in / out 0-8 and 1-3; the square at the same pixels as in EffectCraft at 1, 2, 4, 6 s | not run |
|
||||
| `threed\d1` | three 3D solids at z 0, 500, -300 without a camera over a 2D background | 3D switch and positions; render identical to EffectCraft (15 points) | Transform / Material Options rebuilt for 3D (R35) |
|
||||
| `footage\f2` | a jpeg sequence, a ProRes mov, a TIFF | all found, sizes and durations; TIFF identical to EffectCraft, the jpeg sequence and the mov 11-13 levels apart (the sRGB working space of the round-1 template, as f1) | not run |
|
||||
| `text\t1`-`t3` | 113 with the text replaced (same length, longer, longer with the layout emptied) | the new text, laid out and rendered right (R33) | t3: layout rebuilt |
|
||||
| `text\t4` | point text, three centred paragraphs, right-aligned Arial box text, two style runs with tracking, faux bold + all caps, faux italic + small caps with automatic leading, an anchor | all texts, fonts (Times New Roman, Arial), sizes, fills, justification (7413 / 7414 / 7415), tracking 50, leadings, the box 400 x 200 at [-200, -100], anchor [10, 20]; rendered with small caps, all caps and italics | font lists rebuilt (R34), layouts filled, nothing else |
|
||||
| `footage\f1` | png, jpeg and OpenEXR stills, an OpenEXR sequence (101-103, 4 digits), a png sequence, an mp4, a WAV, and an 8 x 8 RGBA png written with the RGB png prototype | every item found, names, sizes, durations, rates; renders identical to EffectCraft for png, OpenEXR, both sequences and the 8 x 8 png (21 points), jpeg and mp4 11-12 levels apart in both (round 1: the template's sRGB working space; re-exported with the project settings, R37, within one level); the timed layers ended at their source (R31) | aliases filled with the machine and volume name, `sspc` / `opti` / colour data of the stale png refreshed from the file (R30), an empty Mask Parade added to the WAV layer (as in 351) |
|
||||
| `effects\e1` | Tint on a red solid, Gradient Ramp on a 640 x 360 solid, a keyed Exposure and a second (renamed) Tint on a grey solid | every value and key; the renamed Tint showed the effect's name (R26; `e1b`, written after the fix, shows `Tint 2`); renders: Ramp and Exposure within 1 level of EffectCraft, Tint 10 levels apart from EffectCraft's Rec. 709 luminance (AE uses Rec. 601, `fix-tint-luma`) | value records copied into `EfdG`, `pard` value slots refreshed, `fnam` reset |
|
||||
| `precomp\p1` | 5 compositions, a 30 fps 640 x 360 precomposition with anchor and position, one composition used twice, a chain C2 > B > A, Scale keys on a precomposition layer | everything; renders identical to EffectCraft at 39 points (`READBACK.md` there) | C2's `idpc` count rewritten and its two `idpi` swapped (R23; p1b with the count fixed: only the swap), otherwise only noise |
|
||||
|
||||
### What After Effects needs: the read-backs behind the code-built chunks (2026-10-29)
|
||||
|
||||
Each variant is an export with one change: a chunk left out, or bytes zeroed. After Effects 2026
|
||||
opened it, and its own project export (JSON) was compared with the unchanged file; "same" means
|
||||
no difference there (renders where noted). The exporter writes only what the "needed" rows name,
|
||||
as constants in `export/base.rs`, `export/footage.rs`, `export/synth.rs` and `export/textproto.rs`.
|
||||
|
||||
| Chunk | Can be left out or zero (same) | Needed (what After Effects does without it) |
|
||||
|---|---|---|
|
||||
| top level | `svap`, `gpuG`, `sfnm`, `pcms`, `pdvc`, `ExEn`, `wsns`, `nnhd` absent; `fdta`, `Rhed`, `Rout`, `ppSn`, `prda` zero; `head` 16..19 and the `nhed` / `nnhd` save noise zero; `PTRE`, `CIF*` absent; `AFsi` / `ARsi` zero | `adfr`, `qtlg`, `mrid`, `acer`, `CPPl`, `fcid`, `oacc` ("missing data"); `head` 0..11 ("created with an older version"); the `nhed` rates ("zero denominator"); `LRdr` and `LSIf` ("file is damaged"), with an `AFsi` / `ARsi` body of 1 872 bytes ("missing data") |
|
||||
| composition | `idpc` absent; `cdrp` / `comr` zero; the view layers' property trees empty; `ldta` beyond the decoded fields zero | `dats` (**After Effects crashes**); `cdrp` / `comr` present; `cdta` 0..3, 176, 184, 196, 200 (zeroed: crash); the eleven view layers ("invalid number of ... cameras") |
|
||||
| items | `Ewst`, `Gide` absent; `idta` beyond 0..24 and 58 zero; `pgui` zero; `mnfo` absent | `idta` 23 (renders differ); the seven f-records after every item and layer ("missing data"); `CLRS` and `ftgi` ("missing data"); a solid's `sspc` constants (`SOLID_SSPC`, "missing data") |
|
||||
| footage | `opti` beyond its first 12 bytes zero; `sspc` 64..66, 69, 79, 97..101, 112..115, 159, 199..201, 212 zero; our own sRGB profile (or `{}`) in `CLRS` | the `opti` header and size, the `sspc` constants per kind (`kind_item`) |
|
||||
| property records | `tdb4` beyond the decoded bytes zero, except: | `tdb4` 8..11, the class word (keys are dropped without it); 16..55, 0.0001 and four 1.0 (a roving key reads back at time 0 without them, `re-keyframes`) |
|
||||
| text document | `/98`, `/1/2`, `/1/3`, `/1/4`, `/1/0/9`, the kinsoku sets `/0/4`; the sheets' and fonts' entries down to their names; the run dictionaries down to the keys the exporter writes; the layout down to `/0` and `/2` | `/0/5`, `/0/6` and `/0/8` (the layer is skipped; `/0/8` holds the box frame: empty, a box text turns into point text); `/1/0` (skipped) and all of `/1/0/0` (After Effects crashed); in every run the other sheet's key (`/6` in a paragraph run, `/5` in a style run; skipped); `/1/5` (without it After Effects rewrites the document on open) |
|
||||
|
||||
After all chunks were built in code, 20 cases (the lab cases e1, e2, m1, x3, y3, o1, o2, g3, t4, t7,
|
||||
t8, f1-f3 and six corpus re-exports) were read back against the template-based export: no
|
||||
difference.
|
||||
|
||||
## How fidelity is checked in this repository
|
||||
|
||||
- `crates/aep/tests/it/export/built.rs`: projects built in code are exported and parsed back with
|
||||
`effectcraft-aep`; every exported field of the composition header, the layer header, the solids,
|
||||
the Transform values, the keys (time, interpolation, value) and the expressions is compared; the
|
||||
container is consistent (writes back byte for byte); the report, the refusal and the JSON round
|
||||
trip; the output is deterministic; an export holds no local data.
|
||||
- `crates/aep/tests/it/export/roundtrip.rs`: every case of the corpus (opt-in, `EFFECTCRAFT_AEP_FIXTURES`) is imported,
|
||||
exported and imported again; the two imported projects are equal for the exported subset (more
|
||||
than 100 compositions, masks included).
|
||||
- `crates/aep/tests/it/export/masks.rs`: masks built in code (all seven modes, inverted, feather,
|
||||
opacity, expansion, open path and hexagon with tangents, path keys with hold and label, feather
|
||||
keys with per-dimension ease, a 400 x 300 source) are exported and parsed back with
|
||||
`effectcraft-aep` (name, mode, invert, counter, values with their default/stored state, vertices
|
||||
and tangents in pixels, the box, open bit, key records), then imported and compared field by
|
||||
field; every mask fixture goes through import, export and import; what cannot be written is
|
||||
reported; hostile masks (70 000 vertices, a single point, a line, a lopsided path, 1e300, a
|
||||
1000 character name) never panic and the container stays consistent.
|
||||
- `crates/aep/tests/it/export/effects.rs`: every effect case is imported, exported and parsed back; every parameter's stored value, keys (time,
|
||||
value, interpolation), the instance name and the input layer equal what After Effects wrote,
|
||||
and a second import gives the same EffectCraft values; edited parameters and keys no fixture
|
||||
has, two instances of a type (one full `parT`), the project lists, renamed instances; switched-off
|
||||
effects and expressions written and read back, unmapped effects reported; NaN, 1e300 and text values never panic.
|
||||
- `crates/aep/tests/it/export/text.rs`: every text case through import, export and parse:
|
||||
text, paragraphs, style runs (font, size, fill, tracking, faux, caps, leading), box equal to
|
||||
After Effects' document, and a second import equal; documents built in code (CRLF / LF, runs
|
||||
across a line break, a new font, box text, empty text, the name rule, the layer type); what is
|
||||
reported; hostile text (150 000 characters, NaN sizes, emoji, parentheses and backslashes,
|
||||
runs that do not cover the text). `src/export/textdoc.rs` has the tree's own tests.
|
||||
- `crates/aep/tests/it/export/footage.rs`: one item of every kind written and parsed back (type
|
||||
tag, size, duration, rate, numbering, the name rule, png options, layer sources and the time
|
||||
remapping maximum), escaped paths, fixtures 126 and 603 through import, export and import, what
|
||||
is reported (other types, gaps, NTSC rates, no path, layers past their source), hostile footage
|
||||
(100 000 frames, huge sizes, control characters, a sequence without numbers).
|
||||
- `crates/aep/tests/it/export/precomp.rs`: a 640 x 360, 30 fps, 3 s composition in a 25 fps one
|
||||
(source id, `idpc` / `idpi`, the size used for the anchor, the time remapping maximum, the name),
|
||||
import of the result, one composition used twice and chains of three, loops and missing
|
||||
compositions reported; `roundtrip.rs` compares the precomposition layers of 124 and 319.
|
||||
- `crates/aep/tests/it/export/robustness.rs`: empty names, huge and zero sizes, NaN and infinite
|
||||
numbers, non-positive frame rates, 2 000 layers with 20 000 keys, parent loops, folder loops,
|
||||
missing sources, 10 000 character names.
|
||||
- `crates/engine/src/tests/aep_export.rs`: the command, refusal, dry run, bad input, the menu entry.
|
||||
|
||||
## Clean-room method
|
||||
|
||||
The exporter reads no file After Effects wrote: every chunk is built in code. Every layout it
|
||||
writes is established in [aep-format.md](aep-format.md) from our own corpus or in the table above
|
||||
from experiments against After Effects itself. No source code or format documentation of any other
|
||||
`.aep` reader or writer was used.
|
||||
|
||||
## Not done yet
|
||||
|
||||
Not written: effects outside the table (plug-ins, Lumetri, ...), keys on popups / checkboxes / angles / integer sliders, colour key ease,
|
||||
Source Text keys, wiggly / expression selectors, shape items outside trim / round / merge / offset / repeater / polystar / gradients, gradient colour stops, parallel and ambient lights, bezier ease speeds on stretched layers,
|
||||
edited layer style gradients and Knockout, 3D rotations and material options, footage of other types (avi, mp3, pdf, ...),
|
||||
proxies and footage interpretation other than a conformed rate, the Advanced 3D renderer, working spaces other than None, sRGB and Rec.709 Gamma 2.4, 3D model layers (glb), the render queue.
|
||||
Masks: keys on Mask Opacity and Expansion (no keyed prototype in the corpus), the
|
||||
mask colour, lock, feather falloff, motion blur and RotoBezier switches (not decoded).
|
||||
+68
-6
@@ -1,6 +1,6 @@
|
||||
# After Effects project format (`.aep`)
|
||||
|
||||
> **Last reviewed:** 2026-10-19 · **Last updated:** 2026-10-19 · **Change:** the stretch numerator is signed (a negative one reverses the layer, 981-983); layer styles (3.12b, 950-980); before that: footage: `sspc@118` of an image sequence is its folder's modification time (860, read-back x1-x3); before that: text: paragraph `/24`, `/35` and style `/68` are neutral for 0 and 1 (732-734, edited in place); before that: keyframes: the ease of mask path keys (599, 700), the spatial speed unit of fractional values (597, 598, 730); before that: text: character and paragraph settings, manual kerning, vertical text, `/nil` is neutral (S17: 538-578, 595, 596); before that: cameras, lights and 3D rotations (S16: the auto Position of a two-node camera is where After Effects renders it, 537; camera and light defaults read back 255, 256; Orientation `otst/otda` 488), Timewarp (529-533), an expression on an effect parameter (534); before that: text: style slot /13 is superscript / subscript (526-528; every scripted text of the corpus is superscript), wrapping box text 517; before that: effects of batch 5 (409-450, 511-516: integer sliders, checkbox defaults in `pard`, arbitrary data of Hue/Saturation and Levels, the parameters of Invert, Fast Box Blur, Gaussian Blur, Drop Shadow, Glow; section 3.7); before that: shape items of batch 5 (451-484, 519: ellipse, fill and stroke opacity, fill rule, group transform, shape layer anchor, trim paths, rectangle roundness, polystar fields, gradient default, nested groups, the defaults of every item; section 3.9); before that: track mattes: mode at `ldta` byte 107 and the matte layer id at byte 160 (cases 402-408, section 3.1); before that: text: automatic leading is verified (400); Exposure (`ADBE Exposure2`, cases 389-397): group start / end parameter types 13 / 14, float slider valid minimum and slider range (section 3.7); before that: text: U+0003 is a paragraph break (386, 387); before that: a stored text Anchor Point is pixels (385, now V); before that: the XML form `.aepx` is read into the same chunk tree (section 1, "The XML form"); the `head` save counter is a `u32` at byte 16; text documents for the importer (stroke colour, automatic leading, the font of each run, the undecoded-slot check, the text-layer Anchor Point, the Source Text record); before that: third round of verified field layouts (explicit-default semantics, type-major bezier ease, exact time stretch and in / out points, Time Remapping state, comp pixel aspect / shutter phase / display start, Point default, Layer Control None, dropdown selection, one `mkif` per mask, polystar and radial gradient records, text styles, camera and light options, marker cue data, audio and image-sequence footage) added to the typed view of `effectcraft-aep` · **Target:** Adobe After Effects 2026
|
||||
> **Last reviewed:** 2026-11-05 · **Last updated:** 2026-11-05 · **Change:** versions: `svap` is the version that wrote the file, `head` bytes 4 to 7 the version it is written for, and what a Save a Copy As 25.x / 24.x of After Effects 2026 changes (section 1, "Versions"); before that: the stretch numerator is signed (a negative one reverses the layer, 981-983); layer styles (3.12b, 950-980); before that: footage: `sspc@118` of an image sequence is its folder's modification time (860, read-back x1-x3); before that: text: paragraph `/24`, `/35` and style `/68` are neutral for 0 and 1 (732-734, edited in place); before that: keyframes: the ease of mask path keys (599, 700), the spatial speed unit of fractional values (597, 598, 730); before that: text: character and paragraph settings, manual kerning, vertical text, `/nil` is neutral (S17: 538-578, 595, 596); before that: cameras, lights and 3D rotations (S16: the auto Position of a two-node camera is where After Effects renders it, 537; camera and light defaults read back 255, 256; Orientation `otst/otda` 488), Timewarp (529-533), an expression on an effect parameter (534); before that: text: style slot /13 is superscript / subscript (526-528; every scripted text of the corpus is superscript), wrapping box text 517; before that: effects of batch 5 (409-450, 511-516: integer sliders, checkbox defaults in `pard`, arbitrary data of Hue/Saturation and Levels, the parameters of Invert, Fast Box Blur, Gaussian Blur, Drop Shadow, Glow; section 3.7); before that: shape items of batch 5 (451-484, 519: ellipse, fill and stroke opacity, fill rule, group transform, shape layer anchor, trim paths, rectangle roundness, polystar fields, gradient default, nested groups, the defaults of every item; section 3.9); before that: track mattes: mode at `ldta` byte 107 and the matte layer id at byte 160 (cases 402-408, section 3.1); before that: text: automatic leading is verified (400); Exposure (`ADBE Exposure2`, cases 389-397): group start / end parameter types 13 / 14, float slider valid minimum and slider range (section 3.7); before that: text: U+0003 is a paragraph break (386, 387); before that: a stored text Anchor Point is pixels (385, now V); before that: the XML form `.aepx` is read into the same chunk tree (section 1, "The XML form"); the `head` save counter is a `u32` at byte 16; text documents for the importer (stroke colour, automatic leading, the font of each run, the undecoded-slot check, the text-layer Anchor Point, the Source Text record); before that: third round of verified field layouts (explicit-default semantics, type-major bezier ease, exact time stretch and in / out points, Time Remapping state, comp pixel aspect / shutter phase / display start, Point default, Layer Control None, dropdown selection, one `mkif` per mask, polystar and radial gradient records, text styles, camera and light options, marker cue data, audio and image-sequence footage) added to the typed view of `effectcraft-aep` · **Target:** Adobe After Effects 2026
|
||||
|
||||
What `crates/aep` ([`effectcraft-aep`](../crates/aep/README.md)) reads and writes, and the evidence
|
||||
for each statement. Everything here was observed on project files we generated ourselves; no
|
||||
@@ -124,6 +124,31 @@ counterpart (a name that is not 1 to 4 characters) are skipped with their conten
|
||||
inputs; `crates/aep/tests/it/import/aepx.rs` imports both forms of each pair and expects the same
|
||||
project and report. An ignored test runs the comparison over the whole local corpus.
|
||||
|
||||
### Versions (`head`, `svap`) and files saved for 25.x and 24.x
|
||||
|
||||
Evidence: 12 families of the corpus (`layers-comps`, `footage-precomp`, `masks`, `shapes`, `text`,
|
||||
`effects`, `3d`, `keyframes`, `markers`, `styles`, `settings-a`, `1200-lumetri`), each saved three
|
||||
ways by After Effects 2026 (26.2.1x2) in one session: a normal save (`-26`), File ▸ Save a Copy As
|
||||
25.x (`-25x`) and Save a Copy As 24.x (`-24x`), 36 files. All of them are copies made by 2026; no
|
||||
file written by After Effects 24 or 25 itself was examined, so nothing below says what those versions
|
||||
write on their own.
|
||||
|
||||
| Fact | Evidence |
|
||||
|---|---|
|
||||
| `svap` (4 bytes, the first top-level chunk) is the version of the After Effects that **wrote** the file: `0f 11 0e 02` in all 36 (a down-saved copy keeps the 2026 word) and in all 1 192 other `.aep` files of the local corpus folders. After Effects opens a file without `svap` (`t-nosvap`). | All 36; corpus scan. |
|
||||
| `head` bytes 4 to 7 are the version the file is **written for**: `0f 11 0e 02` (= `svap`) in the 2026 saves, `0f 0b 06 61` in the 25.x copies, `0f 03 06 41` in the 24.x copies, the same word in every family. After Effects refuses a file whose word is zero ("created with an older version [0.0x0]", `u-zhead`). | All 36; `u-zhead`. |
|
||||
| The word is packed (bit 31 first): bits 25-24 and 21-19 the major, 18-15 the minor, 14-11 the bug-fix, 10-8 a stage (6 in all), 7-0 the build; bits 31-26 (`000011`) and 23-22 (`00`) are the same in all samples and not decoded. `0f110e02` = **26.2.1x2**, which is what the saving After Effects reports for itself (`app.version` recorded by the corpus scripts) - this one sample fixes minor, bug-fix and build. The copies read as **25.6.0x97** and **24.6.0x65** (major **V** from three consecutive majors; minor, bug-fix and build of the copies **E**: one other word decides their positions). `effectcraft_aep::AeVersion`, `AepFile::file_version`, `AepFile::saved_by_version`. | Tests `decodes_the_sampled_words`, `the_container_of_a_copy_states_its_version_in_head_and_the_writer_in_svap`. |
|
||||
| `head` bytes 0 to 1 (a `u16`) step with the major: 97 (`0x61`) in the 2026 saves, 96 in 25.x, 95 in 24.x; bytes 2 to 3 are 5, 9 and 6 (**?**). Bytes 8 to 15 differ between families, not between versions; bytes 16 to 19 are the save counter (one higher in each copy). Nothing is derived from bytes 0 to 3 (`AepFile::head_revision` exposes the `u16`). | All 36. |
|
||||
| The XMP packet after the container is no guide: its `CreatorTool` is "Adobe After Effects 2026" in the copies too. | All 36. |
|
||||
| **26 against 25.x** (per family, chunk tree aligned by item and layer id): besides `head`, `nhed` / `nnhd` differ in 6 bytes (`nhed` 20-23 and 26-27: `827fa930` / `0015` in the 2026 save, `00000000` / `011b` in both copies; session state, see section 4). Every **solid's** `Pin /CLRS` changes: the 2026 save has `dcui` (1 byte, 1), `apid` all `0xff` and an empty `Utf8`; the copies drop `dcui`, set `apid` to a GUID (`601e9973...`, the same in all) and embed a 935-byte JSON with an ICC profile (`baseColorProfile/colorProfileData`). A **footage** item that the 2026 save stores with an assigned profile (`mcsp` = 1 and a profile JSON) loses `mcsp` and that JSON in the copies, and its `epid` becomes all `0xff`. The rest is save noise: the footage alias `ascendcount_base` (the copies were saved in another folder), `sspc` byte 112 of a footage item (a modification time), the `oEXR` `opti` of an EXR item (the copies hold the display device name `\.\DISPLAY1` as UTF-16 at byte 126 and, in the 24.x copy, a few non-zero bytes from byte 2 102 that look like uninitialised memory; the 2026 save has zeros), the plug-ins' sequence data `sdat` of Lumetri Color (16 instances and the project's `EfdG`), the mask counter `mkif` byte 8. No chunk is added or reordered, and no `pard`, `cdat`, `ldta`, `cdta`, `tdb4`, keyframe, text or shape chunk differs. | All 12 families. |
|
||||
| **25.x against 24.x**: only `head` (bytes 1, 3, 5 and 7), and the session noise in `opti` and `sdat` above. | All 12 families. |
|
||||
| Lumetri Color in the copies has the parameter definitions (`pard`, valid ranges included) of the 2026 plug-in, byte for byte. | `1200-lumetri`. |
|
||||
|
||||
The importer reads none of the chunks that change (it reads the solid colour from `opti`, not
|
||||
`CLRS`), so the copies import to the same project and report as the 2026 save
|
||||
([aep-import.md](aep-import.md), "What is reported"; test
|
||||
`copies_saved_as_25x_and_24x_import_like_the_2026_file`).
|
||||
|
||||
## 2. Typed view (`Project`, `Item`, `Comp`, `Layer`, `PropGroup`, `Property`, `KeyList`)
|
||||
|
||||
The structure the typed view is built on; each row has a test in `crates/aep/tests/it/container/typed.rs` against
|
||||
@@ -375,7 +400,7 @@ out speed, at 40 out influence**). Scale key 1 of 305 (`n = 3`, in 0 / 33 %, out
|
||||
15 / 66) is `0,0, .33,.33, 15,15, .66,.66`. Component `i` uses slot `i` of each block (**V** for the
|
||||
block layout and the units, Scale speed in fractions per second and Mask Feather speed in pixels
|
||||
per second; **E** that different values per dimension sit in the slot of their dimension, because
|
||||
the tool that made the corpus applies one ease to all dimensions; **V** since a file of ours with three different eases for the three Scale dimensions read back the same three in After Effects). `Keyframe::plain_ease_slots`
|
||||
the tool that made the corpus applies one ease to all dimensions; **V** since the exporter wrote three different eases for the three Scale dimensions and After Effects read the same three back, aep-export.md R13). `Keyframe::plain_ease_slots`
|
||||
returns the doubles as stored, `Keyframe::ease_component(i)` one component's `KeyEase`. Layout of
|
||||
the spatial template: `u32` at 8 (1 normally, 0 after the spatial auto-bezier and continuous
|
||||
switches were turned off together, 054), `f64` arc length of the segment to the next key at 16
|
||||
@@ -576,6 +601,43 @@ suffix is a parameter id, not the `parT` position (Curves -0002 Channel before -
|
||||
-0005 Quality before -0004). API: `CurvesData`, `MeshData`, `curve_lut`. [V; details: corpus FORMAT
|
||||
S]
|
||||
|
||||
**Cryptomatte plug-in** (match name `Cryptomatte`, the free Cryptomatte plug-in, 2.6.6 in our
|
||||
sample; read from 88 instances in one production project, not made by us, and from our own After
|
||||
Effects 2026 sample 1400 (corpus `aep/cryptomatte.aep`: a fresh instance, Selection Mode on, each
|
||||
Output item, and two presets of our own that choose a layer and pick one object or material in the
|
||||
plug-in's UI; scripting read-backs and renders per composition); H for what neither varies):
|
||||
`parT` 0 the input layer; 1 Settings: **arbitrary data** (type 11, `Cryptomatte-0001`, a record and
|
||||
`aRbs` / `aRbp` in every instance); 2 Output: popup (Colors, Matted Colors, Matted RGBA, Matte
|
||||
Only), **`pard` default 1 = Colors** (bytes 56..64 `00000001 0004 0001`, in the instance's own and
|
||||
the project's shared definition; scripting reads 1 from a fresh effect: 1400 `crypto-00-defaults`),
|
||||
a written record (`tdsb` 1) holding the item number when set (1400: 2, 3, 4; production: 3 in 81,
|
||||
4 in 7); 3 Selection Mode: checkbox (`pdnm` `fnord!`), no record when off, and when on **a
|
||||
default-state record (`tdsb` 3) holding 1.0** that After Effects reads back as on and renders
|
||||
(1400 `crypto-01-set-all`, `crypto-02-chk-toggled`; the general reader takes only `tdsb` 1
|
||||
values, so the importer reads this one itself); the built-in entry. As in every project, only the
|
||||
first instance has its own `parT`; the others borrow the project's (1400). The Settings data is
|
||||
the plug-in's own layout, **little-endian**: `cry1`; bytes 4..32 differ between instances with
|
||||
the same settings (zeros in one, pointer-like values in others: not read); the layer name at 32,
|
||||
NUL-padded to 64 bytes (`CryptoObject`, `CryptoMaterial` in 1400: the EXR layer's name, which is
|
||||
also its channel prefix there, so the sample cannot tell which of the two the plug-in stores); at
|
||||
96 a `u32` 1 and a `u32` that is the same in every instance; at 104 the `u32` length of the
|
||||
selection text including its NUL; at 108 **the MurmurHash3 (x86, 32-bit, seed 0) of the selection
|
||||
text with its NUL** (V on the three texts of 1400: `<11a97486>` `0x27880767`, `<a5deeb9e>`
|
||||
`0xf7374a5b`, the empty text `0x514e28b7`; not read); a zero byte at 112; the selection text from
|
||||
113, the matte list as comma-and-space separated ID hashes `<11a97486>, <deadbeef>` (303 tokens in
|
||||
production, all in that form). Each token is the 8-digit hash of a picked name as the EXR's
|
||||
manifest lists it, i.e. the MurmurHash3 of the name (1400: `box_red` `11a97486` picked on
|
||||
CryptoObject, `mat_paint` `a5deeb9e` on CryptoMaterial). After the text 2 to 3 bytes, zeros or
|
||||
left-overs of a longer earlier text (1400's presets: 127 bytes, three zeros after the text). **A fresh effect** (the `parT`
|
||||
default and the record alike, 116 bytes, 1400) is `cry1`, zeros to 96, then `u32`s 1,
|
||||
`0x514e28b7`, 1, `0x514e28b7` and four zero bytes: no layer and an empty text (length 1, its NUL).
|
||||
`0x514e28b7` is the hash of a single NUL byte, so the `u32` at 100 looks like the hash of a first
|
||||
text that is empty in every sample, its length the `u32` 1 at 96 and its NUL the zero byte at 112
|
||||
(H: one value). Layer and selection decode the same in the production project and in 1400's two
|
||||
presets, and After Effects draws exactly the picked objects as selected (`crypto-06`: the box,
|
||||
`crypto-07`: box and disc, the two objects of `mat_paint`). API: `CryptomatteData` (refuses data without `cry1`, a length past the end or
|
||||
a non-zero byte at 112).
|
||||
|
||||
### 3.8 Masks and bezier paths (`tdgp[ADBE Mask Parade]`)
|
||||
|
||||
| Field | Chunk path | Offset | Type | Meaning | Evidence | Conf. |
|
||||
@@ -796,7 +858,7 @@ switch group per style, `<style>/enabled` (`dropShadow`, `innerShadow`, `outerGl
|
||||
| gradients | `<style>/gradient` is a `GCst` list, not decoded | - | ? |
|
||||
|
||||
At 32 bpc After Effects renders the modes 6, 7, 11, 12, 16-22 and 25 of a style as Normal (978);
|
||||
at 16 bpc as their formulas. API: `effectcraft_aep::style_map` (the importer's table).
|
||||
at 16 bpc as their formulas. API: `effectcraft_aep::style_map` (table shared by importer and exporter).
|
||||
|
||||
### 3.13 What the tests cover
|
||||
|
||||
@@ -847,12 +909,12 @@ another value).
|
||||
- Bytes 0 (**?**), 16 (600: **H**, the 1/600 s legacy time base), 24, 32, 40 (cache-like: they hold values the session used earlier, do not use them: **E**), 48 (**E**), 176 and 184 (360 in all files: **?**, probably the maximum shutter angle), 196 and 200 (128 and 16: **?**).
|
||||
- Not sampled: a 3840 x 2160 composition, other pixel aspect fractions (1.2121, 1.4587), the preserve-frame-rate and preserve-resolution switches, draft 3D, `Hide shy layers`, frame blending, the 8-versus-16-bit project depth with a coloured background, the display start at 30 fps and with drop frame (whether the denominator at 168 is always the timescale).
|
||||
- `PRin/prda` (12 bytes for Classic 3D, 52 for Advanced 3D: renderer parameters, **E**).
|
||||
- Item `idta` byte 20 and `idpc` (**V** in the corpus: 1 when the composition contains a nested composition), `head`, `nhed`, `nnhd`, `Ewst`, the `f*` records, `Gide`, `PTRE`, `LRdr` and `LSIf` bodies. `head` bytes 16 to 19 are the save counter (a big-endian `u32`; only its low byte changed between most saves, `314` shows the carry) and are the only bytes of the container that change in a Save As without an edit; item `idta` bytes 80 to 83 are the modification time (seconds since 1904-01-01); `nhed` / `nnhd` hold the id of the After Effects session (six sessions seen); `mkif` byte 8 is a running mask counter; `pgui` chunks are random GUIDs. All of that is edit-history or session state (**V** in the corpus) and is not decoded, except the mask counter.
|
||||
- Item `idta` byte 23 (0x20 composition and sequences or clips, 0x30 stills and solids, 4 audio, 0 folder; needed: zeroed, renders differ) and `idpc` (**V** in the corpus: 1 when the composition contains a nested composition), `head`, `nhed`, `nnhd`, `Ewst`, the `f*` records, `Gide`, `PTRE`, `LRdr` and `LSIf` bodies. `head` bytes 4 to 7 are the version the file is written for and `svap` the version that wrote it (section 1, "Versions"); `head` bytes 16 to 19 are the save counter (a big-endian `u32`; only its low byte changed between most saves, `314` shows the carry) and are the only bytes of the container that change in a Save As without an edit; item `idta` bytes 80 to 83 are the modification time (seconds since 1904-01-01); `nhed` / `nnhd` hold the id of the After Effects session (six sessions seen); `mkif` byte 8 is a running mask counter; `pgui` chunks are random GUIDs. All of that is edit-history or session state (**V** in the corpus) and is not decoded, except the mask counter.
|
||||
|
||||
**Properties and keyframes**
|
||||
|
||||
- `tdb4` bytes 4, 8..11 (a descriptor per property class: **E**), 16 (0.0001: **E**), 24 (the composition pixel aspect on spatial properties: **E**), 32..48 (1.0: **E**), 56..67 (a class pair: **E**), 72..78 except 76 and 77 (**E**), 79 (**E**); `tdsb` bits other than 0x1 and 0x2 (group flags `0x401`, `0x11`, ...) and the `tdsb` 01 / 03 of the Vector Materials Group; `tdum` / `tduM` (**?**).
|
||||
- Per-dimension ease values of a colour or a 2-D key (Scale is **V**, above); a colour key with a non-zero ease (a colour has one ease entry; the sample reports 0 / 0); the ease units of a keyed effect Point (the 2-D spatial record itself is **V**, 382, with linear keys only).
|
||||
- `tdb4` bytes 4, 8..11 (a descriptor per property class: **E**; needed, without it keys are dropped), 16 (0.0001: **E**), 24 (the composition pixel aspect on spatial properties: **E**), 32..48 (1.0: **E**; 16..55 are needed by roving keys: zeroed, After Effects reads a roving key back at time 0), 56..67 (a class pair: **E**), 72..78 except 76 and 77 (**E**), 79 (**E**); `tdsb` bits other than 0x1 and 0x2 (group flags `0x401`, `0x11`, ...) and the `tdsb` 01 / 03 of the Vector Materials Group; `tdum` / `tduM` (**?**).
|
||||
- Per-dimension ease values of a colour or a 2-D key (Scale is **V**, aep-export.md R13); a colour key with a non-zero ease (a colour has one ease entry; the sample reports 0 / 0); the ease units of a keyed effect Point (the 2-D spatial record itself is **V**, 382, with linear keys only).
|
||||
- The 8 stale bytes of the marker record and the bytes after the common fields of the mask path record (uninitialised memory).
|
||||
- Expressions in effect parameters, multi-line and non-ASCII expressions, the ExtendScript engine.
|
||||
- Time Remapping with extra keys, with a stretch, on a nested layer; whether keys are rescaled by a stretch.
|
||||
|
||||
+34
-7
File diff suppressed because one or more lines are too long
+19
-2
@@ -261,6 +261,11 @@ build real projects through these interfaces; they are worked examples of everyt
|
||||
`layer.centerAnchor` skip them and fail when every target is locked ("… is locked"); Select
|
||||
All and `layer.selectNext` / `selectPrevious` step over them. Unlock with
|
||||
`layer.setSwitch {"layers": [...], "switch": "lock", "value": false}` or `layer.unlockAll`.
|
||||
* **Quick Set Anchor**: `layer.quickSetAnchor {point: topLeft|top|topRight|left|center|right|bottomLeft|bottom|bottomRight, layers?}`
|
||||
(Layer ▸ Transform ▸ Quick Set Anchor) moves the Anchor Point of 2D layers to that point of their
|
||||
content and shifts Position and its keys so the layers don't move, in one undo step. 3D and
|
||||
locked layers, and layers whose Anchor Point, Scale or Rotation is keyed or has an expression,
|
||||
are skipped (an error when none is left); returns `{layers}`.
|
||||
* **Solid Settings**: `layer.settings {layer, name?, color?, width?, height?, pixelAspect?,
|
||||
affectAll?}` edits a solid or adjustment layer's solid (and renames the layer and the solid).
|
||||
A solid shared by several layers (duplicates share theirs) changes for all of them unless
|
||||
@@ -313,12 +318,21 @@ build real projects through these interfaces; they are worked examples of everyt
|
||||
channels (`{"layers":[{"name":"diffuse","rgba":["diffuse.R","diffuse.G","diffuse.B",""]}]}`);
|
||||
EXtractoR shows them when its `red` / `green` / `blue` / `alpha` are set to those names
|
||||
(`prop.set {"path":"effects/#1/red","value":"depth.Z"}`). Effect Controls offers them as popups.
|
||||
* **Masks**: `layer.addMask {layer, shape: rect|ellipse, rect}` first, then `layer.mask.set
|
||||
{field: feather|expansion|opacity, value}`.
|
||||
* **Masks**: `layer.addMask {layer, shape: rect|ellipse|rounded, rect, roundness}` first, then
|
||||
`layer.mask.set {field: feather|expansion|opacity, value}`. A Rounded Rectangle mask's corner
|
||||
radius (`roundness`, layer pixels) is set when it's added and changed later with
|
||||
`mask.setRoundness {layer, mask, roundness}` (0 = square corners); it only reshapes a rectangular mask.
|
||||
* **Renders**: `renderQueue.add {comp, format: h264|prores|webm|png…, channels: rgba,
|
||||
proresProfile: 4444, output}` then `renderQueue.render {"wait": true}`. Check a render by
|
||||
importing it (`file.import`, `file.newCompFromSelection`) and `render_frame {transparent: true}`
|
||||
for its alpha (ProRes 4444 and WebM VP9 alpha both read back as straight alpha).
|
||||
OpenEXR sequences (`format: exr`) are scan-line files, linear and premultiplied, and take
|
||||
`exrCompression: none|rle|zips|zip|piz|pxr24|b44|b44a|dwaa|dwab` (default `zip`, 16-line
|
||||
blocks; `zips` is ZIP one scan line per block, the "Zip (1 scanline)" many compositing
|
||||
pipelines default to) and `exrBitDepth: half|float` (default `float`); `renderQueue.formats`
|
||||
lists them.
|
||||
PXR24 on float, B44 / B44A on half and DWAA / DWAB are lossy (in the tests every linear sample
|
||||
stays within 0.03 + 6 % of its value); B44 / B44A store 32-bit float uncompressed. Half rounds each sample to the nearest half float and holds it to ±65 504.
|
||||
**A render never writes one file twice either:** when two outputs of the job (two items, a
|
||||
`renderQueue.duplicate` copy, output modules, segments) name the same file, the whole render is
|
||||
refused before anything is written, naming both items. Sequences clash only on frames both
|
||||
@@ -752,6 +766,7 @@ effectcraft-cli exec layer.newNull --bridge 9877 # same commands
|
||||
effectcraft-cli exec file.exportLottie '{"comp":"Main","path":"main.json","includeExpressions":true}' main.ecproj --json
|
||||
effectcraft-cli exec file.importLottie '{"path":"anim.json"}' main.ecproj --save
|
||||
effectcraft-cli render main.ecproj --comp Main --out main.mov --format prores --progress # progress lines on stderr for a program that reads them
|
||||
effectcraft-cli render main.ecproj --comp Main --out 'main_[#####].exr' --exr-compression piz --exr-depth half # half-float PIZ OpenEXR frames
|
||||
```
|
||||
|
||||
Runs that save over the project they opened (`--save`, or `--save-as` the same file) take turns, so
|
||||
@@ -795,6 +810,8 @@ agent can check what a player will not show. A `.lottie` path writes a dotLottie
|
||||
|
||||
`file.importAep {path, allowLosses?, dryRun?, applySettings?}` imports an After Effects project (`.aep` or `.aepx`) into a folder named after the file and returns `{folder, comps, items, report}`. The report counts what arrived and lists, per category, what did not (effects outside the importer's table, undecoded text or style settings, simplified keyframes…); without `allowLosses` an import that would lose anything is refused and changes nothing, and `dryRun` returns just the report ([aep-import.md](aep-import.md)).
|
||||
|
||||
`file.exportAep {path, allowLosses?, dryRun?}` writes the project as an After Effects project and returns `{path, bytes, report}`. The report counts what was written and lists, per category, what was not; without `allowLosses` an export that would lose anything is refused and writes nothing, and `dryRun` returns just the report ([aep-export.md](aep-export.md)).
|
||||
|
||||
Premiere Pro interop goes through timeline interchange (MCP: the `execute_command` tool; control channel: `engine.execute`
|
||||
with the same ids):
|
||||
|
||||
|
||||
+26
-3
@@ -41,7 +41,7 @@ cpal or muda. Everything in L0 to L4, the egui UI and the web app also build for
|
||||
| L3 | `gpu` | The GPU compositor and GPU effects on wgpu compute shaders (Metal, Vulkan, Direct3D 12, WebGPU), checked against the CPU renderer |
|
||||
| L3 | `sam2` | SAM 2.1 image segmentation and video object tracking for Roto Brush, with candle CPU inference and optional wgpu acceleration; native only, empty on wasm32, with user-installed weights |
|
||||
| L3 | `interchange` | Premiere Pro interop through timeline interchange (FCP7 XML, FCPXML, OTIO, EDL, AAF, OMF via FilmCraft's `filmcraft-interchange`): sequences ↔ compositions (§6a) |
|
||||
| L3 | `aep` | After Effects projects in one crate (like `lottie`): `container` reads and writes the `.aep` container (RIFX `Egg!`) as a lossless chunk tree with a byte-identical round trip, bounded for hostile input, reads the `.aepx` XML form into the same tree, and has a typed view of items, layers, properties, effects, masks, shapes, text, markers, footage, cameras, lights and layer styles, from our own observations of generated files ([aep-format.md](aep-format.md)); `import` maps the typed view into a project (items, settings, compositions, every layer kind, Transform, keyframes, expressions, masks, text, shapes, cameras, lights, mattes, markers, time remapping, layer styles, the effects of the importer's table, footage) with a report of everything not imported ([aep-import.md](aep-import.md)) |
|
||||
| L3 | `aep` | After Effects projects in one crate (like `lottie`): `container` reads and writes the `.aep` container (RIFX `Egg!`) as a lossless chunk tree with a byte-identical round trip, bounded for hostile input, reads the `.aepx` XML form into the same tree, and has a typed view of items, layers, properties, effects, masks, shapes, text, markers, footage, cameras, lights and layer styles, from our own observations of generated files ([aep-format.md](aep-format.md)); `import` maps the typed view into a project (items, settings, compositions, every layer kind, Transform, keyframes, expressions, masks, text, shapes, cameras, lights, mattes, markers, time remapping, layer styles, the effects of a shared table, footage) with a report of everything not imported ([aep-import.md](aep-import.md)); `export` builds every chunk in code (prototypes in the layout After Effects writes, cloned and edited only at verified fields) for the same scope, with a report of everything not exported, verified by opening the output in After Effects ([aep-export.md](aep-export.md)) |
|
||||
| L3 | `lottie` | Lottie JSON / dotLottie import and export (layers, precomps, eased and spatial keyframes, shapes, masks, mattes) with a warnings list for what Lottie cannot express |
|
||||
| L3 | `plugin` | Effect plug-ins: loads sandboxed WebAssembly effect modules (plug-in API v1; the wasmi interpreter behind the `wasm` feature, on for native hosts) into the effect registry ([plugins.md](plugins.md)) |
|
||||
| L3 | `helper-host` | External effect helpers: finds helper programs, runs each as a child process and registers its effects (plug-in API 2) over a versioned pipe + shared-file protocol; a crashing helper is an error on the effect, never a crash ([external-helpers.md](external-helpers.md), [ADR 0001](adr/0001-external-effect-helpers.md)). Native only |
|
||||
@@ -220,12 +220,33 @@ instead of skipping pixels (Viewer Zoom Quality More Accurate): GPU frames carry
|
||||
sampled trilinearly, CPU frames go up averaged down by the whole factor that leaves about one
|
||||
texel per screen pixel.
|
||||
|
||||
32 bpc projects keep **half-float frames** on the desktop (`frames::F16Frame`, 8 bytes a pixel:
|
||||
the RAM preview holds half as many as of 8-bit frames). They are rendered without Project
|
||||
Settings ▸ HDR (`RenderOpts::skip_hdr_step`), so the values above 1.0 stay. The viewer's
|
||||
**display pass** (`engine::viewer::DisplayPass`; on the GPU `ui-egui/src/display_gpu.rs` +
|
||||
`display_gpu.wgsl`) then turns a frame into what the panel shows, in this order: decode with the
|
||||
output space's curve, the viewer's exposure in linear light, the HDR step the frame was rendered
|
||||
without (Compand / Tone Map), encode, clamp to 0..1, the display colour conversion (Use Display
|
||||
Color Management, Simulate Output, LUT profiles), Show Channel, premultiply, round to 8 bits.
|
||||
So exposure reaches the full float range, and tone mapping and the monitor conversion see the
|
||||
exposed light. The pass runs on egui-wgpu's device (it needs no GPU compositor) into one RGBA8
|
||||
texture with mips per viewer slot (main viewer, snapshot, each passive viewer: about 110 MB of
|
||||
video memory per slot for a 4K frame, input included); without a device the same steps run on
|
||||
the CPU (`DisplayPass::pixel`, which is also the shader's test reference). 8 and 16 bpc frames
|
||||
stay 8-bit with the HDR step applied when rendered; they go through the same pass when exposure,
|
||||
a channel view or a display conversion applies, else they are drawn as they are. Panels that
|
||||
read the viewer's pixels as 8-bit (Video Preview, keyers, thumbnails) get the half-float frame
|
||||
converted on demand with the HDR step and without the exposure; the Info panel shows the
|
||||
frame's float values. The browser keeps 8-bit frames for now (its frame workers send RGBA8).
|
||||
|
||||
A persistent **disk cache** (`render::disk_cache`, Settings ▸ Media & Disk Cache) backs both
|
||||
the layer cache and the viewer's RAM preview: layer buffers that were slow to render and every
|
||||
preview frame are written (LZ4, atomic rename, checksummed) under 128-bit content keys — the
|
||||
comp's content hash (the comp, what it uses, footage file size and time) plus frame, scale and
|
||||
view for frames; the layer key salted with the footage fingerprint for layers — so entries
|
||||
survive restarts and never go stale. LRU eviction keeps it under the size limit; Edit ▸ Purge
|
||||
survive restarts and never go stale. Frame entries are 8-bit RGBA, or for 32 bpc viewer frames
|
||||
half floats with their bytes shuffled into planes before LZ4 (the entry header's format byte
|
||||
tells them apart; the render options in the key keep the two kinds apart). LRU eviction keeps it under the size limit; Edit ▸ Purge
|
||||
empties it, `cache.diskStats` reports it, and the timeline draws disk-only frames in blue. In the
|
||||
browser the frames live in the Origin Private File System: frame workers write them, the page
|
||||
keeps the index (`DiskIndex`) and reads hits ([web.md](web.md#disk-cache)).
|
||||
@@ -414,7 +435,9 @@ mapping) is the last top-level step, for standard-dynamic-range displays and out
|
||||
renders (sequences and ProEXR) skip it (`RenderOpts::skip_hdr_step`) and decode the frame with the
|
||||
output space's own curve (`Pipe::output_linear`), so they hold linear light in the output space's
|
||||
primaries; unmanaged projects decode with the Working Gamma's curve (sRGB at 2.2, a pure 2.4 power
|
||||
curve at 2.4). Unmanaged projects and Rec. 2100 PQ / HLG outputs never get the HDR step.
|
||||
curve at 2.4). Every OpenEXR file (sequences and ProEXR) names those primaries and the white point in its
|
||||
`chromaticities` attribute (`ColorSpace::exr_chromaticities`; Rec. 709 / sRGB primaries for the default
|
||||
and for unmanaged projects, Rec. 2100 PQ and HLG as BT.2020, the ACES spaces with the ACES white). Unmanaged projects and Rec. 2100 PQ / HLG outputs never get the HDR step.
|
||||
|
||||
**Frame blending**: footage and precomp layers whose source time falls between source frames
|
||||
(rate conform, stretch, remap) blend the two neighbouring frames, by cross-fade (Frame Mix) or by
|
||||
|
||||
@@ -9,6 +9,13 @@ effectcraft --control 9877 # or EFFECTCRAFT_CONTROL_PORT=9877
|
||||
The port must be a number from 1 to 65535, and `--control` overrides the variable. A bad port or an
|
||||
unknown option prints an error and the app exits with status 1 before it opens a window.
|
||||
|
||||
An app started with a control port is meant to be driven by an agent, so a normal launch does not
|
||||
wait for a person: the window opens without taking keyboard focus, and the app skips crash recovery,
|
||||
the Home screen (unless `--home` is given) and the startup update check. Help ▸ Check for Updates
|
||||
still works. One exception: when the window cannot start at all, the app writes the error to
|
||||
standard error and the session log and then shows it in a message box, and it exits only after
|
||||
someone closes that box.
|
||||
|
||||
- **Transport:** TCP on `127.0.0.1` only, one JSON object per line in each direction. Connections are
|
||||
persistent; requests on one connection are answered in order.
|
||||
- **Request:** `{"id": 1, "method": "engine.execute", "params": {...}}` (`id` is echoed back, `params`
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
<!-- Generated from the effect registry (crates/effects/src/catalog.rs); do not edit by hand.
|
||||
Regenerate: UPDATE_DOCS=1 cargo test -p effectcraft-effects --lib effects_doc_is_current -->
|
||||
|
||||
EffectCraft ships 308 effects with 2987 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
|
||||
EffectCraft ships 308 effects with 2988 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
|
||||
|
||||
- **GPU**: 280 effects also run on the GPU compositor with identical results.
|
||||
- **32**: 308 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||
@@ -17,7 +17,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| 3D Channel Extract | `ec.3d.channelextract` | 6 | GPU | 32 | Implemented |
|
||||
| 3D Layer Instance | `ec.3d.instance3d` | 1 | | 32 | Implemented |
|
||||
| Cryptomatte | `ec.3d.cryptomatte` | 4 | GPU | 32 | Implemented |
|
||||
| Cryptomatte | `ec.3d.cryptomatte` | 5 | GPU | 32 | Implemented |
|
||||
| Depth Matte | `ec.3d.depthmatte` | 3 | GPU | 32 | Implemented |
|
||||
| Depth of Field | `ec.3d.depthoffield` | 4 | GPU | 32 | Implemented |
|
||||
| EXtractoR | `ec.3d.extractor` | 8 | GPU | 32 | Implemented |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-19 · **Last updated:** 2026-11-05 · **Change:** ProRes alpha final-run conformance; previously `.aep` / `.aepx` import (`crates/aep`: `container`, `import`) · **Target:** Adobe After Effects 2026
|
||||
> **Last reviewed:** 2026-10-19 · **Last updated:** 2026-11-05 · **Change:** ProRes alpha final-run conformance; previously `.aep` / `.aepx` import and `.aep` export (`crates/aep`: `container`, `import`, `export`) · **Target:** Adobe After Effects 2026
|
||||
|
||||
Every format After Effects reads or writes, with EffectCraft's support. After Effects' list comes
|
||||
from its installed bundle (26.5.0.89: `Info.plist` `CFBundleDocumentTypes`, `Plug-ins/Format`,
|
||||
@@ -12,7 +12,7 @@ from its installed bundle (26.5.0.89: `Info.plist` `CFBundleDocumentTypes`, `Plu
|
||||
|
||||
**Overall ≈ 50%** (estimated, weighted by use): modern delivery and interchange formats are
|
||||
strong, but After Effects' own formats, which decide whether existing work moves over, are the
|
||||
weakest: `.aep` / `.aepx` import is partial, export is not built, templates and presets are 0%.
|
||||
weakest: `.aep` / `.aepx` import and export are partial, templates and presets are 0%.
|
||||
|
||||
Fidelity key: **exact** (bit-exact or within a stated tolerance of a reference), **good** (round
|
||||
trips and renders, checked by tests, not against After Effects), **partial** (named limitations),
|
||||
@@ -22,7 +22,7 @@ trips and renders, checked by tests, not against After Effects), **partial** (na
|
||||
|
||||
| Format | After Effects | EffectCraft read | Write | Fidelity / notes | Tested by |
|
||||
|---|---|---|---|---|---|
|
||||
| Project `.aep` | R/W (native) | partial | none | Main file format; the clean-room method is in [aep-format.md](aep-format.md), the maintainers' decision on it is open (#190). `crates/aep` reads and writes the RIFX container losslessly (byte-identical round trip) with a typed view of the decoded fields, from our own generated files only ([aep-format.md](aep-format.md)). File ▸ Import ▸ After Effects Project… (`file.importAep`, `effectcraft_aep::import`): folders, project settings, compositions, every layer kind, Transform, keyframes, expressions, masks, text with animators, shapes, cameras, lights, track mattes, markers, time remapping, layer styles, the built-in effects of a table (Curves, Levels, Glow, Fractal Noise, Gradient Ramp, … and the expression controls), footage and image sequences; everything else is reported ([aep-import.md](aep-import.md)). Export to `.aep` is not built yet. Effects outside the table (plug-ins, Lumetri, …) are not converted | `crates/aep` tests (`tests/it`: `container`, `import`) |
|
||||
| Project `.aep` | R/W (native) | partial | partial | Main file format; the clean-room method is in [aep-format.md](aep-format.md), the maintainers' decision on it is open (#190). `crates/aep` reads and writes the RIFX container losslessly (byte-identical round trip) with a typed view of the decoded fields, from our own generated files only ([aep-format.md](aep-format.md)). File ▸ Import ▸ After Effects Project… (`file.importAep`, `effectcraft_aep::import`): folders, project settings, compositions, every layer kind, Transform, keyframes, expressions, masks, text with animators, shapes, cameras, lights, track mattes, markers, time remapping, layer styles, the built-in effects of a table shared with the exporter (Curves, Levels, Glow, Fractal Noise, Gradient Ramp, … and the expression controls), footage and image sequences; everything else is reported ([aep-import.md](aep-import.md)). File ▸ Export ▸ After Effects Project… (`file.exportAep`, `effectcraft_aep::export`) writes the same scope with every chunk built in code; exports were opened and read back in After Effects 2026 ([aep-export.md](aep-export.md)); everything else is reported. Effects outside the table (plug-ins, Lumetri, …) are not converted | `crates/aep` tests (`tests/container`, `tests/import`, `tests/export`) |
|
||||
| XML project `.aepx` | R/W | partial | none | Read into the same chunk tree as `.aep` by `crates/aep` (`read_aepx`; mapping in [aep-format.md](aep-format.md) section 1), so `file.importAep` imports it with the same coverage as `.aep`; not written | `crates/aep` `aepx` tests (container and import) |
|
||||
| Templates `.aet` / `.aetx` | R/W | none | none | Own `.ectemplate` (Home ▸ Templates, Save as Template) | `templates.rs` tests |
|
||||
| Motion Graphics template `.mogrt` | W (and R in Premiere) | none | none | Own `.ectemplate` Essential Graphics templates; FilmCraft rejects our `.fcgt` export (#611) | `essential.rs` tests |
|
||||
@@ -41,7 +41,7 @@ trips and renders, checked by tests, not against After Effects), **partial** (na
|
||||
| PNG | R/W | yes | yes (sequence) | good |
|
||||
| JPEG | R/W | yes | yes (sequence) | good |
|
||||
| TIFF | R/W | yes | yes (sequence) | good |
|
||||
| OpenEXR (multi-layer, Cryptomatte) | R/W | yes | yes (32-bit sequence) | good; HTJ2K compression fails (#482); a multi-layer frame decodes once for its picture and EXtractoR / Cryptomatte from the first frame on, except Media Replacement items, which are learned at their first frame, and files whose picture the `image` crate still decodes (#474); footage alone keeps only its picture's channels, though a single-part file still decompresses every channel's pixels; decoded channels are held as f32 |
|
||||
| OpenEXR (multi-layer, Cryptomatte) | R/W | yes | yes (scan-line sequence, half or 32-bit float, every compression but HTJ2K) | good; written files carry a `chromaticities` attribute for the output space's primaries and white; HTJ2K compression fails (#482); a multi-layer frame decodes once for its picture and EXtractoR / Cryptomatte from the first frame on, except Media Replacement items, which are learned at their first frame, and files whose picture the `image` crate still decodes (#474); footage alone keeps only its picture's channels, though a single-part file still decompresses every channel's pixels; decoded channels are held as f32 |
|
||||
| Photoshop PSD / PSB | R (footage, composition) / W (layers, sequence) | yes (layers, groups, masks, styles, smart objects) | Save Frame As ▸ Photoshop Layers; no Photoshop sequence output | good, from Adobe's published spec |
|
||||
| Illustrator `.ai`, PDF, EPS | R | yes (vector, Continuously Rasterize, Create Shapes; Composition and Composition - Retain Layer Sizes, #1536) | — | partial: no JPX / JBIG2 images, no Illustrator procset EPS |
|
||||
| SVG | R | yes | — | good; SVG markup on the clipboard pastes as a shape layer (26.3, #738); Illustrator's own clipboard data (PDF/AI) doesn't |
|
||||
@@ -100,6 +100,10 @@ trips and renders, checked by tests, not against After Effects), **partial** (na
|
||||
|
||||
H.264 MP4, HEVC MP4, AV1 MP4, ProRes MOV, uncompressed 8-bit RGB / RGB + Alpha MOV (`raw `, + PCM;
|
||||
#372), WebM (VP9 or AV1 + Opus), PNG / JPEG / TIFF / OpenEXR / Targa sequences, animated GIF, WAV, AIFF.
|
||||
OpenEXR sequences are scan-line RGBA files in half or 32-bit float (default) with None, RLE, ZIPS,
|
||||
ZIP (default), PIZ, PXR24, B44, B44A, DWAA or DWAB compression; PXR24 on float, B44 / B44A on half
|
||||
and DWAA / DWAB are lossy (#2911). No tiled or multi-part output, no HTJ2K (the `exr` crate can't
|
||||
write it).
|
||||
Missing against After Effects + Media Encoder: 16-bpc uncompressed, AVI and lossless-compressed
|
||||
single-file video (Animation / PNG codecs), DPX / Photoshop sequences, MXF, DNxHR, hardware
|
||||
encoders ([hardware-parity.md](hardware-parity.md)), and rendering safely over an existing file
|
||||
@@ -138,7 +142,7 @@ check and a write still require exporter-side revalidation.
|
||||
|
||||
## Estimate
|
||||
|
||||
**≈ 130–230 h**: `.aep` / `.aepx` 80–160 h (import of the common scope is in `crates/aep`; export is not built; the rest is in [aep-import.md](aep-import.md)), wiring TGA / DPX /
|
||||
**≈ 130–230 h**: `.aep` / `.aepx` 80–160 h (import and export of the common scope are in `crates/aep`; the rest is in [aep-import.md](aep-import.md)), wiring TGA / DPX /
|
||||
HDR / MXF / AVI / DNx import and DPX output 10–20 h, FBX animation and skinning, USD and Substance 20–40 h,
|
||||
relinking 6–12 h, `.mogrt`-compatible templates 15–30 h (partly parallel with the rest).
|
||||
|
||||
|
||||
+10
-1
@@ -131,7 +131,16 @@ channels alone.
|
||||
|
||||
The Cryptomatte effect keys objects by their Cryptomatte ID passes, with the names from the
|
||||
file's manifest. Pick them with the eyedropper next to its Selection, then click in the
|
||||
Composition viewer (Shift adds, Alt or Ctrl removes).
|
||||
Composition viewer (Shift adds, Alt or Ctrl removes). Matte Only shows the matte as grey in
|
||||
RGB over full alpha, as After Effects' Cryptomatte plug-in does; projects saved before that
|
||||
change keep their matte in alpha as well. Its Layer popup lists the Cryptomatte
|
||||
layers the footage has (a file with only `CryptoMaterial` shows just that), and the standard
|
||||
`CryptoObject`, `CryptoMaterial` and `CryptoAsset` when the layer has no Cryptomatte footage. The
|
||||
stored Layer value is unchanged for those three, so older projects load as they were while each kind appears once. With
|
||||
several layers of one kind (`A.CryptoObject`, `B.CryptoObject`) the first keeps the standard
|
||||
value; the others, like any other layers, are stored as 3 and up in the file's name order (so a value of 3 reads another layer in
|
||||
another file). The popup lists them in Effect Controls and the timeline; scripts and
|
||||
`prop.get` still see the three standard options.
|
||||
|
||||
## Replacing and reloading
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Where EffectCraft falls short
|
||||
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-30 · **Change:** minor (gap 4 closed: every pull request and push to `main` runs `cargo xtask ci` in CI, #2204; earlier change: gap 25: an AccessKit audit test and a theme contrast test, Effects & Presets, Settings and Render Queue named and keyboard-operable, #1335; earlier change: path-editing layer bounds, #1220; issue, PR and catalog references gap 21: percentage guides and effect colour labels in; earlier change: re-audited against `upstream/main` 470ea7a3; ranked gap list from the 10 October re-measure otherwise unchanged) · **Target:** Adobe After Effects 2026 (26.5.0.89)
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-30 · **Change:** minor (gap 25, second slice: Info, Flowchart, Script Console and the 3D Camera Tracker block named, described and keyboard-operable, #1335; earlier change: gap 4 closed: every pull request and push to `main` runs `cargo xtask ci` in CI, #2204; earlier change: gap 25: an AccessKit audit test and a theme contrast test, Effects & Presets, Settings and Render Queue named and keyboard-operable, #1335; earlier change: path-editing layer bounds, #1220; issue, PR and catalog references gap 21: percentage guides and effect colour labels in; earlier change: re-audited against `upstream/main` 470ea7a3; ranked gap list from the 10 October re-measure otherwise unchanged) · **Target:** Adobe After Effects 2026 (26.5.0.89)
|
||||
|
||||
An honest assessment of how far EffectCraft is from being a real replacement for After Effects,
|
||||
and the work that closes the gap. This document is meant for contributors and agents choosing
|
||||
@@ -81,7 +81,7 @@ are grouped by area; the issue tracker has the rest.
|
||||
| 22 | **Encoder efficiency** | HEVC / AV1 have no B-frames, multi-reference or SAO / CDEF; Opus no FEC / DTX; single-file intermediates are 8-bit uncompressed QuickTime only (#372 closed by #778: no 16-bpc, AVI or lossless-compressed codec); #325 render over existing file | Larger files than After Effects + Media Encoder | 20–40 h | G6 · [file-format-parity.md](file-format-parity.md) |
|
||||
| 23 | **Essential Graphics interchange** | Templates are `.ectemplate`, not `.mogrt`; #338 and #611 closed | Premiere editors can't use our templates | 15–30 h | — · [file-format-parity.md](file-format-parity.md) |
|
||||
| 24 | **Timeline depth requests** | #601 expand precomps in place, #602 reorderable render stack, #604 guide-only effects, #425 keyframe confusion (#355 and #428 closed) | Power-user speed | 15–30 h | G2 · [ui-parity.md](ui-parity.md) |
|
||||
| 25 | **Accessibility** | AccessKit is enabled in eframe. `ui_accessibility_audit` walks the AccessKit tree and counts unnamed or roleless controls per panel; Project (#776), Effects & Presets, Settings (every page) and the Render Queue with its Templates dialog are at zero and held there, with named, stateful nodes and arrow-key, Home / End, Enter and Space operation of their lists (#1335). The other 30 or so panels are counted but not fixed (the Timeline alone has about 55 unnamed controls; the baseline in the test shows each). `ui_contrast` checks the theme tokens against WCAG 2: 13 of 48 pairs fall short (primary-button text, error text, control borders, the whole Light theme's warning colour). **No screen reader has been run on any panel**: the tree is checked, not how NVDA, VoiceOver or Orca speak it. Not covered: keyboard-only operation of the other panels, focus order across panels, colour-blindness, text size, reduced motion. App shortcuts pause while any widget has keyboard focus (egui), so Ctrl+Z does nothing with a list row focused | Excludes users; After Effects supports screen readers poorly too | 12–25 h (first slice, about 3 h, done) | G9 · [ui-parity.md](ui-parity.md) |
|
||||
| 25 | **Accessibility** | AccessKit is enabled in eframe. `ui_accessibility_audit` walks the AccessKit tree and counts unnamed or roleless controls per panel; Project (#776), Effects & Presets, Settings (every page), the Render Queue with its Templates dialog, Info, the Flowchart, the Script Console and the 3D Camera Tracker block of Effect Controls are at zero and held there, with named, stateful nodes, arrow-key, Home / End, Enter and Space operation of their lists and graph (#1335). Panels that only paint (Info, the tracker's status line) expose what they show as read-outs, and the Script Console's output as a log; each fixed scenario must show a minimum number of controls or read-outs, so zero cannot mean "looked at nothing". The other 25 or so panels are counted but not fixed (the Timeline alone has about 55 unnamed controls; the baseline in the test shows each); the Lumetri Scopes, Tracker, Layer, Waveform (a Timeline drawing) and the panels sharing `panel_kit` were left for when the open PRs on their files land. The shared-loop panels squeezed to nothing by the panels opened before them (Scopes, Media Browser, Metadata, Content-Aware Fill) read "zero" in the audit without being looked at. `ui_contrast` checks the theme tokens against WCAG 2: 13 of 48 pairs fall short (primary-button text, error text, control borders, the whole Light theme's warning colour). **No screen reader has been run on any panel**: the tree is checked, not how NVDA, VoiceOver or Orca speak it. Not covered: keyboard-only operation of the other panels, focus order across panels, colour-blindness, text size, reduced motion. App shortcuts pause while any widget has keyboard focus (egui), so Ctrl+Z does nothing with a list row focused | Excludes users; After Effects supports screen readers poorly too | 10–22 h (two slices, about 6 h, done) | G9 · [ui-parity.md](ui-parity.md) |
|
||||
|
||||
## Where we're going: workstreams in priority order
|
||||
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
# UI parity
|
||||
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-18 · **Change:** minor (accessibility row: an AccessKit audit and a contrast test, three more panels named and keyboard-operable, #1335; earlier changes: shape group Contents row elided; path-editing layer bounds #1220; closed issue references re-audited against `upstream/main` 470ea7a3) · **Target:** Adobe After Effects 2026
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-18 · **Change:** minor (accessibility row, second slice: Info, Flowchart, Script Console and the 3D Camera Tracker block named and keyboard-operable, #1335; earlier changes: accessibility row: an AccessKit audit and a contrast test, three more panels named and keyboard-operable, #1335; shape group Contents row elided; path-editing layer bounds #1220; closed issue references re-audited against `upstream/main` 470ea7a3) · **Target:** Adobe After Effects 2026
|
||||
|
||||
How closely EffectCraft's interaction matches After Effects: tools, handles, snapping, nudging,
|
||||
modifier keys, shortcuts, drag behaviour, numeric entry, panels, context menus and feel. Evidence
|
||||
@@ -20,7 +20,7 @@ feel and platform differences, and the 26.x interface changes are not in yet.
|
||||
| Timeline | Switches, modes, parent & link, twirl-downs, reveal shortcuts (U, UU, P, S, R, T, A, E, M, F, Alt+Shift…), pick whips, graph editor, work area, markers | 75% | `ui_keyframes`, `ui_timeline_*`; expand precomps in place (#601); #456, #457, #569 and #355 (Graph Editor jitter and buttons, negative rotation, bulk keyframing) are closed; Proportional Scrubbing (26.2: Ctrl+Alt / Cmd+Opt scrub of hot text spreads the change over the selected layers by selection order, #770) matched for layers selected as a whole, not yet for individually selected properties; every layer has the Video switch, nulls, cameras and lights included (a hidden null, camera or light has no box or handles in the viewer and isn't picked there; other selected hidden layers keep their layer controls and paths, #1769); a row control (a switch, the label swatch, a Modes cell) clicked on one of several selected layers acts on the whole selection, with the layers the switch doesn't apply to left out (#1849); a shape group's inner Contents (After Effects' elided Vectors group) draws no row — the group's path, fill/stroke and Transform list directly under it |
|
||||
| Composition viewer | Selection, handles with direction-aware cursors, rulers, guides, grids, snapping, channels, exposure, region of interest, snapshots, 3D views, Spacebar hand | 70% | `ui_viewer`, `ui_viewers`; Cmd/Ctrl +/− doesn't zoom (#570); anchor point grab on Linux (#492); #352 (snapping) is closed; no 3D transform gizmos (#392, #347); percentage guides (26.5) exist as View ▸ Add Percentage Guide and Convert Guides to Percentage / Pixels (a guide stores a percent of the comp, so it follows comp-size changes; behaviour inferred from the release note, not compared with After Effects); Copy Frame to Clipboard (26.3) is in the Composition menu and the viewer's context menu |
|
||||
| Tools panel | All After Effects tools; long-press flyouts; Tool Creates Shape / Mask; Fill / Stroke options | 80% | #213, #227 fixed; #553 (pen and vertex tools on motion paths) is closed |
|
||||
| Nudging and modifiers | Arrow nudge (1 px, Shift 10 px), Alt/Option duplicates and slips, Shift constrains | 75% | #290 nudging fixed; #491 (Linux multi-layer edits) is closed |
|
||||
| Nudging and modifiers | Arrow nudge (1 px, Shift 10 px), Alt/Option duplicates and slips, Shift constrains | 75% | #290 nudging fixed; #2968 Alt-drag in the viewer duplicates the dragged layers; #491 (Linux multi-layer edits) is closed |
|
||||
| Numeric entry | Scrubbable values, typed entry with Tab / Shift+Tab, angle revolutions, Escape cancels, non-finite rejected | 85% | `ui_angles`, `ui_inline_edit` (#93, #141, #361); rename arrows on Linux (#488) |
|
||||
| Effect Controls | After Effects' twirl-downs, angle dial, point crosshair, eyedropper, curves/levels editors, effect menu on right-click | 70% | `ui_effect_controls`, `ui_fx_editors`; 26.5 rebuilt this panel with icon controls and effect colour labels: labels exist (a colour bar on the effect header and on the effect's Timeline row, right-click Label menu, `effect.setLabel`), the rebuilt controls are not matched |
|
||||
| Context menus | Layer, property, Project, Timeline empty area, work area, labels with colours | 80% | `ui_blank_context_menus`, `ui_project_rows`; #227, #290, #407 fixed; a comp's Project row menu has Composition Settings… (#1058); layer context menu has Layer Styles (#1611) |
|
||||
@@ -30,7 +30,7 @@ feel and platform differences, and the 26.x interface changes are not in yet.
|
||||
| Preview feel | Spacebar / Numpad 0 previews, RAM preview, cache bars, adaptive resolution | 55% | `ui_ram_preview`, `ui_live_preview`; stutter (#595, #304); scrubbing lag (#329, closed by #784) |
|
||||
| Home screen, Learn, templates | Home with recent projects, template gallery, interactive Learn tutorials | 70% | Layout nitpicks (#581); Home literals untranslated (#574) |
|
||||
| Quick Apply (26.2) | Search to apply effects, presets or commands | partial | A command palette exists; not After Effects' Quick Apply |
|
||||
| Accessibility | AccessKit enabled; `ui_accessibility_audit` counts unnamed or roleless controls per panel: Project (#776), Effects & Presets, Settings (all pages) and Render Queue with Templates are at zero, with names, roles, selected / open / checked state and arrow-key, Home / End, Enter, Space operation (#1335); `ui_contrast` checks theme tokens against WCAG 2 | 20% | No screen reader has been run on anything; the other panels are counted, not fixed (Timeline ≈ 55 unnamed controls); 13 of 48 token pairs are below WCAG 2 (primary button text, error text, control borders, Light theme warning colour); focus order across panels, colour-blindness and reduced motion are unchecked; app shortcuts pause while a widget has keyboard focus |
|
||||
| Accessibility | AccessKit enabled; `ui_accessibility_audit` counts unnamed or roleless controls per panel: Project (#776), Effects & Presets, Settings (all pages), Render Queue with Templates, Info, Flowchart, Script Console and the 3D Camera Tracker block are at zero, with names, roles, selected / open / checked state and arrow-key, Home / End, Enter, Space operation (#1335); painted-only text (Info, status lines) and the console output are read-outs and a log in the tree; `ui_contrast` checks theme tokens against WCAG 2 | 20% | No screen reader has been run on anything; the other panels are counted, not fixed (Timeline ≈ 55 unnamed controls; Scopes, Tracker, Layer and the Waveform have open PRs on their files and are untouched; their on-screen scopes and waveform have no text description yet); 13 of 48 token pairs are below WCAG 2 (primary button text, error text, control borders, Light theme warning colour); focus order across panels, colour-blindness and reduced motion are unchecked; app shortcuts pause while a widget has keyboard focus |
|
||||
| UI scale | Settings ▸ Appearance ▸ UI Scale 75–200% | beyond | After Effects follows the system's scaling (#284) |
|
||||
|
||||
Path editing (#1220): the Composition viewer suppresses a selected layer’s transform box while
|
||||
@@ -42,7 +42,7 @@ cover shape paths and masks, corner-vertex dragging and undo, and layers without
|
||||
## Estimate
|
||||
|
||||
**≈ 60–110 h**: the open interaction issues above (25–45 h, about 0.2–0.5 h each with a scripted
|
||||
test), 3D gizmos 8–15 h, the 26.x interface changes 10–20 h, an accessibility pass 12–25 h (the first slice, three panels and the audit, is done). Checking feel against After Effects needs the owner's Mac with After
|
||||
test), 3D gizmos 8–15 h, the 26.x interface changes 10–20 h, an accessibility pass 10–22 h as in [gaps.md](gaps.md) gap 25 (two slices, seven panels and the audit, are done). Checking feel against After Effects needs the owner's Mac with After
|
||||
Effects running; most of the rest parallelises.
|
||||
|
||||
## Revision history
|
||||
@@ -50,6 +50,7 @@ Effects running; most of the rest parallelises.
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-18 | minor | A shape group's inner Contents (After Effects' elided Vectors group) draws no row: the group's path, fill/stroke and Transform list directly under the group |
|
||||
| 2026-10-13 | minor | Accessibility row, second slice: Info, Flowchart, Script Console and the 3D Camera Tracker block named and keyboard-operable (#1335); no screen reader tested |
|
||||
| 2026-10-13 | minor | Accessibility row: AccessKit audit and contrast tests, Effects & Presets, Settings and Render Queue named and keyboard-operable (#1335); no screen reader tested |
|
||||
| 2026-10-13 | patch | Layer bounds are suppressed during path-point editing (#1220); shape/mask interaction regressions |
|
||||
| 2026-10-12 | minor | Closed issues (#302, #320, #491, #511, #553 and others) marked as closed instead of open; panels in their own windows (#486) |
|
||||
|
||||
Reference in New Issue
Block a user