Commit Graph
4607 Commits
Author SHA1 Message Date
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
ash 41585f2aec Merge pull request #3450 from arnav-hiwarkar/fix/3087-aep-effect-placeholders
.aep import: keep effects without a counterpart as placeholders (#3087)
2026-11-12 14:21:58 +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 04bbc67528 #3087: test a placeholder found by its match name under ADBE Effect Parade 2026-11-12 13:42:53 +05:30
arnav-hiwarkar b762b4800d Merge remote-tracking branch 'upstream/main' into fix/3087-aep-effect-placeholders
# Conflicts:
#	crates/expr/src/host.rs
2026-11-12 13:42:21 +05:30
Sean Hawkey 24df676af5 Expressions, Scripting: a layer's property numbers and match names follow After Effects (#3528) (#3529)
* Expressions, Scripting: a layer's property numbers follow After Effects

In After Effects a layer's top-level number reaches the same group on every layer of a kind:
Marker is 1, and slots the layer has not used yet (Time Remap, Motion Trackers, Layer Styles,
Data, Sets...) keep their place. EffectCraft numbered only the groups a layer holds, without
Marker, so `layer(n)` and `layer.property(n)` reached a different group, and the numbers moved
as groups were added. Rigs that read a palette by number, such as
`comp("Palette").layer(1)(4)(n)(1)` on a shape layer, read Transform instead of Effects; their
`try` fell back to the stored value and every linked colour rendered near-black, with no
expression error.

`effectcraft_project::ae_order` holds After Effects' slot order per layer kind, observed in
After Effects 26.3 from a script (`layer.property(i).matchName`) and from an expression
(`thisComp.layer(..)(i).name`) on new layers of each kind. The expression host and the
scripting model resolve a layer's numbers through it: 1 is the markers, a used slot is its
group, an unused one is an empty group (never null, so loops over `numProperties` are safe).
`numProperties` at layer level and `propertyIndex` of top-level groups are After Effects'
numbers too. 3D model and primitive layers keep EffectCraft's own numbers.

* Expressions: a layer's top-level groups by After Effects match name

`thisLayer("ADBE Effect Parade")`, as scripts such as Karton write their expressions, reaches the
group through the same table as the layer's numbers: Effects, or an empty group for a slot the
layer has not used, and `("ADBE Marker")` the markers.

* Expressions, Scripting: review follow-ups for After Effects layer numbers

- An enabled Time Remap (a top-level property) reports After Effects' number 2 as its propertyIndex.
- In scripting, an unused slot resolves to the same empty group by match name
  (`property("ADBE Layer Styles")`) as by number (`property(7)`).
- The empty group is callable like any group: `layer.property(7)(1)` is null, not a TypeError.
2026-11-12 13:40:56 +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 6f8c39d5f4 #3087: expressions find an effect by its After Effects match name
effect("Pseudo/0.25") and thisLayer("Effects")("Pseudo/0.25") now match a
placeholder's effect id, now that its match id is one path segment.
2026-11-12 13:06:51 +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
ash 9a4d02a827 Merge pull request #3449 from arnav-hiwarkar/fix/3087-aep-psd-layers
.aep import: an item made from one Photoshop layer shows that layer (#3087)
2026-11-12 12:58:31 +05:30
ash 103760584a Merge pull request #3533 from arnav-hiwarkar/fix/3493-lut-cache
Apply Color LUT and Lumetri follow a LUT file that changes on disk (#3493)
2026-11-12 12:00:00 +05:30
ash c9e553266c Merge pull request #3530 from arnav-hiwarkar/fix/3456-tracker-signature
Warp Stabilizer, 3D Camera Tracker: keep analyses saved before 0.23 (#3456)
2026-11-12 11:39:19 +05:30
ash 8a2b4cc633 #2796: ScriptUI panel bars hide while the window fits its dock (#2880)
A docked panel's solid bars, once shown, took their width from the viewport, so a window that exactly filled its dock stayed too large and kept its bars. The bars are now hidden while the window fits its dock (to the whole point, #2585) and stay solid when it overflows (#269).
2026-11-12 11:37:26 +05:30
arnav-hiwarkar 63520598a0 #3493: test the followed-files limit on a cache of its own
The test filled the shared cache past the limit, which could let go of a
file another test running alongside still follows.
2026-11-12 11:36:40 +05:30
arnav-hiwarkar dc3e5245c2 #3493: show the frame again in the viewers once a LUT file changes
The viewers replace their picture only when the frame key changes, and a
frame rendered again after a LUT change had the same key, so the old look
stayed on screen. The viewer frame keys carry the LUT generation (in memory
only), which also leaves frames still rendering with the old LUT unasked
for, so the refusal of late frames is no longer needed.
2026-11-12 11:29:57 +05:30
arnav-hiwarkar d036127bf4 #3493: key a layer reading a precomp on the LUT file its override names
A precomp instance whose Essential Property names another LUT file renders
with that file, but a layer reading the instance through a layer parameter
was keyed on the nested comp's own file, so editing the overriding file left
that layer cached.
2026-11-12 11:29:57 +05:30
arnav-hiwarkar 82c2a23f07 #3493: keep following the LUT file just read past the followed-files limit
Past 1024 followed files, the trim kept only files with a cached LUT and
dropped the one being noted (its LUT is cached right after), so a later
edit to it went unnoticed. The tests start from an empty folder.
2026-11-12 11:29:57 +05:30
arnav-hiwarkar 63aca62574 #3493: follow LUT files whose LUT left the cache; bound it by bytes too
The watcher stamped only the files whose parsed LUT was still cached, so
with more LUTs than the cache keeps, an edit to an evicted one left the
viewer's frames stale. The cache now remembers the stamp each LUT file was
last read with (bounded separately), and keeps at most 64 LUTs and 512 MiB
of tables. Frames finishing while the viewer empties its cache are refused.
2026-11-12 11:13:12 +05:30
arnav-hiwarkar 58fcfeb54d #3456: test that placed-page footage doesn't match a 0.22 key
0.22 keys are hashed without placed_page, so only footage that isn't
placed as a page may match them.
2026-11-12 11:04:21 +05:30
arnav-hiwarkar 2166596625 #3493: drop the viewer's frames once a LUT file in use changes
Renders now follow a LUT file, but the viewer and RAM preview kept the
frames already rendered with the old one. A small worker stamps the LUT
files in use every two seconds and wakes the interface when one changed;
the interface then drops its frames, and a frame whose render started
before the change is not kept when it finishes.
2026-11-12 11:01:32 +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 e36e25512d #3456: solve again from 3D Camera Tracker tracks saved before 0.23
Analysing a tracker reused its saved tracks only when their key was
today's, so solving a 0.22 project again (after a Solve Method change)
tracked the footage again, and failed when it was offline.
2026-11-12 10:56:40 +05:30
arnav-hiwarkar cd08781aab #3456: keep Warp Stabilizer and 3D Camera Tracker analyses saved before 0.23
Their keys were compared only with today's signature, so the first edit
of a project saved by 0.22 (even adding it to the render queue, as
effectcraft-cli render does) cleared both analyses and queued them again,
and lost them when the footage was offline.

One check, warp::key_matches, now accepts a key made from today's
signature or from the one 0.22 wrote for the same frames, and keeps it.
Warp Stabilizer, the 3D Camera Tracker and Roto Brush all use it.

The regression test opens a project saved by the released 0.22.0 CLI.
2026-11-12 10:56:39 +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 6a78d94a5b #3493: key cached layers and frames on the LUT files they read
A layer graded with Apply Color LUT or Lumetri's Input LUT and Look was
cached by the parameter text alone, so the same frame kept showing the old
LUT from memory and from the disk cache after the file changed. A layer's
cache key (and the key of a layer reading it, through a precomp too) now
holds each LUT file's size and modification time, and a render lists the
LUT files it read, so a frame kept on disk made with an older file is not
used.
2026-11-12 10:51:27 +05:30
arnav-hiwarkar 6780c65671 #3456: write the footage in analysis signatures field by field
The Warp Stabilizer, 3D Camera Tracker and Roto Brush signatures hashed
the footage item's derived Debug text, so every new Footage field changed
every signature and dropped every saved analysis (placed_page did in 0.23).
Write the same text from a named list of fields instead: a new field no
longer compiles until it is decided there. Pin the text in a test.
2026-11-12 10:44:57 +05:30
arnav-hiwarkar d503b26e21 #3493: Apply Color LUT reads a LUT file again once it changes on disk
load_lut kept the first result for a path for the life of the process: an
edited file, or one created after a failed load, was never read again.
Each file-backed entry now keeps the file's size and modification time and
is read again once they move; inline .cube text stays cached by content.
The cache keeps at most 32 LUTs (least recently used goes first).
2026-11-12 10:43:41 +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
Dylan M. Taylor 79c42749a8 Export, Media: codecs and containers from craft-libs; remove hevcenc, av1enc, vp9enc (#3522)
* Export, Media: H.264, ProRes, HEVC, VP9, AV1, AAC, MP4 and Matroska come from craft-libs

crates/export and crates/media use craft-video, craft-audio and craft-media
(one craft-libs rev) instead of filmcraft-h264enc, -prores, -aac, -isobmff,
-matroska and -opus. The av1enc, hevcenc and vp9enc crates are removed:
craft-video carries the same encoders, with the later changes merged in
storytold/craft-libs#829. Their round-trip tests move to crates/export/tests
and run against craft-video.

opusenc stays (craft-libs has no Opus encoder yet), as do filmcraft-mpegts
and the filmcraft-vp9, -hevc and -av1 decoders the tests use as oracles.

* Pin craft-libs at the main commit that has the encoder colour description (e7c3dda7)
2026-11-11 18:46:50 -05:00
FredApps f334bcf3ef CI: nightly release dry run (#3520)
release.yml also runs nightly on main with craft-common-actions' dry-run: every release build
except macOS, with nothing signed, pushed or drafted. Breakage that only the release build hits
(riscv64, Flatpak, FreeBSD, packaging) then shows up the next morning instead of on release
day, as it did on 11 Nov 2026.
2026-11-11 23:04:03 +01:00
Dylan M. Taylor b47a7104ac Scripting: run-time compiled code passes the nesting check; bounded file masks, socket line search and file reads (#2975)
* Scripting: run-time compiled code passes the nesting check; bounded file masks, socket line search and file reads

eval and the Function constructors compiled script-built strings without
the bracket-nesting check the script itself passes, so a short script could
overflow the parser's stack. A host hook now checks every dynamic
compilation. Folder.getFiles turned masks into a backtracking regular
expression; a two-pointer wildcard match replaces it. Socket readln
rescanned the whole buffer after every read; only new bytes are searched.
Script files that are not regular files (pipes, devices) are not read.

* address review: check_nesting scans template interpolations, file masks keep wildcard length

* address review: file masks fold case as /i does; the nesting check skips regular expression literals

* address review: nesting scan reads ++/-- and keyword-named properties as division context

* address review: property-name state set at each word start; a number's decimal point is not a property dot

* address review: a leading hashbang line is a comment for the nesting scan; eval source is checked as it stands
2026-11-11 15:54:55 -05:00
Dylan M. Taylor 00d50b8c68 ScriptUI: bound the edges and bands a fillPath is tessellated with (#3001)
* fix(scriptui): bound the edges and bands a fillPath is tessellated with

* address review: cap the triangles a ScriptUI fill produces

* address review: raise fillPath limits (65,536 edges, 262,144 bands, 4M triangles, work cap); fill what fits and log a warning instead of nothing

* address review: bound the fillPath crossing scan: edges swept by top, pairs and boundaries counted against the work and band budgets

* address review: bands with crossings that were not cut are left unfilled when the crossing scan is over budget

* address review: bands left out of a fill still count against the work budget
2026-11-11 15:54:28 -05:00
Dylan M. Taylor b7e848a629 Import, Relink: index sequence runs and folder listings; probe relink candidates once (#3000)
* fix(engine): index sequence runs and folder listings on import; probe relink candidates once

* address review: cap relink candidates before cloning paths; reuse a picked folder's listing for its run lookups
2026-11-11 15:54:01 -05:00
Dylan M. Taylor e339a5b375 Effects: cap Camera Lens Blur and median radii (#2951)
* Effects: cap Camera Lens Blur and median radii

Camera Lens Blur built its iris table (one row per pixel of radius) from an
uncapped Blur Radius before any size check, and the median window is filled
(2r + 1)^2 per row from an uncapped Radius, on the CPU and the GPU. The
radius is now capped where the work is built: 2000 buffer pixels for the
lens iris (UI max 500) and 512 for the median window (UI max 255), on both
paths.

* address review: cap camera lens reach with the spans
2026-11-11 15:53:34 -05:00
Dylan M. Taylor 3ddfbd8a00 Tracking: face skin search visits only window pixels; stabilizer smoothing window capped (#3022)
* track: face skin search visits only window ring pixels; cap stabilizer smoothing window

* address review: stabilizer window test asserts the cap with a far jump

* address review: smoothing radius covers every supported Smoothness (1000%, Smooth More, 240 fps); a frames x radius work cap bounds planning
2026-11-11 15:53:08 -05:00
Dylan M. Taylor 5735095cc8 Camera tracking: repeated observations of one camera combine in the bundle reduction (#3502)
* Camera tracking: repeated observations of one camera combine into one contribution per parameter

* Camera tracking: parameter-slot lookup for points that repeat a camera
2026-11-11 15:52:52 -05:00
Dylan M. Taylor 7c0c57cb27 Segment: bound MobileSAM prompt points, TFLite metadata and folded DEQUANTIZE sizes (#3509) 2026-11-11 15:52:25 -05:00
Dylan M. Taylor 0b196915e8 VP9 encoder and WebAssembly plug-ins: refuse short planes, limit memories and tables (#2995)
* VP9 encoder and WebAssembly plug-ins: refuse short planes, limit memories and tables

encode_frame sized and filled superblock-aligned coding planes from the
configured dimensions (up to 65536 x 65536) even when the planes passed in
held a few bytes, making up the missing samples; it now encodes nothing for
planes shorter than the frame. The plug-in sandbox capped each linear
memory but not how many memories or tables a module declares, or their
elements; one memory, four tables and 65,536 elements per table are
allowed.

* address review: checked size arithmetic in the encoder's short-plane guard
2026-11-11 15:51:58 -05:00
Dylan M. Taylor cbddc9d284 Fix Flatpak updates and bump to 0.24.1 (#3516)
* fix(flatpak): allow release checks and self-update helpers

* test: keep Flatpak permission parsing within strict Clippy rules

* chore: bump version to 0.24.1 for the Flatpak update fix
2026-11-11 15:25:13 -05:00
Dylan M. Taylor 072e1af9a4 Render settings: a saved Frame Rate override is held to the supported rates (#3503)
* Render settings: a saved Frame Rate override is held to the supported rates

* Render settings: describe shows the rate an out-of-range override renders at
2026-11-11 15:13:09 -05:00
Dylan M. Taylor 4ba6ff32b8 PDF functions and mesh shadings: charge retained values, refuse short tables, bound lattice rows and raster work (#2972)
* PDF functions and mesh shadings: charge retained values, refuse short tables, bound lattice rows and raster work

Depth-limit fallbacks were not charged to the function node budget; cached
copies of a function deep-copied its coefficient and program vectors while
only its nodes were charged; sampled functions made up missing samples as
zeros; calculator programs had no size limit; a lattice row was filled
before any limit applied; and overlapping mesh triangles each scanned their
whole bounding box.

The node is charged before the depth check; coefficients, bounds, encode
arrays and calculator operators are charged (eight to a node) so cached
copies carry their cost; a sampled function shorter than its table is
unsupported; calculator programs hold at most 4,096 operators; lattice rows
past the triangle limit are refused and vertices stop at it; and triangle
bounding boxes share a budget of 64 passes over the raster.

* address review: bound calculator token count while parsing

* address review: count skipped calculator tokens toward MAX_CALC_OPS

* address review: skip arrays and dictionaries in a calculator program without building them

* address review: scalar tokens in a skipped compound are passed over without building them
2026-11-11 15:12:55 -05:00
Dylan M. Taylor 6c1391d4de Text: run lookup by bisection, bounded path flattening, font folders and files shared (#2991)
* Text: run lookup by bisection, bounded path flattening, font folders and files shared

run_at scanned the style runs from the first for every character; the
ordered runs are now bisected (the scan remains for gaps). Path text
flattened at a fixed 0.05 tolerance, so huge coordinates made hundreds of
millions of points; the tolerance grows with the path's size and
non-finite paths measure nothing. Font name records aliasing one string
are decoded once. The font folder walk listed folders reached through
several links again along every path; each real folder is listed once.
Faces of one font collection each read the whole file; they share it.

* address review: font walk breadth-first across roots, font file cache keyed by size and mtime and read outside the lock, first-subpath path measure with finite check, scratch dir guard

* address review: font files shared through file_bytes with the open file's metadata; run lookup by bisection only for ordered runs

* Pass the runs' ordered flag with them to stay under the argument limit

* address review: reversed run ranges take the scan
2026-11-11 14:40:29 -05:00
Dylan M. Taylor 428a861356 Project: layer name suffixes in one pass; template names bounded and never a parent folder (#2973)
* Project: layer name suffixes in one pass; template names bounded and never a parent folder

unique_layer_name tried each numeric suffix against every layer, so
importing many layers of one name was cubic. The suffixes in use are now
gathered once. sanitize kept '.' and '..' (a comp name used as a path
component in an output template could point at the parent folder) and
copied a name of any length into every template token; dot-only names
become underscores and names are cut to 255 characters.

* address review: limit sanitized names to 255 bytes

* address review: keep long sanitized names distinct with a hash suffix

* address review: unique layer names use the numbered_name stem rule
2026-11-11 14:40:15 -05:00
Dylan M. Taylor 0f200006fa EPS: bound copies, storage, text outlines, group nesting and font discovery (#2953)
* EPS: bound copies, storage, text outlines, group nesting and font discovery

The PostScript interpreter's numeric copy grew the operand stack past its
limit and charged nothing for the values copied; array and string
constructors retained storage without charging it; show and charpath copied
every glyph outline into the text path before any path or budget check; and
the font pre-scan searched the whole prefix again for every eexec, as the
bounding-box scan did for every rejected marker on a line. Clipping groups
also nested without limit.

Copies past the operand limit copy nothing; copies, arrays and strings count
against the operation budget; each outline is charged and checked against the
path limit before it is copied; headers are found in one pass and each is
tried once; a rejected bounding-box comment is skipped to its line end; and
the output builder drops groups past 256 open ones.

* address review: EPS font look-back stops at the previous font; charge input characters

* address review: count enclosing groups against the nesting limit in pattern and mask builders

* address review: close a replaced soft mask before nesting the new one; skip patterns whose wrappers pass the depth limit
2026-11-11 14:40:02 -05:00
Dylan M. Taylor 0e248459d2 Tracking: bounded crop size, Roto chain span and resection copies (#2989)
* Tracking: bounded crop size, Roto chain span and resection copies

A stored pre-tracking blur or search area sized the padded crop with no
limit; both are held to what the frame can use (blur at most 100, as the
options command allows). A Roto Brush span from the project made one render
build a key for every frame of it, rescanning all strokes per frame; the
chain reaches at most 200,000 frames either side of the base and strokes
are grouped by frame once. The camera solver copied the whole
reconstruction for every unsolved frame, even frames seeing too few points
to resect; those are skipped first.

* address review: in_span uses the MAX_CHAIN-bounded span, forward key range cannot overflow
2026-11-11 14:39:47 -05:00
Dylan M. Taylor 094d59528b Box blur: count repeated edge pixels instead of visiting them (#2990)
* Box blur: count repeated edge pixels instead of visiting them

The starting window of each horizontal and vertical box pass visited every
position of the radius, including any number outside the image that repeat
the edge pixel, so a radius from a project could make one pass take
arbitrarily long. The pixels inside are summed once and the repeated edge
pixels are added as a count times the edge value.

* address review: skip zero-count edge pixels in the box passes
2026-11-11 14:39:33 -05:00
Dylan M. Taylor c6d5c62d5a PSD: bounded zip errors, budgeted prediction row, row-bounded PackBits, channel checks before the result (#2987)
* PSD: bounded zip errors, budgeted prediction row, row-bounded PackBits, channel checks before the result

A failed inflate was formatted with Debug, which prints the whole partial
output; only the status is kept. The 32-bit ZIP prediction scratch row was
allocated outside the decode budget; it is charged now. PackBits packets
were appended whole even past the row's end, growing the reserved plane;
they are cut at the row. The composite and layer decodes allocated the
document-sized result before checking that the channel data exists; the
compression and raw/RLE lengths are checked first.

* address review: check short ZIP composite data and the layer mask channel before allocating the result

* address review: drop the channel prechecks that rejected PSDs the lenient decode accepts; keep the allocation bounds
2026-11-11 14:39:19 -05:00
Dylan M. Taylor 6ff564e750 SVG: bound tessellation, kept geometry, attributes, entity expansion and re-rendering (#2986)
* SVG: bound tessellation, kept geometry, attributes, entity expansion and re-rendering

Circles and rounded rects flattened at a fixed 0.01 tolerance, so a huge
radius made unbounded curves; the tolerance now grows with the radius as
arcs already did. Every path a <use> copies and every gradient reference's
stops were parsed and kept without a document-wide limit; both now share a
budget of 8 million items. An element with thousands of attributes made
the XML parser's duplicate check quadratic, and repeated entity references
could expand attribute text to gigabytes below the node limit; a preflight
refuses more than 1,024 attributes per element and entity references that
expand past 64 MiB. Nested knockout and non-isolated groups re-rendered
their children several times per level; node draws are now held to 64 per
node of the tree.

* address review: entity_bytes takes the largest declaration of each name

* address review: drop tessellation-tolerance, attribute-count and entity-size refusals; keep retained and draw budgets, warn when the draw budget is hit
2026-11-11 14:39:06 -05:00