mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-07 19:11:54 +00:00
* EXR export: write the output space's chromaticities attribute (#3037) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * EXR chromaticities: tests for unmanaged and PQ ProEXR, every space once Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * EXR export: note why the comp background is added before decoding (review point) CodeRabbit asked to convert Comp.background to the output space before compositing. Every output path (Image::to_rgba8_over) adds the background to the output-encoded frame as it is, so the EXR now matches the other outputs; converting would make the EXR colour differ from the PNG of the same comp. Comment only. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Format the merged code with rustfmt Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
157 lines
14 KiB
Markdown
157 lines
14 KiB
Markdown
# File format parity
|
||
|
||
> **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`,
|
||
`Resources/usd_plugins`) and its public documentation; ours from `crates/media/src/lib.rs`
|
||
(`STILL_EXTENSIONS`, `VIDEO_EXTENSIONS`, `MODEL_EXTENSIONS`, FilmCraft's `AUDIO_EXTENSIONS`),
|
||
`crates/engine/src/commands/file.rs` (`DATA_EXTENSIONS`) and `OutputFormat::ALL` in
|
||
`crates/project/src/render_queue.rs`. Video codecs come from FilmCraft's pure-Rust crates
|
||
([architecture.md](architecture.md)).
|
||
|
||
**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 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),
|
||
**none**.
|
||
|
||
## After Effects' own formats
|
||
|
||
| Format | After Effects | EffectCraft read | Write | Fidelity / notes | Tested by |
|
||
|---|---|---|---|---|---|
|
||
| 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 |
|
||
| Animation preset `.ffx` | R/W, 621 bundled | none | none | Own JSON `.ecpreset`; `.ffx` is out of clean-room scope (AGENTS.md §2) | `animation.rs` tests |
|
||
| Render Settings / Output Module templates `.ars` / `.aom` | R/W | none | none | Templates are stored in the project and settings instead | `render_templates.rs` tests |
|
||
| Scripts `.jsx` | R (run) | yes | — | After Effects object model and ScriptUI ([scripting-parity.md](scripting-parity.md)) | `crates/script` tests |
|
||
| Binary scripts `.jsxbin` | R (run) | none | — | No public specification: out of scope | — |
|
||
| Motion graphics JSON `.mgjson` | R (data) | none | none | Media Browser lists it; import accepts only `.json` / `.csv` / `.tsv` data | — |
|
||
| Captions `.aecap`, graphics `.aegraphic` | R/W | none | none | — | — |
|
||
| Native `.ecproj` | — | yes | yes | Versioned JSON; schema migrations tested; absolute footage paths, no relinking (gap 18) | engine project tests |
|
||
|
||
## Footage: stills
|
||
|
||
| Format | After Effects | Read | Write | Fidelity / notes |
|
||
|---|---|---|---|---|
|
||
| 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 (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 |
|
||
| GIF | R/W | yes | yes (animated) | good |
|
||
| BMP | R | yes | — | good |
|
||
| WebP | — | yes (still; animated WebP as footage with its frames, timing and alpha, #762) | — | beyond After Effects |
|
||
| Targa `.tga` | R/W | yes (still and sequence; alpha interpreted on import) | yes (sequence; uncompressed 24-bit RGB or 32-bit RGB + Alpha, straight or premultiplied) | good: import (#745); output reads back through it unchanged; the alpha is not labelled in the file (no TGA 2.0 extension area), so it is interpreted on import |
|
||
| DPX / Cineon `.dpx` `.cin` | R/W | yes (still and sequence; 8/10/12/16-bit luma, RGB, RGBA, both byte orders; log code values shown as stored, or converted by Interpret Footage ▸ Cineon Settings ▸ Logarithmic Conversion) | none | partial: import (#756) and Cineon Settings; no DPX output; common in film VFX |
|
||
| Radiance `.hdr` | R/W | yes (still and sequence, 32-bit linear float like OpenEXR) | none | good for import (#745); no Radiance output |
|
||
| SGI, PICT, ElectricImage, RLA/RPF images | R | none | none | Legacy; RPF camera data import exists (`keys.rpfCameraImport`) |
|
||
| Camera Raw (DNG, CR3, NEF…) | R (Camera Raw) | none | none | LightCraft has decoders that could be shared |
|
||
| HEIF / AVIF | R (platform) | none | none | — |
|
||
|
||
## Footage: video and audio
|
||
|
||
| Format | After Effects | Read | Write | Fidelity / notes |
|
||
|---|---|---|---|---|
|
||
| MP4 / MOV with H.264 | R/W | yes | yes (own encoder, + AAC) | decoder bit-exact on conformance streams (FilmCraft) |
|
||
| HEVC (H.265) | R/W | yes | yes (MP4 `hvc1`, Main / Main 10) | encoder bit-exact with ffmpeg's decode; no B-frames / SAO, larger files |
|
||
| AV1 | R | yes | yes (MP4 `av01`, WebM) | beyond After Effects' export; no B-frames / CDEF |
|
||
| Apple ProRes (422 family, 4444) | R/W | yes | yes | alpha final-run syntax checked in exported MOVs (#293); 4444 colour tag wrong (#340); no ProRes RAW |
|
||
| WebM / Matroska (VP9, AV1, Opus) | R (limited) | yes, incl. VP9 alpha | yes | beyond After Effects |
|
||
| MXF (OP1a / OP-Atom) | R/W | yes (FilmCraft's demuxer: AVC, MPEG-2 incl. XDCAM / D-10, VC-3 / DNxHD, ProRes; PCM audio) | none | partial: import (#767), checked against ffmpeg; no MXF output |
|
||
| AVI | R | yes (FilmCraft's demuxer: AVI 1.0 / OpenDML; Motion JPEG, H.264, HEVC, uncompressed; PCM, MP3, MP2, AC-3 audio) | none | partial: import (#1701), checked against ffmpeg; H.264 needs its parameter sets in the chunks (global headers and copies with an `avcC` record don't decode until storytold/filmcraft#2303), and a damaged video index loses the video (storytold/filmcraft#2302); DV, MPEG-4 Part 2 (DivX / Xvid) and other legacy codecs import but show why they have no picture; no AVI output |
|
||
| DNxHD / DNxHR | R/W | unverified (FilmCraft decodes it; reachable only inside `.mov`) | none | — |
|
||
| BRAW, R3D, ARRIRAW | R (plug-ins / SDKs) | none | none | proprietary SDKs |
|
||
| Variable-frame-rate phone video | R | unverified | — | needs a real-media corpus |
|
||
| WAV, AIFF | R/W | yes | yes (16-bit PCM) | good |
|
||
| MP3, AAC / M4A, FLAC, Ogg, Opus | R | yes | AAC and Opus inside video only | MP3 silence (#324) is closed |
|
||
|
||
## 3D models
|
||
|
||
| Format | After Effects 26 | Read | Notes |
|
||
|---|---|---|---|
|
||
| glTF / GLB | R | yes (meshes, PBR, skins, animations) | good |
|
||
| OBJ / MTL | R | yes | good |
|
||
| FBX | R (`usdFbx`) | yes (7.x binary and ASCII: meshes with normals, UVs and per-polygon materials, the Model hierarchy with pivots and pre/post rotation, Lambert/Phong materials with embedded or relative textures, axes turned to Y-up; no animation, skinning, cameras or lights; 6.x rejected) | partial |
|
||
| USD / USDZ | R (`usd_plugins`) | none | gap 13 |
|
||
| STL | R (`usdStl`) | yes (binary and ASCII, VisCAM and Magics facet colours; Z-up turned to Y-up) | good |
|
||
| Substance 3D materials `.sbsar` | R (26.0) | none | gap 13 |
|
||
|
||
## Interchange and data
|
||
|
||
| Format | After Effects | Read | Write | Notes |
|
||
|---|---|---|---|---|
|
||
| Premiere Pro `.prproj` | R (import) | none | none | Out of clean-room scope pending a decision; we interchange through FCP7 XML, FCPXML, OTIO, EDL, AAF and OMF instead (beyond After Effects) |
|
||
| Lottie JSON / dotLottie | via third-party plug-ins | yes | yes | beyond After Effects; a warnings list names what Lottie can't express |
|
||
| JSON / CSV / TSV data | R | yes (`footage()`, data layers) | — | good |
|
||
| LUTs (`.cube`, `.3dl`, `.csp`, `.spi1d/3d`, CDL) | R | yes (Apply Color LUT, OCIO effects) | — | Apply Color LUT bug (#514) is closed |
|
||
| OpenColorIO configs | R (OCIO 2.5.1, ACES 2.0 in 26.5) | yes (OCIO v2 subset, Blender configs) | — | partial: no ACES 2.0 output transforms |
|
||
| Fonts (TTF, OTF, TTC, variable) | R | yes | — | good |
|
||
| Mocha shapes | R (Mocha AE) | own documented JSON | — | Mocha's format is not public |
|
||
| Vanishing Point `.vpe` | R | none | — | no public specification; the menu entry explains why |
|
||
|
||
## Render queue outputs (`OutputFormat::ALL`, 14)
|
||
|
||
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
|
||
(#325).
|
||
|
||
### Render source protection
|
||
|
||
The render queue refuses an output that would replace footage, an attached proxy, or the
|
||
project file. It checks every output module and segment before dispatching the item. A refused
|
||
item is marked Failed with its reason; other valid queued items can still render. Render logs
|
||
are checked too. No render parameter overrides this protection.
|
||
|
||
Checks compare normalized paths and native file identities, so an existing symbolic link,
|
||
hard link, junction or Windows short name cannot disguise a source as another output file.
|
||
Source identities are indexed once per check and matching identities are verified. The index
|
||
does not keep file handles open for the lifetime of a long sequence.
|
||
|
||
Default output names move aside when they would replace a protected file. This also applies to
|
||
Create Proxy: recreating a proxy chooses a new default name and attaches the new proxy, leaving
|
||
the previous proxy file intact. An explicit output path is kept as entered and refused if it
|
||
would replace a protected file.
|
||
|
||
After Pre-render imports its result, that file is footage too. Re-queueing the same item
|
||
with the same output is refused; run Pre-render again for a fresh default output, or choose
|
||
another Output To. An item's `last_output` is not permission to replace imported footage.
|
||
|
||
Image-sequence checks inspect existing frames in the requested range. Skip Existing Files leaves
|
||
existing frames alone. If a frame number appears in a folder name, the checker probes each
|
||
frame separately and refuses ranges over 100,000 frames instead of silently checking a prefix.
|
||
Choose a fixed output folder or a shorter range for those exports.
|
||
|
||
This guard does not introduce an overwrite flag, confirmation dialog, or changed replacement
|
||
defaults for the CLI, scripts or MCP. Replacement consent and staging ordinary output files
|
||
belong to the exporter. The source check runs before export; filesystem changes between that
|
||
check and a write still require exporter-side revalidation.
|
||
|
||
## Estimate
|
||
|
||
**≈ 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).
|
||
|
||
## Revision history
|
||
|
||
| Date | Change | Summary |
|
||
|---|---|---|
|
||
| 2026-11-05 | minor | ProRes encoder pin and export regression for final alpha runs; native macOS decode validated, Resolve still unverified (#293) |
|
||
| 2026-10-30 | minor | Render-queue source-protection behavior and limits consolidated here from the standalone page |
|
||
| 2026-10-12 | minor | #324 and #514 marked closed; open `.aep` PRs #251 and #860 noted |
|
||
| 2026-10-10 | major | First file format parity document, from the After Effects 26.5 bundle and this repository's import/export registries |
|