Commit Graph
79 Commits
Author SHA1 Message Date
Valentin KleinandFredApps a4bde72bca .aep: import twice as fast, faster export (#3302)
* aep: a layer's markers and Source Text are read where they are stored, not searched for (#3276)

A layer's marker list is a direct child of its root tdgp, and its Source Text record
btds a direct child of the ADBE Text Properties group. Markers walked the whole layer
subtree twice per layer (most layers have no marker list, so the walk always ran to the
end), and the three text-document accessors walked it once each.

* aep export: build the prototype template once, sort names with cached keys (#3277)

The template every export clones from is immutable; it is now built by the
first export of the process and kept (about 0.93 MB). The name sort builds
each natural key once instead of on every comparison. Root solids move into
the Solids folder instead of being copied. The two key-list rewrites share one
emit-local helper and match names compare as bytes instead of a decoded String.
Output is byte-identical.

* aep container: size sibling lists, hex and write output once; borrow plain .aepx attributes (#3275)

- read.rs: count the chunk headers of a sequence first and allocate the
  children Vec once (bounded by the input and by the chunk limit).
- aepx.rs: decode_hex sizes its output once; attribute values without
  an entity are borrowed instead of copied through decode_entities.
- typed.rs: PropGroup::get compares match names without building a
  String (same result as the lossy compare, invalid UTF-8 included).
- write.rs: reserve the exact output size up front (try_reserve, so a
  hand-built tree cannot abort on the allocation).
- read.rs, settings.rs, values.rs: use the shared bytes.rs readers.

Output is identical on the corpus (tree, written bytes, import).

* aep container: review fixes (#3275)

- read.rs: reserve with try_reserve_exact, so a raised chunk limit on a
  small target cannot abort before parsing; the capped reservation and a
  deep, retry-heavy nest of broken lists are pinned by tests.
- write.rs: a tree past the 32-bit container size fails with TooLarge
  before anything is serialised.
- aepx.rs: drop an unneeded local and lifetime annotation.

* aep export: note what the concurrency test covers and why a failed template build is not kept (#3277)

* aep: text documents are read without a String per token, defaults built once (#3276)

TextDocument::parse reads a borrowed tree: names, bare words and strings without
escapes point into the body, keys are borrowed when they are /names, numbers parse from
the borrowed bytes, and dictionaries and arrays are moved into vectors of their exact size
when they close. The style and paragraph defaults are built once instead of per document.
parse_text_dict keeps its public TextValue result (the borrowed tree turned owned).

The old owning parser stays as a test-only oracle: a differential test compares both on
40 000 generated bodies (every key form the owned keys canonicalise, non-UTF-8 names,
escapes, unclosed strings, non-finite numbers), every cut of a realistic document and
nesting around the depth limit.

* aep: text-dict test builds its prefix without repeat(1) (#3276)

* aep: an escaped text string drops its byte order mark without a range drain (#3276)

* aep export: one key-time loop, key-list rewrite, tdmn decoder and byte writer for the listed modules (#3278)

In masks, shapes, styles, synth, fxsynth, base, camlight, skeleton,
textanim and tree (effects.rs, plan.rs and emit.rs keep their copies for
the follow-up work):
- New export/keys.rs: the key-time loop (timed_keys), the key-list rewrite
  (fill_key_list, set_key_list, replace_cdat) and the spatial key record,
  shared by masks, shapes and styles.
- One tdmn decoder (synth::tdmn_name, lossy UTF-8 up to the first NUL, as the
  importer reads it) and one encoder (synth::tdmn) instead of the copies in
  masks, styles, shapes, textanim, camlight and skeleton.
- One byte writer: tree::put, which errors. base::bytes, synth::tdb4,
  synth::key_list, fxsynth::pard, masks::shph and the keyed path records write
  through it; their fixed layouts keep the infallible signatures behind
  tree::fixed, which logs, and a test builds every layout and requires no error.
- camlight::put_group uses the shared group split (tree::split_group), which
  errors on a match name without a node instead of stopping quietly.

Export output is byte-identical on the corpus and the synthetic fixtures.

* aep export: pin the key-time loop's report, name the layout in the base::bytes log (#3278)

* aep export: review fixes for #3278

- tree::Fixed replaces tree::fixed: a fixed layout writes every field that
  fits (as the old code did) and keeps the first error; finish() logs it
  once with the layout's description and counts it per thread, so the
  every-layout test cannot race with other tests.
- base::bytes logs one line naming its length and fields.
- masks::build_atom errors when the prototype has no Mask Shape instead of
  dropping the path of every mask quietly.
- log sorted into the dependency list.

* aep: review fixes for #3276 text documents and markers

text_document reads the first btdk in the Source Text record, the documents
text_document_count counts. The old slot checks (same, is_neutral, undecoded_fields)
join the test oracle and are compared on 20 000 generated slot dictionaries; mixed
dictionary/array chains and a document left open at the depth limit join the parser
differential. An escaped string's byte order mark is drained without a copy.

* aep: remove test-only container accessors, share report and time-base code (#3281)

- Remove 35 public container accessors that only tests called (header,
  camera/light, keys, footage, masks, shapes, version) and the dead
  StyleMap::switch_name. Tests now check the importer's own reading of the
  same bytes, or the decoder it uses; three import tests gain the values
  the removed checks covered.
- Import and export reports share one loss entry (Loss<C>) with record,
  total, count and summary; the JSON shape is pinned for both.
- Collector::lose_with builds a detail only below the detail cap (callers
  switch later).
- The export uses the container's TimeBase; one NTSC table serves import
  and export; TimeBase::frames saturates like units instead of returning 0.

* aep: review fixes for #3281: NTSC table beside TimeBase, real-path NTSC round trip, light and camera defaults on import, doc wording

* aep: drop a local check tool committed by mistake (#3281)

* aep export: sort keys as one compact byte string, bounded by the name (#3277)

The cached natural keys were Vecs of (bool, String, u128) segments: a 255-byte
name of alternating letters and digits took about 14 KB, held for every item at
once. Each key is now one byte string whose byte order is the natural order,
at most 5 bytes per name byte. Order and output are unchanged.

* aep container: check bdata digits and size before reserving (#3275)

decode_hex reserved the decoded size before it knew the value decodes,
so a large bdata with a bad digit reserved half its length first. It now
refuses a value larger than a chunk's 32-bit size field (TooLarge), checks
every digit in one vectorisable pass, and only then reserves, with
try_reserve_exact so a value that does not fit in memory is an error
instead of an abort.

* aep export: the every-layout test counts from a baseline, not from zero (#3278)

* aep export: correct the order the natural sort key doc gives for equal numbers (#3277)

* aep: the format notes name the accessors that remain (#3281)

* aep: the format notes name the 1 x 1 mask scale of text and shape layers (#3281)

* aep: read imports through a flat tree index (#3300)

Import reads an .aep through TreeIndex, one depth-first array of chunks that
borrows the file's bytes, instead of building and freeing the owned Chunk tree
(one Vec per list). The parser mirrors read::Parser step for step: the same
chunk count order against max_chunks, the same depth limit and opaque
fallbacks, the same errors. The typed view reads through Node, a handle to a
chunk of either tree, so the accessors work unchanged on both; .aepx keeps its
owned tree. Chunk, parse and write stay for export and editing.

The array starts small and doubles, never past what the input can hold or the
chunk limit allows; a failed allocation is TooLarge, a tree past u32 indices
TooManyChunks.

* aep export: the time remap and Transform keys use the shared key-list and match-name helpers; doc comments for the audit's functions (#3300)

* #3300: import .aepx through the flat index

The .aepx reader no longer builds the owned chunk tree element by
element. It decodes every body (hex, text, alias JSON, numbers) into
one buffer and records the chunks as depth-first entries whose bodies
are ranges of that buffer; TreeIndex::from_drafts turns them into the
index the importer reads, which borrows the buffer. read_aepx converts
that index into the owned tree in one pass.

The tree and the errors are those of the previous reader, which the
tests keep as the reference: generated and damaged documents under
seven limit sets, invalid UTF-8, hostile input, the fixture directory,
and imports through both. Bound tests pin the chunk list to the input
and the limit, and check that dropped content leaves nothing behind.

* #3300: read_aepx keeps its own owned-tree reader

read_aepx, read_aepx_with_limits and parse_any build the owned chunk
tree element by element again, as before the .aepx index, instead of
converting the index (which held the decoded buffer and the owned tree
at the same time). Import keeps the index reader. The tests now
compare the two production readers.

* #3300: test keys that round to the same time

* aep: compare the destination projects in the index-equivalence tests

both() and both_aepx() kept only the ImportResult of each path; they now keep
both destination Projects and assert structural and serialized equality, so a
layer or property value regression cannot pass on equal ids and reports.

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-13 00:54:29 +01:00
Valentin Klein 10a34b854f .aep import/export: text animator Stroke Opacity and Source Text expressions (#3170) (#3305)
* #3170: .aep import of the text animator Stroke Opacity and the Source Text expression

ADBE Text Stroke Opacity maps to the animator's strokeOpacity (percent, as
After Effects shows it). The Source Text expression is imported like any
property's (Utf8 text, tdb4 bytes 119/120); one whose flags give no state
is still reported. New committed fixture text-synthetic.aep, made in After
Effects 2026 with the working space None.

* #3170: .aep export of the Source Text expression and the text animator Stroke Opacity

The Source Text expression is written enabled or disabled as After Effects
stores it (tdb4 bytes 119/120, Utf8 after cdat); a disabled one is no longer
dropped without a report. strokeOpacity is written as ADBE Text Stroke
Opacity with the record After Effects wrote in text-synthetic.aep. A test
exports the fixture and imports it again.

* #3170: doc comments on the functions this change touches
2026-11-13 00:16:37 +01:00
Valentin Klein 8028f46808 .aep import: a test pins the Orientation expression import (#3087) (#3313)
* .aep import: Photoshop layers, placeholder effects, shape direction and Orientation expressions (#3087)

- A footage item showing one Photoshop layer shows that layer (record index, bounds and name
  from its opti) and is named <layer>/<file>; an adjustment layer shows white where it applies
  (opt-in through SourceLayer::adjustment_white, the direct Photoshop import is unchanged).
- Effects without a counterpart arrive as placeholder effects with their parameters, so
  expressions reading them keep working; pseudo effects are no loss, a Dropdown Menu Control
  becomes EffectCraft's. Slider ranges are checked, sizes are bounded.
- The loss prompt shows every detail line the report keeps and how many more there are.
- Shape direction and gradient highlight records in the default state are no loss.
- An expression on Orientation is imported with its value or keys.

* .aep import: report placeholder records and popup defaults that borrowed definitions leave open (#3087)

A value record the borrowed definitions do not name is reported instead of dropped, and a
borrowed popup without a record reports that its default item is not known (the first is used).

* Merge upstream/main into aep-import-bugs-3087
2026-11-13 00:15:49 +01:00
arnav-hiwarkar 98d82ae761 #3470: Turbulent Displace: Random Seed takes After Effects' range
Random Seed takes 0..100000, After Effects' recorded valid range (and what
the other effects' Random Seed already takes). The CPU and GPU hash it the
same way with wrapping u32 arithmetic, so any seed costs the same.
Default and slider are unchanged.
2026-11-12 15:47:15 +05:30
arnav-hiwarkar 74bcb30540 #3471: Transform: Scale Height and Scale Width take After Effects' range
Scale Height and Scale Width take -30000..30000 %, After Effects' recorded
valid range in the same units. The CPU and GPU share the matrix
(transform_matrix_px) and the buffer growth, which is capped at 16 Mpx
(transform_grow; the CPU test already renders 100000 %), so larger scales
stay bounded.
Default and slider are unchanged.
2026-11-12 15:47:15 +05:30
arnav-hiwarkar 3bd7de6ad8 #3475: Beam: Starting and Ending Thickness take After Effects' range
Starting and Ending Thickness take 0..4000, After Effects' recorded valid
range in the same units (pixels). The CPU and GPU renderers draw the beam
per pixel from its distance to the segment, within the layer's buffer, so
a thicker beam costs the same.
Default and slider are unchanged.
2026-11-12 15:47:15 +05:30
arnav-hiwarkar c4cd9f1c2e #3477: Scatter: Scatter Amount takes After Effects' range
Scatter Amount takes 0..1000, After Effects' recorded valid range in the
same units (pixels); the slider stays 0..127. The CPU and GPU renderers move
each pixel by one random offset of up to the amount, so a larger amount
costs the same; samples outside the layer are transparent.
Default and slider are unchanged.
2026-11-12 15:47:15 +05:30
arnav-hiwarkar a08240155c #3474: Radial Wipe: Feather takes After Effects' range
Feather takes 0..32000, After Effects' recorded valid range in the same
units. The CPU and GPU renderers evaluate the feather per pixel in closed
form (a Gaussian of the distance to the wedge, radial_wipe_alpha), so a
wide feather costs the same and stays finite.
Default and slider are unchanged.
2026-11-12 15:47:14 +05:30
arnav-hiwarkar 29c76bb6b4 #3472, #3473: Venetian Blinds: Width and Feather take After Effects' ranges
Width takes 1..32000 and Feather 0..32000, After Effects' recorded valid
ranges in the same units (pixels). The work stays bounded: the renderers
floor the width at 1 buffer pixel, sum at most 40 Gaussian terms per
pixel, and above 8 widths the feather is the even veil
(venetian_cover; the GPU hands that case to the CPU, as before).
Default and slider are unchanged.
2026-11-12 15:47:14 +05:30
arnav-hiwarkar 85f47ec072 #3476: Grid: Border takes After Effects' range
Border takes 0..2000, After Effects' recorded valid range in the same units
(pixels). The CPU and GPU renderers compute the border per pixel in closed
form, so a wider border costs the same; one wider than the cells covers
them.
Default and slider are unchanged.
2026-11-12 15:47:14 +05:30
arnav-hiwarkar ce4db78896 #3480: Change Color: Hue Transform takes After Effects' range
Hue Transform takes -1800..1800 degrees, After Effects' recorded valid range
in the same units. The CPU and GPU renderers wrap the hue (rem_euclid /
fract_euclid), so five turns render like the hue they end on, at the same
cost.
Default and slider are unchanged.
2026-11-12 15:47:14 +05:30
arnav-hiwarkar 244f9bd846 #3483, #3484: Noise HLS: amounts and Grain Size take After Effects' ranges
Hue, Lightness and Saturation go up to 1000 % and Grain Size down to 0 (it
keeps its wider maximum, 100), After Effects' recorded valid ranges in the
same units. Noise HLS Auto shares the controls and widens with them.
Defaults and sliders are unchanged.

The renderers need no change: hue wraps, lightness and saturation clamp per
pixel after the noise is added, and Grain Size is floored at 0.1 px on the
CPU and the GPU, so the new values cost the same and stay finite.

The amounts were written as "percent" in keyframe text because their range
was 0..100, and Grain Size would become "percent" at 0..100; UNITS rows keep
the units they had.
2026-11-12 15:46:48 +05:30
arnav-hiwarkar 363a800d3f #3478, #3479: 4-Color Gradient: Blend and Jitter take After Effects' ranges
Blend goes up to 10000 (it keeps its minimum of 1, below After Effects' 5)
and Jitter up to 500 %, After Effects' recorded valid ranges in the same
units. Defaults and sliders are unchanged.

The CPU and GPU renderers capped Jitter's dither at 100 %, so higher values
would be stored but render as 100. Both now take it from one function,
four_color_jitter, which keeps the dither linear up to 500 %. Blend 10000
is a finite softening (Blend squared) at the same cost.

Jitter was written as "percent" in keyframe text because its range was
0..100; a UNITS row keeps that unit.
2026-11-12 15:14:34 +05:30
arnav-hiwarkar 39a3b565d8 #3487, #3488, #3489, #3490, #3491: Levels (Individual Controls): levels and gamma take After Effects' ranges
Input/Output Black and White of every channel take -10000..10000 and Gamma
goes down to 0 (it keeps its wider maximum, 10), After Effects' recorded
valid ranges in the same units (the .aep mapping is Conv::Plain). Defaults
and the 0..1 / 0.1..3 sliders are unchanged.

Gamma 0 renders as the existing 0.01 floor, an exponent of 100, so an HDR
value a few times Input White would overflow f32 and an infinite curve
times an empty output range is NaN. The curve is now bounded at +-1e6 on
the CPU (Lv::apply) and the GPU (fxt_lv), so the output stays finite over
the whole range; values below the bound are unchanged.

The engine regression (tests/effect_ranges.rs) sets both ends of each row
through prop.set, checks undo/redo, save and reopen, and that a project
saved with the narrower ranges opens with the new ones and its values.
2026-11-12 15:11:21 +05:30
arnav-hiwarkar 216fc6052d #3470, #3471, #3472, #3473, #3474, #3475, #3476, #3477, #3478, #3479, #3480, #3483, #3484, #3487, #3488, #3489, #3490, #3491: guard effect hard ranges against After Effects' recorded ranges
A test walks every effect parameter the .aep table maps to a recorded After
Effects definition (fx_defs) and checks that the effect's hard range
(ParamUi::Slider min..max, what prop.set and the value editors clamp to)
covers After Effects' valid range, converted to EffectCraft's units the way
the importer converts values. Conv::number_scale is that conversion, now
shared by the importer and the test.

The parameters still narrower are allow-listed by issue (#3460-#3492); the
test fails on a new narrower parameter and on a row that is no longer
needed, so the list shrinks as the issues are fixed.
2026-11-12 15:05:48 +05:30
arnav-hiwarkar e7a396856d #3087: a placeholder's float slider keeps its range
It arrived as a plain number, so Effect Controls neither dragged it over its
slider range nor clamped an edit to its valid range, as After Effects does.
2026-11-12 13:54:03 +05:30
arnav-hiwarkar a2ada5a5fe #3087: count every parameter a placeholder walks, and popup names by length
A definition of parameters that are not kept, shared by many instances, was
walked again for each one without spending the budget; long popup item
names were kept per instance with each item counted once.
2026-11-12 13:14:16 +05:30
arnav-hiwarkar 0da7691d9d #3087: give a placeholder a match id that is one path segment
A pseudo effect's match name (Pseudo/0.25) was its group's match id, so a
match path to its parameters (keyframe paste, pick whip) was cut apart at
'/' and '.' and found nothing. The effect id stays the match name.
2026-11-12 13:06:51 +05:30
arnav-hiwarkar f6e2bdf6a0 Merge remote-tracking branch 'upstream/main' into fix/3087-aep-effect-placeholders
# Conflicts:
#	docs/aep-import.md
2026-11-12 13:01:53 +05:30
arnav-hiwarkar 98b8e69f7f #3087: name each effect the placeholder budget skips
A skipped instance was reported through the capped detail list, so past 20 effect losses it was only counted. It now goes through the per-effect grouping under a key of its own, so it is named like any effect without a counterpart and is not merged into a kept instance's line.
2026-11-12 11:00:10 +05:30
arnav-hiwarkar a4d0952115 #3087: stop building placeholders once the import's budget is spent
A parameter past the budget was skipped and the next one tried, so the
rest of the effect, and every later effect without a counterpart, still
read its shared definitions and built popup items. The first parameter
that does not fit now ends the budget; a later effect without a
counterpart is reported on its own line without reading its definitions.
The docs now say that a placeholder's layer parameters report their keys
and expression.
2026-11-12 10:51:32 +05:30
arnav-hiwarkar 949fdb4b08 Merge remote-tracking branch 'upstream/main' into fix/3087-aep-effect-placeholders
# Conflicts:
#	docs/aep-import.md
2026-11-12 10:39:03 +05:30
arnav-hiwarkar 14d04536aa #3087: report a Photoshop layer item as big as neither its layer nor the document
Such an item is shown on the document, which the size does not confirm; it is now reported in unsupported_items, when the opti stores the document's size.
2026-11-11 22:02:46 +05:30
arnav-hiwarkar a6fba29c3e #3087: report a Photoshop layer whose framing the item's size cannot tell
An item as big as its layer's bounds was taken for Retain Layer Sizes. When
the layer is also as big as the document but not at its corner, a
document-framed item has the same size, so it may show shifted. The opti
record holds the document's size at bytes 32 and 36 (1000, 1000 in every
record of the issue's project, the .psd's size); with it, that case is
reported. The layer still shows at its own bounds.
2026-11-11 21:42:54 +05:30
arnav-hiwarkar 0e52ca784c Merge remote-tracking branch 'upstream/main' into fix/3087-aep-psd-layers
# Conflicts:
#	crates/aep/tests/it/import/synthetic.rs
#	docs/aep-import.md
2026-11-11 21:38:03 +05:30
arnav-hiwarkar f6b4da0aeb Merge remote-tracking branch 'upstream/main' into fix/3365-aep-loss-report
# Conflicts:
#	crates/aep/tests/it/import/synthetic.rs
2026-11-11 17:19:01 +05:30
arnav-hiwarkar 6dbd751ae5 Merge remote-tracking branch 'upstream/main' into fix/3087-aep-psd-layers
# Conflicts:
#	crates/aep/tests/it/import/synthetic.rs
2026-11-11 17:18:38 +05:30
ash 284cec6e46 .aep: popup items match ignoring case outside ASCII too (#3432 follow-up) (#3447)
popup_key lowered only ASCII letters, so an item starting with a capital
outside ASCII (French "Écran") kept it and never matched the same word in
lower case. After Effects stores popup items in its UI language, and
#3432 starts matching such names through popup_alias, keyed by
popup_key. Use Unicode lower case; ASCII keys are unchanged.
2026-11-11 17:16:46 +05:30
sunkiddaandsunkidda 12260b288c .aep import: recognize French names for Levels' Channel and Clip popups (#3432)
* .aep import: recognize French names for Levels' Channel and Clip popups

After Effects stores a popup's items in the UI's own language. A project saved
by a French After Effects writes "RVB", "Activé" and "Désactivé pour la
couleur 32 bpc" for Levels' Channel and Clip to Output Black / White.
EffectCraft's own popup options are English only, so these never matched by
name and the parameter fell back to its default, reported as "has no
counterpart".

Add popup_alias, a small table of confirmed After Effects labels in other
languages, used as a fallback in popup_mapping after the exact name match
fails. A synthetic test reproduces the three losses from a real file and
checks they are gone, with the same values as the English original.

* Cover Levels' French Red, Green and Blue popup items too

popup_alias only mapped "rvb" (RGB), so a French Levels with Red, Green or
Blue selected still lost that selection the same way. Add "rouge", "vert"
and "bleu" (from the same real pdnm list, confirmed against it); "Alpha" is
already the same word in both languages. A direct test on popup_alias covers
every item of the Channel popup it claims to know.

---------

Co-authored-by: sunkidda <sunkidda@users.noreply.github.com>
2026-11-11 17:16:25 +05:30
arnav-hiwarkarandClaude Opus 5.5 6708964046 #3365: sort the unmapped effects with sort_by_key (clippy)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:15:43 +05:30
arnav-hiwarkarandClaude Opus 5.5 a44b042fce #3087: report the items a placeholder popup cuts
A popup of more than 1024 items kept only the first 1024 without a word, and
a valid selection past them was reported as damaged data. The cut is now
reported, and such a selection takes the first item and says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:00:44 +05:30
arnav-hiwarkar e2e3413011 #3087: an item made from one Photoshop layer shows that layer
Footage made from one layer of a layered .psd showed the whole document and
was named after the file. Its opti (tag 8BPS) stores the layer: the record
index (BE i32 at 14, -1 for the merged image), the bounds (LE i32 at 78-93)
and the name (UTF-8 at 344). The item now shows that layer as EffectCraft's
own Photoshop import does (at its own size with Retain Layer Sizes), named
<layer>/<file>. A layer without pixels (an adjustment layer) keeps the merged
image and is reported.
2026-11-11 16:56:00 +05:30
arnav-hiwarkarandClaude Opus 5.5 b60f231fe9 #3087: keep an effect without a counterpart as a placeholder
Third-party plug-ins and pseudo effects (expression controls) were dropped on
import, so expressions reading them broke. Such an effect now arrives as an
effect group whose effect id is its After Effects match name: it renders
nothing, like a missing plug-in, and is listed by Find Missing Effects. It
holds the effect's parameters flat, named as in After Effects, with their
values, keys and expressions in the units After Effects' expressions read.
It is still reported (once per match name, "kept without rendering") and
counted in imported.placeholderEffects. The parameters and popup items kept
are bounded per import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 16:46:06 +05:30
arnav-hiwarkarandClaude Opus 5.5 1f38431faf #3087: an expression on Orientation is imported with its value or keys
An Orientation with an expression was reported as "not decoded" and lost its
value and keys too. The expression is stored in the otst's tdbs like on any
property, so it now arrives with the stored value or keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 16:45:55 +05:30
arnav-hiwarkarandClaude Opus 5.5 b55226320b #3087: a shape property in the default state is no loss
Shape Direction, blend mode, composite order and the gradient highlight
values were reported when their record was in the default state (tdsb 3),
which holds After Effects' default (docs/aep-format.md 3.4), so a plain
guide rectangle reported "shape direction not imported". They are now read
only from written records, keys or an expression.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 16:45:55 +05:30
arnav-hiwarkarandClaude Opus 5.5 6b7731d05a #3365: the losses prompt shows every detail line and how many have none
The prompt showed 3 detail lines per category under a bare count, which read
as the project's total. The headline now says how many occurrences are left
out or changed, every line the report keeps shows, and "… and N more" counts
the rest. The import report records that number as `unlisted` (an unmapped
effect's line covers all its instances); unmapped effects are listed most
used first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 16:45:55 +05:30
Claude 4ca93da78f AEP import report: name at most 100 different unmapped effects (#3365)
An effect past the limit was still added to the example lines when they had room,
so a report could name up to 120. Past the limit an effect is only counted.
2026-11-10 15:54:19 +00:00
Claude 25ce8aff21 AEP import: name every unmapped effect in the report, with its instance count (#3365)
The import report keeps only the first 20 example lines per category, and each
unmapped effect instance was one line in the Effects category. With other losses
in that category, an effect used on many layers could be counted without being
named. Each effect with no counterpart now gets one line, with how many instances
there were ('... and 68 more like it'), placed before the other examples. The
category's count is unchanged.
2026-11-10 14:53:27 +00:00
Valentin Klein 7c62272305 .aep import: placed .ai/PDF footage framed by its ArtBox, Null anchors at the corner (#3099) (#3311)
* #3099: placed .ai pages fill the size After Effects stored; null anchors default to the corner

Footage of an .aep keeps the size After Effects stored for a placed PDF / Illustrator / EPS
file (the ArtBox, rounded up), and masks and anchors are measured against it. The page was
drawn as a PDF viewer frames it (CropBox, /Rotate) at its own size, scaled by the width ratio
at full resolution and stretched to the stored aspect below 3/4: masks too large, art off
centre, squeezed at half resolution.

- effectcraft-pdf: Framing::Placed / parse_page_framed (ArtBox clipped to the CropBox,
  /Rotate ignored); boxes with a non-finite corner or span are not used
- Footage::placed_page (set by the .aep importer, serialized only when true): the page is
  framed so and filled into the stored size (effectcraft_render::vector_fit), decoded,
  continuously rasterised and in Create Shapes from Vector Layer; Reload keeps the size;
  the frame cache keys it by size and framing
- .aep import/export: a null's absent Anchor Point is [0, 0], as After Effects reads it
- fixture ai-mask-3099.aep (own synthetic files, scrubbed, profiles stubbed) and tests
  against After Effects 2026's read-back

* #3099: placed-page framing only for .ai and .pdf, EPS as before; doc comments

After Effects' sizing was measured with PDFs named .ai only; an EPS from an .aep keeps its
bounding box at its own size (listed as not measured in docs/gaps.md and docs/aep-import.md).
Doc comments on the touched functions.

* #3099: shapes from a placed page keep its frame (track matte)

Create Shapes from Vector Layer converted every path of a placed page, so bleed, art past the
artboard or the page's background showed where the footage's raster hides them. The layers it
makes from a placed page now take a hidden "<layer> Frame" shape layer (the footage's
width x height) as their Alpha track matte: drawn at the comp's resolution, its edge is the
raster's on shape and image layers alike. The command returns the frame layer's id too.
2026-11-08 21:52:32 -06:00
vangel76 5f2d88ea5c Merge remote-tracking branch 'upstream/main' into aep-import-lumetri
# Conflicts:
#	crates/aep/src/container/effect_map.rs
#	crates/aep/src/export/effects.rs
#	crates/aep/src/import/effects.rs
#	crates/aep/tests/it/export/effects.rs
#	crates/aep/tests/it/import/effects.rs
#	docs/aep-import.md
2026-11-08 16:32:43 +01:00
FredApps 07be19d402 Merge pull request #3255 from vangel76/aep-import-puppet-3093
AEP import: Puppet arrives as EffectCraft's Puppet, its meshes and pins are reported
2026-11-08 15:46:00 +01:00
FredApps 7ca249d085 Merge pull request #3120 from vangel76/aep-import-thirdparty
.aep import: map CC Color Neutralizer, EXtractoR and IDentifier
2026-11-08 15:45:08 +01:00
FredApps 5832219d76 Merge pull request #3248 from vangel76/text-layer-name-follow-3233
Text layers: the name follows the text until the user renames the layer
2026-11-08 15:44:58 +01:00
FredApps 362b39038f Merge pull request #3256 from vangel76/clamp-time-stretch-3247
Time stretch: hold a loaded or imported stretch to ±1 % to ±9900 %
2026-11-08 15:44:45 +01:00
vangel76 c8ccf807e1 #3233: AEP import names an unnamed text layer as a following name is cut, so export and import agree 2026-11-08 04:53:01 +01:00
vangel76 e41d6d9d68 Merge remote-tracking branch 'upstream/main' into text-layer-name-follow-3233 2026-11-08 04:42:27 +01:00
vangel76 25ae9ad266 Merge remote-tracking branch 'upstream/main' into aep-import-puppet-3093
# Conflicts:
#	docs/aep-format.md
2026-11-08 02:39:14 +01:00
vangel76 3c08eb0647 Merge remote-tracking branch 'upstream/main' into aep-import-lumetri
# Conflicts:
#	crates/aep/src/import/effects.rs
2026-11-08 02:32:59 +01:00
vangel76 b90950c6a2 .aep import: a Channel Info record without its data is reported as damaged
import_extra returned as soon as the arbitrary data was missing, so an
EXtractoR or IDentifier value record whose aRbs/aRbp payload is gone was
silently imported with the default channels; the payload may have chosen
other ones (Z). As for Lumetri's data parameters, a stored record without its
payload now adds a DamagedData loss; a parameter with no record stays the
silent default. The test now asserts the report.
2026-11-08 02:32:56 +01:00
vangel76 67a39d9a2b .aep import: EXtractoR (copy) reads the effect input, not the EXR plane of that name
After Effects' Channel Info kind 2 "(copy)" was imported as R, G, B or A.
EffectCraft's EXtractoR resolves those names through the footage's aux planes
first, and an OpenEXR keeps its own R/G/B/A planes, so "(copy)" read the
original file plane and dropped what an earlier effect did to the channel.

"(copy)" is now a reserved EXtractoR channel name (EXTRACTOR_COPY) that the
CPU and GPU renderers never look up in the aux planes: it is the input's own
channel of the same position. The importer writes it for kind 2.

Render regression: a modified input buffer with same-named R/G/B/A aux planes
of other values; GPU parity case with (copy) on an aux scene with R/G/B.
2026-11-08 02:32:11 +01:00