# 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` 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 ` 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":,"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).