Found while testing crates/doc/src/lib.rs:379 (number_unit, reached from parse_measure at :374)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A follow-up to #3677. A data URL in CSS, `style="fill:url(data:…)"` or a paint's
`url(…)`, stayed in the text the prepass searches, since only a value beginning with `data:`
was left out (CodeRabbit's review of #3677). Each `url(` in an element's quoted value whose URL
begins with `data:` is now left out up to the byte that closes it, its quote or `)`, so what
follows it, `display:none` say, stays. A `url(` that does not close inside its value is kept
whole: where the end is unclear, less is left out, never more.
The scan now jumps over a data URL with one search for the byte that ends it, instead of
stepping through it a byte at a time. The markers' capacity is a saturating sum, so no range can
panic it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round a closed path the editing data's text document writes the end of
the type's range unwrapped, past the last segment (the start plus the
run), or wrapped the other way. The importer clamped it to the path's
end, so the type kept only the stretch from its start bracket to the
path's end and overflowed. Both brackets are now wrapped onto the loop
(non-finite values fall back to the start); an open path is clamped as
before.
The object's own path that then replaces the frame is written open,
ending on its start: such a path is now closed where its ends meet, so
the span is measured round the loop.
Hand-written tests: an unwrapped, a reversed and a negative range on a
closed circle, a range inside it, an exact full turn that fits a short
line, an open path still clamped, and a path ending on its start closed.
Review follow-up (CodeRabbit): a linear gradient expands to a mesh with a
column at each stop, and the mesh mixes linearly between columns, so a
Perceptual blue -> yellow came out as the grey RGB mix in the middle.
`breaks` now also takes a column at each stop the Perceptual expansion adds
(Gradient::expanded), so the mesh follows the OKLab curve; Classic is
unchanged. the_mesh_option_makes_a_mesh_that_paints_the_gradient gains a
Perceptual case; without the change 1812 pixels differ from the gradient.
Review follow-up (CodeRabbit): the test's doc comment quoted a phrase from
the public help page. AGENTS.md allows feature names and a cited URL, not
copied wording; the comment now describes the behaviour itself and keeps
the URL.
Illustrator 30.0's Method choice in the Gradient panel
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-perceptual-interpolation-in-gradients.html).
- paint.editGradient takes `method: classic|perceptual` (one undo step);
paint.setFill/setStroke take it in `gradient`, and gradient params (swatches,
copied paints) carry it when Perceptual. Anything else is refused without
editing. One edit can set `method` and `dither` together.
- The Gradient panel shows a Method menu (Classic, Perceptual) for linear and
radial gradients, dimmed without a gradient; the Dither box stays under the
stops.
- i18n: "Perceptual" where a complete catalog lacked it, and the `method`
error in all 24 catalogs.
- Docs: G13 (estimate 14–24 h → 13–23 h, after Dither's 15–25 h → 14–24 h),
target-app-parity (row and evidence), ui-parity, mcp.md, plugins.md.
Illustrator 30.0's gradient Method, from the public documentation
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-perceptual-interpolation-in-gradients.html):
Classic or Perceptual; perceptual interpolation matches colour transitions to
how the eye perceives differences between colours, and perceptual gradients
are rasterised when saved as PDF or SVG.
- Gradient gains `method` (Classic | Perceptual; serde default Classic,
written only when Perceptual, so older files open as Classic and Classic
files are written as before). The colour crate's OKLab helpers open up for it.
- Perceptual mixes neighbouring stop colours in OKLab (sample, and
sample_keep_model for the stroke meshes, Expand and blends); opacity stays
linear. Renderers that mix linearly get stops on the perceptual curve,
placed by halving each half segment where a straight line strays more than
0.4 of a colour level (a nearly straight segment needs none). Classic
gradients expand exactly as before.
- draws_as_pixels includes Perceptual gradients, so SVG and PDF write a
Perceptual fill or stroke as an image at the document's raster effects
resolution, clipped to what it paints. A gradient along or across a stroke
stays slices (PDF no longer draws it as a within-stroke image).
- With Dither: a dithered Perceptual fill is the perceptual mix with dither's
noise (the canvas's dithered grids are keyed by method too); SVG and PDF
write it as an image with the noise, and a dithered Perceptual stroke as an
image without it, as the canvas draws it (GradientPaint::pixels, SVG's
raster_paint and PDF's area are told whether they paint a fill).
Review follow-up (CodeRabbit): the refusal skipped any request carrying a
`swatch` key, so `"swatch": null` with a dithered gradient got through.
Only a named swatch (a gradient swatch keeps its own dither) is exempt now.
The dither test sends `"swatch": null`; it fails without the change.
The prepass decided what to look for in the tree by a dozen substring searches over the whole
SVG text (`:not(`, the event attributes, `script`, `transform:` and nine more), and a separate
pass counted the elements' depth before roxmltree. With an embedded picture the text is mostly
its data URL: a 1.35 MB card SVG (a 968 KB PNG) paid about 7 ms of searches and 1 ms of depth
scan per import, on a 64 ms render, and a 54-page PDF export 54 times that.
`depth::prescan` is the one pass: it counts the depth as `xml_too_deep` did (the DOCTYPE entity
count included) and, in the same walk, notes every `data:` attribute value, answering the text
with those values left out, their quotes kept. The prepass and `fx::blends` search that text;
the edits are still made in the source. A data URL can hold no tag, attribute name, selector or
CSS declaration, and everything else, a `style="display:none"` included, is kept, so what is
found is what was found before.
`vectorcraft-cli perf --deep` gains a tracked row, "import SVG, one embedded PNG", beside the
export row it had.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With Copy Links on and Copy Fonts off, File › Package still read every
placed PDF or .ai page to find the fonts of its type, and then dropped
them. With Copy Links on and Relink off, it still read the preview and
the PDF of each placed .vectorcraft document, which only the rewritten
copy uses.
Package now reads the placed pages for their fonts only with Copy Fonts
on, and it reads the preview and the PDF only with Relink on. With Copy
Fonts off, packaging a document that places the 60 pages of a PDF took
about 390 ms in a release build and now takes about 20 ms. The package
it writes is the same.
With the Artboard tool, File › Revert kept the active artboard, but a
command.batch with a file.revert step made artboard 1 the active one.
A batch that switched to another document and back, and the Actions
panel menu's Batch… over a folder whose files it closes or over data
sets with a Revert step, did the same. Each of them starts the tool
afresh and ends in the document it started in, and the app moved the
view to the fresh tool's artboard 1. Only a plain file.revert was
handled, by its command id.
The engine now counts the times switching or closing documents, or
replacing the active one, starts the tool afresh (Session::tool_resets),
and records whether the tool's options were set after that
(Session::tool_set_since_reset). When the count changes during a
command and nothing set the tool's options after it, the app starts
the tool on the active artboard, as it does when a command chooses
another document. When a later step chose the tool's artboards (a
Paste or Cut after the revert), the artboard the tool then has active
becomes the active one.
Docs: docs/mcp.md, docs/target-app-parity.md.
Review follow-up (CodeRabbit): a gradient drawn as pixels was sized from its
area in the space it is drawn in, so type scaled 4 times got a quarter of the
raster effects resolution; the image is now sized from the area on the page,
within the same pixel caps. And type whose fill is drawn as art (a dithered
or freeform gradient's image, a pattern) went out as outlines only; its
characters are now also written as invisible text (rendering mode 3), as
unpainted type already is, so they can be selected and searched.
Review follow-up (CodeRabbit): paint.setStroke kept `dither: true` from its
gradient, and a stroke can hold dither anyway (swapped from a fill). Outline
Stroke and appearance expansion copied the stroke's paint into the outline's
fill, which then drew dithered although the stroke never did. The outline's
fill now goes without dither (Paint::stroke_as_fill), and paint.setStroke
refuses a gradient with `dither: true`, as paint.editGradient does.
Review follow-up (CodeRabbit): a dithered fill skipped set_paint's guard for
a gradient without stops and sampled a transparent grid to draw nothing. It
now takes the same no-fill path.
Review follow-up (CodeRabbit): a large dithered gradient mostly off-screen
sampled its whole grid (up to 4096 x 4096 cells, 64 MiB) to draw a small
visible part, and did it again on each zoom step. The renderer now samples
only the cells the render target can show, a cell more each way for the
bilinear mix, snapped out to 256-cell tiles so a pan within them reuses the
grid. Cells keep their place in the whole grid, so the gradient and its
noise are the same pixels as before (GradientPaint::pixels_within).
With Pick Color From Artwork, a Shape Builder merge took the look of
the front-most object over the first covered region the drag touched,
and the current fill when the drag touched only areas no object
covers. The reference app's combine page gives the look of the object
where the drag starts, else of the object where it ends, else of the
topmost selected object.
shapeBuilder.merge now follows that rule: the merged shape takes the
appearance, opacity and blending mode of the front-most of the objects
it is given (the selection by default) under the first point, else of
the one under the last point, else of the front-most of those objects.
A drag from a hole across a gray bar into a red square now gives a red
shape, where it gave the bar's look. A click in an area no selected
object covers, such as a hole that gray bars enclose or a triangle
that lines enclose, takes the front-most selected object's look
instead of the current fill. Pick Color From Color Swatches works as
before. The tool already sends the press point first and the release
point last.
Docs: docs/mcp.md, docs/target-app-parity.md.
## Summary
Illustrator `%AI9` art styles already stop reading filters after 4096 entries so
hostile styles cannot grow without bound. This adds a regression test that
importing a style with more than `MAX_FILTERS` basic filters finishes with a
bounded effect list, matching the existing hardening behaviour.
## Tests
- `art_styles_ignore_filters_beyond_the_cap` in `crates/eps/src/import/ai/style.rs`
Part of gap G8
Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
## Summary
When the Paragraph panel acts on more than one paragraph whose displayed
direction differs (explicit LTR/RTL or Auto resolved from content), neither
Left-to-Right nor Right-to-Left is highlighted, matching alignment and the
other mixed-value paragraph controls in Adobe Illustrator.
## Tests
- `paragraph_mixed_direction_buttons_show_neither` in `panels/paragraph.rs`
Part of gap G4
Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
File › Package… put the file's path in front of every error its writer
returned. A refusal from the automation write root already names the
path, so the dialog's error and the status bar named the file twice,
for example on Windows for a document named aux:
`<path>: automation path rejected: reserved device names are not
allowed: <path>`.
The dialog now keeps a writer error that already contains the path as
it is, and puts the path in front of one that does not, such as the
desktop's "Access is denied. (os error 5)".
PDF import measured each gap between two glyphs against the larger of
the line's first glyph size and the next glyph's. A word set beside a
much larger one, apart by positioning and no space glyph (a browser's
margin between two sizes), stayed under a fifth of that em: "≤ 4" at
20 pt and "hours" at 8 pt, 3.4 pt apart, came in as "≤ 4hours".
A gap is now measured against the smaller of the two glyphs beside it.
Lines in one size read their gaps as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Enter and Escape went to the active tool whatever modifiers were held,
and the command bound to the chord then ran as well. With a command
bound to Cmd+Enter, Cmd+Enter ended the Pen tool's path, opened the
Move dialog with the Selection tool, or typed a line break while the
Type tool edited text, and then ran the command. With a command bound
to Cmd+Escape, Cmd+Escape ended the Pen path, the Type tool's text
editing or isolation mode before the command ran.
A press of Enter or Escape whose chord a command is bound to, matched as
the shortcut dispatcher matches it, now runs only that command, also
when the tool claims the key. While the Type tool edits text, only Cmd
chords count. While a tool is in the middle of an interaction (a drag,
or between the clicks of a tool such as Blend or Dimension), and for
plain Enter and Escape and chords that no command is bound to, the key
goes to the tool as before.
Illustrator 30.0's gradient Dither, from the public documentation
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-dither-in-gradients.html):
a Dither checkbox in the Gradient panel; dither converts the gradient into
pixels with subtle noise, works with gradient fills only, and follows the
Document Raster Effects resolution.
- Gradient gains `dither` (serde default false, written only when on, so
older files open unchanged and new files keep it).
- The renderer draws a dithered linear or radial fill as an image paint: the
gradient sampled a cell per device pixel or finer (quarter-octave steps,
cached per thread), each channel given seeded triangular noise of up to a
level before rounding. Same pixels every run; strokes and undithered fills
draw exactly as before. PNG and other raster exports go through it.
- SVG and PDF write a dithered fill as an image at the document's raster
effects resolution, clipped to the shape (the freeform-gradient path);
characters' fills stay vector gradients in SVG, with a warning.
- paint.editGradient and paint.setFill take `dither`; strokes refuse it.
The Gradient panel's Dither box is dimmed behind the Stroke proxy.
- i18n rows in all 24 catalogs; docs: G13, target-app-parity, ui-parity,
mcp.md, plugins.md.
Shared groundwork for gradient options SVG and PDF can't express (Illustrator
30's dither and perceptual gradients): GradientPaint::pixels samples a placed
linear or radial gradient on a grid (Ramp: its stops, midpoints as stops, mixed
as the renderer mixes them), and draws_as_pixels says which gradients are drawn
that way (freeform ones, so far). The SVG and PDF writers' freeform-gradient
image path takes any such gradient, a linear or radial one at the document's
raster effects resolution. No change in output yet.