The version 7 .ai writer wrote the %_Bs color stops of every gradient
from ramp point 100 down to 0. The Adobe Illustrator File Format
Specification version 7.0 lists a linear gradient's stops in that order
(section 1.4.3). Its radial definition, "White & Purple Radial", lists
them from 10 to 100 (section 3.5.2). As reported on #2807, the
reference app reads both stops of #2687's radial gradient, written from
100 down to 0, at ramp point 100 and draws the circle in #1b2a6b. It
reads a radial gradient with three stops written that way in reverse
order.
The writer now lists a radial gradient's stops from the first to the
last. Each stop is written with its own midpoint to the next stop, and
section 3.5.2 says the last stop's midpoint is ignored. Linear
gradients are unchanged.
The Offset Path dialog remembers the values it was last confirmed
with (#1152), but the Preview checkbox was not among the kept
fields, so every open started unchecked and Preview had to be
ticked manually each time. Preview now rides along with offset,
joins and miterLimit in the fields kept on OK, as Simplify's
retained fields already keep theirs.
Unchanged: a very first open (nothing kept yet) still starts
unchecked, and Cancel keeps nothing.
Adds a regression test beside offset_path_remembers_its_last_values:
open, tick Preview, confirm, reopen - the checkbox is still on.
Verified by reading the flow on current main (menus.rs opens on
app.ui.offset_path when it is an object, d.bool("preview") defaults
to false when the field is absent, form::preview_result ticks the
checkbox into d.fields) and by tracing the existing #1152 test;
no Rust toolchain on this machine, so the test was not executed
here - saying so plainly for the reviewer.
Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
The desktop app reads the User Defined swatch and graphic style
libraries while it creates its window: set_user_dir runs
Libraries::rescan, which read and parsed every file in the folder
outside the panic guard that commands have. A parser bug that panicked
on one file stopped the app before its window appeared, at every
launch while the file stayed in the folder.
rescan now reads and parses each file inside guard::catch_panic and
skips a file whose reading panics, as it skips one that can't be read.
The other libraries are listed.
Found while testing crates/color/src/palette_io.rs:373
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/tools/src/xform/measure.rs:20 (shown at :66, reported at :80)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/doc/src/xml_id.rs:80 (strip_unique), triggered by from_object_name at :32
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Random in a Transform effect takes its seed from the object's id. The
copies of a gradient path that its Transform effect makes were given new
ids before their strokes were outlined, so a random Transform on the
stroke moved each outline elsewhere than the canvas drew it. The copies
are now outlined first and given new ids after.
A fill with a random Transform effect stayed on a copy of the path with
a new id, so it drew elsewhere too. It now paints the outline its
geometry effects make of the path, on a copy of its own.
outlines_keep_the_random_transforms_the_canvas_drew checks Outline
Stroke, Expand and Flatten Transparency against the canvas for both
cases and for a random Transform that makes the copies.
The canvas and export tests compute their expected pixels with
edge_coverage, the function the renderer uses, so a change to it went
unnoticed. edge_coverage_gives_each_methods_documented_share checks its
output for None, Sharp, Crisp and Strong at eleven coverages, including
the half-coverage threshold and the points where Crisp and Strong
reach 0 and 255.
When a JPEG XL image embeds an ICC profile that the decoder can't convert
to sRGB without a color management system, its colors come back as
stored. The PNG the image was stored as left the profile out, so a saved
document or an SVG export held those colors without their color space.
That PNG now carries the profile when it is an RGB one. Colors converted
to sRGB are stored without a profile, as before, and so are a gray
image's colors, which come back as RGB.
A path whose Transform effect makes copies and that has a gradient fill
or stroke is drawn on the canvas as separate copies, each painting its
gradient by itself. Outline Stroke merged the copies into one path, so
the gradient stretched across all of them. It now outlines the strokes
of the art the canvas draws, as Expand Appearance does. Expand's Stroke
option and Flatten Transparency's Convert All Strokes to Outlines
change the same way.
outline_strokes in pathops.rs now imports reshaped from expand.rs, which
has had it since #2616, and the copies of reshaped and with_path that
this branch added to pathops.rs are removed. The two copies were
identical, so Outline Stroke and Expand behave as before.
The pre-existing swatch name-clash test is a negated conjunction that clippy 1.95 flags as nonminimal_bool, which stopped cargo xtask ci before this PR's crates were checked. Rewrite it as the equivalent disjunction; no behaviour change.
Profile samples added inside a flattened chord used the chord's direction for their normal, so a zoomed outline jogged at every flatten vertex. Keep the source segment that drew each flattened piece and ask it for the exact tangent at added samples; fit runs with coordinate-wise Fritsch-Carlson tangents. Adds a regression test and records the change in the parity evidence.
Adds .coderabbit.yaml so CodeRabbit no longer reviews or comments on PRs opened by dependabot[bot].
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
One pattern fill tiles at most 250,000 times (PatternDef::offsets_covering),
but Scene::fill writes each tile's own pattern fills again, down to the
nesting limit of 8, with nothing bounding the total. A pattern whose tile
art is filled with another pattern (or with itself) multiplies the tile
count at every level: a small document makes EMF export (the engine's
"emf" format, also reachable over MCP and the control channel) run out
of memory or never finish, and the unsaved document is lost with it.
The scene now keeps one tile budget for the whole walk. A fill whose
tiles don't fit in what is left, and every pattern fill after it, is
written as one colour like WMF writes patterns, with a warning.
The test tiles squares filled with dots (over half a million fills before)
and a pattern filled with itself, and checks both stay under the budget.
Reported-by: BS.ehlas.cz
One Repeat makes at most 250,000 instances (a 500 x 500 grid; radial
counts stop at 1,000), but its source may hold another Repeat and every
instance draws that one again: expand() clones the inner Repeat once per
instance, and the renderer, the exporters (expand_deep) and hit testing
evaluate each copy. Nothing bounds the product, so a document under 1 KB
with a 500 x 500 grid of a 500 x 500 grid asks for 62.5 billion
instances: opening it freezes the app or runs it out of memory, and the
unsaved work in other documents is lost with it.
A Repeat now counts the instances the Repeats nested in its source make
in all and keeps only as many of its own as fit in 250,000, the budget
pattern fills already keep (at least one, so the source still shows).
A Repeat with nothing nested keeps all its instances, as before.
The test nests a 500 x 500 grid in a 500 x 500 grid and in a mirror,
counts the instances from the transforms (62.5 billion before) and
checks they stay within the budget, while one grid and a small nest keep
all of theirs.
Reported-by: BS.ehlas.cz
When a constant profile's half-width reaches the local radius of curvature, the smooth inner offset crosses the centre and the non-zero fill leaves a hole. Cut through the centre at those samples; the cut is inside the painted stroke. Varying profiles keep the offset because their crossings can be real caustics, where a cut would show as a slit. Regression test: a radius-5 circle with a 30 pt stroke contains its centre.
Offset smooth points along the curve's normal instead of the flattened polyline's miter (w/cos), which spiked the edge at every sample on tight curves. Fit runs with chord-length Catmull-Rom tangents so a short sample cannot flatten the long segment beside it, and add a coarse wide-circle regression test.
A width profile's sides now run as a cubic Hermite curve through the
points, its slope at each one the weighted harmonic mean of the secants
either side (Fritsch-Carlson). That is C1 continuous and monotone
between points, so it never overshoots them, a width change eases in and
out instead of making a corner, and moving one point only reshapes the
two segments beside it. The ends are flat; two points at one t are still
a step.
The outline samples the curve along the path (densify subdivides each
profile segment) so the smooth width draws on straight runs too, and a
flat profile keeps its old straight pieces.
Hit testing now counts every segment near enough to reach the point, as
the wider side of a farther one can paint past the nearest; gradient
strips reach over projecting dash caps. Tests updated for the new
sampled widths; new tests cover smoothing, monotonicity, locality and
steps.
GradientMesh::remove_point_lines (the Mesh tool's Alt-click,
object.mesh.deletePoint) only checked that the clicked point exists,
then indexed the points above and below or left and right of it by the
mesh's rows and columns. A gradient mesh read from a document is not
checked against its rows and columns, so one with fewer points than
they need (rows 2, columns 1 and 3 points instead of 6) indexes past
the end and panics: the engine guard rolls the command back with an
internal error, and the web app, which has no guard, aborts.
It now returns false for a mesh that isn't valid, as locate (and with
it adding mesh lines), the outline and the mesh lines already do, and
the command reports that nothing could be deleted.
The test Alt-clicks an interior row's point and an interior column's
point of two such meshes and checks nothing is deleted and the mesh is
unchanged.
Reported-by: BS.ehlas.cz
GradientMesh::is_valid and the two mesh envelope checks (envelope_grid
and the envelope's surface) compare the number of points with
(rows + 1) × (cols + 1), multiplying the document's u32 rows and
columns unchecked. With u32::MAX rows and columns the product is 2^64:
debug builds panic on the overflow and release builds wrap it to 0, so
a mesh or a mesh envelope without points passes as valid. Selecting
it, Outline view, hit testing and export then index points that aren't
there (the mesh lines, outline and edges), tessellating it asks for an
impossible capacity, and a mesh envelope with handles to fill in loops
2^64 times making them until memory runs out. On wasm32, where usize
has 32 bits, 65,535 rows and columns are enough, and a panic there
aborts the web app.
The three checks now share grid_len, which counts the points with
checked arithmetic and gives None when the count doesn't fit in a
usize: a grid that can't be counted matches no point list, so it is
malformed like any other wrong count.
The test checks that a u32::MAX × u32::MAX mesh without points isn't
valid and has no lines, outline or quads, that envelope_grid refuses
those counts, and that a mesh envelope with them leaves art in place.
Reported-by: BS.ehlas.cz