Commit Graph
8337 Commits
Author SHA1 Message Date
JonRC 5e3d35c0e0 contributors: add my name for the About window 2026-11-03 01:07:42 -03:00
JonRC e44f5a1b77 Fix #5312: EPS files written by Ghostscript's eps2write open
`gcheck` and `scheck` answered false for every operand. The PLRM (3rd
ed., `gcheck`) says they are true for a simple object, and false only
for a composite one in local VM. The eps2write prolog copies its
initial graphics state into global VM and leaves a value alone only
when `gcheck` is true. So the integers in the `currenthalftone`
dictionary took the copy path, left the stack one item off, and the
next `put` failed with a typecheck in `cp2g`.

Simple objects now answer true. Composites still answer false, since
every composite here is in local VM.
2026-11-03 01:07:29 -03:00
Dylan M. Taylor ed4a663986 DXF export: charge symbol art as it is placed unclipped, test the segment budget
Count nine-slice symbol art with clips off, as place_symbol_art_unclipped
places it, so art crossing the guides is not over-charged; add a test that
the flattening segment budget stops a path independently of the point
budget; format.
2026-11-02 22:33:39 -05:00
dotCooCoo 5df9b2d1e0 .ai save: nine-slice symbol placement counts toward the symbol art limit 2026-11-02 18:59:11 -08:00
dotCooCoo 029661cf78 .ai save: the editing copy places symbol instances through their 9-slice guides
A scaled instance of a symbol with 9-slice guides was written with its art stretched as a whole. The editing copy now places it as the canvas draws it, and leaves the instance out when placing it would pass the symbol art limit.
2026-11-02 18:55:11 -08:00
dotCooCoo f8095edf0d Merge commit 'refs/check/4968-main' into feat/ai-editing-data
# Conflicts:
#	crates/eps/src/lib.rs
#	crates/pdf/src/lib.rs
#	docs/file-format-parity.md
2026-11-02 18:47:39 -08:00
Dylan M. Taylor aaca80c961 Affinity import: bound archive index names and charge failed entry extraction (#5291)
The table chain's records and entry names now have a whole-archive allowance, only the
first name of an id is decoded and kept (no clone into the entry map), and an entry's
size is charged before it is decompressed and stays charged when it fails, with the
failure remembered so a broken entry is not decompressed again.
2026-11-02 21:36:16 -05:00
Dylan M. Taylor f8f9280d75 Repeat test: drop the full-budget expand from the large-source test 2026-11-02 21:31:03 -05:00
Dylan M. Taylor 1f80c018c7 Gradients: compare NaN samples as printed in the stop lookup test 2026-11-02 21:03:30 -05:00
Dylan M. Taylor 572a51894f Affinity import: index the fields of large objects and read four rectangle corners
Object::get scanned every field for each lookup, and a shared object is looked up again
on every visit. Objects with more than 16 fields now keep a sorted tag index (first and
last position), so get and get_last are binary searches with the same first/last-
occurrence results. Rectangle corner types and radii read at most the four entries that
can matter instead of converting the whole arrays.
2026-11-02 20:46:58 -05:00
Dylan M. Taylor f1d4969864 DXF export: bound curve flattening work and expanded symbol art
Flattening a path for DXF capped the points it kept but kept flattening every remaining curve
after the cap, each curve at a fixed tolerance however large it was. Symbol instances placed
inside symbols were cloned and walked once per instance with only the nesting depth bounded, so
a small symbol graph of empty symbols could fan out into billions of visits that write nothing.

- Flatten through vectorcraft_geom::flatten (a bounded number of segments per curve) and stop
  at the first curve past the point or segment budget.
- Charge each symbol instance's art to MAX_SYMBOL_NODES with charge_symbol_art before placing
  it, as the document does elsewhere; once spent, no further instances are expanded.
2026-11-02 20:43:17 -05:00
Dylan M. Taylor 67ef83f155 Gradients: midpoints and freeform points find their stops by binary search
A gradient whose stops are not strictly increasing (repeated offsets, or
a file's unsorted stops) sampled each Classic off-centre midpoint, and
each freeform point when recoloring, by scanning the stops from the
start, so many stops and points cost their product. Such stops now get
an index of the furthest offset reached so far, which never decreases,
and each sample finds the pair of stops sample() would pick by binary
search, giving the same colour for any order of offsets.
2026-11-02 20:39:15 -05:00
Dylan M. Taylor 9a1ac9dad3 Brush and symbol libraries: look definitions up by name
Making a brush or symbol library, and gathering the resources copied
art uses, found each referenced swatch, symbol, pattern, graphic style,
text style and brush by scanning all the definitions of its kind, and
checked library membership and panel order by scanning the lists again,
so a library or document of N definitions cost N squared comparisons.
Each kind is now indexed by name the first time a reference to it is
followed, and the library code looks names up in sets and maps built
once.
2026-11-02 20:38:22 -05:00
Dylan M. Taylor 1d7c8f93ab Export for Screens: name patterns and taken names take linear work
The file name pattern searched the rest of the pattern for a closing
bracket at every opening bracket, so a pattern of many unmatched brackets
took quadratic time to render, in the export and its live preview. A
token's name never holds a bracket, so the search now stops at the next
opening one.

Output naming tried -2, -3 and so on from the start for every file given
a taken name, so one name pattern over many artboards rebuilt and looked
up every number used before for each file. It now goes on from the last
number given for that name, and remembers which file got a name from it.
2026-11-02 20:35:58 -05:00
Dylan M. Taylor 1ad4233846 Page ranges: overlapping intervals skip indices already picked
parse_range walked every index of every interval, so a range that names
the same pages many times ("1-,1-,1-,...") cost the number of parts
times the page count. Picked indices now link to the next unpicked one,
so each page is visited about once whatever the overlap.
2026-11-02 20:33:30 -05:00
Dylan M. Taylor 09d8872f15 Depth limit: merge containers at the cap in one pass over the siblings
limit() replaced each too-deep container by its children with
Vec::splice in the middle of the sibling list, moving every later
sibling each time: a wide list of such containers took quadratic time
to load. The list is now rebuilt in order from a worklist, so each node
moves a bounded number of times.
2026-11-02 20:33:29 -05:00
Dylan M. Taylor f204445119 Repeat: the instance budget counts opacity-mask art and the nodes each copy holds
A Repeat kept up to 250 000 instances whatever its source held, and the
nested-instance count skipped the art of opacity masks. Expanding copied
the whole source for every kept instance, and a Repeat inside an enabled
mask was evaluated again for each outer copy.

The budget now counts the Repeats inside enabled opacity masks, and keeps
a second bound, MAX_REPEAT_COPIES, on the nodes and path anchors copied
in all (nested Repeats included).
2026-11-02 20:33:27 -05:00
Dylan M. Taylor 653f178b28 Affinity import: count path segments and masks in master page copies
Master page copies were charged one unit per shape whatever its number
of segments, and nothing for attached vector or pixel masks, so a master
holding one huge curve or mask could be rebuilt on thousands of pages
within the copy allowance. Copy costs now count every subpath and
segment of a shape's path and its vector mask, and a pixel mask's
pixels.

A node's vector mask also skipped the document range check the node's
own art passes; its control points are now checked as mapped into the
document (non-finite ones included) and an object whose mask reaches
beyond the range is left out, as one whose art does.
2026-11-02 20:33:10 -05:00
dotCooCoo eaab849f42 Document Info: count objects that share an id as separate objects
The walk that skips objects it has already visited keys them by address
instead of id, so two objects that share an id in a damaged file are
both counted. An object reached through a selected group and again on
its own still counts once.
2026-11-02 17:01:29 -08:00
dotCooCoo 33f429972b Merge main (11862f61) into feat/native-save-precision 2026-11-02 16:29:45 -08:00
Brandon ThomasandClaude Opus 5.5 11862f6119 Release 0.21.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.21.0
2026-11-02 19:27:35 -05:00
NetterSilverandClaude Haiku 5.5 6ecf286f6b Drop invalid XML characters from SVG link URLs and aria-labels
Found while testing crates/svg/src/export/nodes.rs:492 (Writer::node, `xlink:href` built with xml_escape)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 21:42:27 +00:00
dotCooCoo e2b6903a3a Symbols: test that a guide dragged on the canvas changes how a scaled instance draws
A 200 x 150 instance of a 9-slice symbol draws its 10 pt corner square about 17.14 pt wide once the left guide is dragged from 25% to 5% in symbol editing mode, as the square then reaches into the stretched center band.
2026-11-02 13:41:39 -08:00
dotCooCoo b63517df7f Symbols: a dragged 9-slice guide keeps its place when saved guides are closer than the minimum band
A guide whose saved position leaves a center band narrower than the minimum no
longer jumps to the art's edge when a drag does not move it closer to the
opposite guide. The 9-slice scaling line in docs/target-app-parity.md no longer
says guides are not drawn in symbol editing mode.
2026-11-02 13:16:49 -08:00
dotCooCoo 537511ae65 Symbols: a dragged 9-slice guide moves with Real-time Drawing off
With Performance > Real-time Drawing and Editing off, tool previews are
applied only on release. A ruler guide being dragged was already exempt;
a symbol's 9-slice guide, a symbol.options preview that sets only
nineSliceGuides, now is too, so the dotted guide follows the pointer
while it is dragged. The release still leaves one Symbol Options step.
2026-11-02 13:03:45 -08:00
dotCooCoo 931e94cdea Symbols: 9-slice guides are shown and dragged on the canvas while editing a symbol
While a symbol with Enable Guides for 9-Slice Scaling on is edited in
symbol editing mode, the canvas draws its two horizontal and two vertical
guides as dotted lines across its art, with any tool and with ruler guides
hidden. With the Selection tool the pointer near a guide shows the Move
cursor, and dragging the guide sets it through `symbol.options`'s
`nineSliceGuides` as one undo step; Escape during the drag puts it back.

A guide within 4 screen pixels takes the press before the art and the
bounding box. A dragged guide stays within the art's box and stops short
of the opposite guide; positions outside the art are not supported yet.
2026-11-02 12:38:33 -08:00
dotCooCoo 2d0fa32d30 Docs: the projection checks compare absolute values
The horizon test compares a point's signed w with the largest absolute
value in the bottom row, and the invertibility test compares the
absolute value of the determinant with the largest absolute value among
its six products, so a negative determinant counts as invertible. A
stored projection that overflows is divided by the largest absolute
value in its bottom row.
2026-11-02 08:53:47 -08:00
dotCooCoo 5f20bbc3e8 Document Info: test that a Repeat object in a selected group is listed once
With Selection Only, a selection that holds a group and the Repeat object
inside it, in either order, lists that Repeat object once.
2026-11-02 08:25:02 -08:00
dotCooCoo b5885e35fa Geometry: a projective map at a small scale maps a far point without overflowing
The power of two that brings a small map's entries up before a point is mapped is now reduced by the power of two of the point's larger coordinate, so no product of an entry and a coordinate reaches 1. A map that mapped a point before the scaling step does so again, and affine_at uses the same entries.
2026-11-02 08:24:29 -08:00
dotCooCoo 0f48ba21e9 Geometry: a projective map at a tiny scale maps points and derivatives as at unit scale
apply and affine_at multiply every entry by the power of two that brings the
largest to at least 1/2 when all entries are below 1/2, so products that would
be subnormal keep their precision. The identity times 5e-324 mapped (0.5, 0.5)
to (0, 0) before. Maps with an entry of 1/2 or more are unchanged.
2026-11-02 08:15:21 -08:00
dotCooCoo ce9002008b Docs: file-format-parity keeps one .ai editing-data paragraph
The editing-data paragraph appeared twelve times, each copy holding one
feature's change. The copies are merged into one paragraph.
2026-11-02 08:10:43 -08:00
dotCooCoo 4494d40c33 Geometry: the inverse of an invertible projective map is kept however small its relative determinant
adjugate_inverse no longer applies the relative determinant test to its result. That test is not symmetric between a map and its inverse: a map whose determinant is 1e-8 of its largest product can have a finite, accurate inverse whose own ratio is near 1e-16, so inverse() was None while is_invertible() was true. The result is now kept when every entry is finite and its determinant, summed from the six products in split form, is not 0.
2026-11-02 07:55:19 -08:00
dotCooCoo d1fb8e540c Geometry: a projective map with entries far apart in size keeps its inverse
The inverse holds every cofactor and the determinant as a mantissa and a
power of two, and forms each entry from their quotient. A map such as
diag(1e-300, 1e300, 1e-300), which counts as invertible, now gets its
inverse diag(1e300, 1e-300, 1e300) instead of none.
2026-11-02 07:45:10 -08:00
dotCooCoo b22912b25a Geometry: the inverse of a projective map with a tiny determinant keeps its w row
When a cofactor of a map with a normal but very small determinant underflows to 0, the
inverse formed from the map itself can have a w row of 0, so every point mapped through it
is refused. The inverse is now kept only when it is itself invertible, and is otherwise
formed from the map divided by a power of two that brings its determinant's products near 1.
2026-11-02 07:37:37 -08:00
dotCooCoo bffb4e6e87 Geometry: a projective map with entries of very different sizes stays invertible
The scale-free determinant test no longer builds a rescaled copy of the map. It
forms each of the six determinant products as a mantissa product and an
exponent sum and compares their sum with the largest of them, so a map such as
diag(1e307, 1e-157, 1e-157), whose rescaled copy overflows, is still invertible
and loads from a document. The rescaled copy is built only to invert a map whose
determinant is not finite or is below the normal range.
2026-11-02 07:21:32 -08:00
dotCooCoo e365c09141 Geometry: a projective map's horizon and invertibility checks do not depend on its scale
A homography means the same map at any nonzero uniform scale of its
entries, but `apply`, `is_invertible` and `inverse` compared w and the
determinant with absolute thresholds. A map stored with small entries
refused every point and could not be inverted, and a map with large
entries drew points past its horizon.

- `apply` refuses a point when w is at most 1e-9 times the largest
  magnitude in the w row.
- A map counts as invertible when its determinant is more than 1e-15
  times the largest of the six products the determinant sums, taken on
  a copy rescaled by a power of two. An inverse whose determinant
  overflows or underflows is taken on that copy.
2026-11-02 07:10:04 -08:00
dotCooCoo 588ff039a9 Type: Tate-chu-yoko offsets in character style definitions stay in range
A character style whose Up/Down or Left/Right offset lies past MAX_COORD
either way now opens with the nearer end, as text runs already do, so
text that takes the style no longer gets the out-of-range offset. An
offset within range is not rewritten.
2026-11-02 06:32:52 -08:00
dotCooCoo 8b249f8275 Document Info: Objects counts each object once
An object with a gradient or a pattern on both its fill and its stroke
counted twice in Objects. It now counts once for each paint kind it uses.

With Selection Only, a selection that holds a group and an object inside
it walked that object twice, so the object counts, the paths' total
length and Repeat Objects counted it twice. The walk now visits each
object once.
2026-11-02 05:06:38 -08:00
dotCooCoo 52d180910d PDF export: spot inks named All or None print as spot inks
In a Separation color space, the colorant name All marks every plate and
None marks no plate (ISO 32000-1:2008, 8.6.6.4). A spot swatch with one of
those names was written under its own name, so its paint printed on every
plate or on none.

PDF export now writes such an ink as All Spot or None Spot, numbered
All Spot 2, All Spot 3 and on when a spot swatch already has that name,
the same rule EPS export uses. The new name is used for fills and strokes,
tint shadings, DeviceN gradients, colorized images and the Lab alternates.
Registration still writes All.
2026-11-02 05:04:46 -08:00
dotCooCoo 750c34c247 Recolor Artwork: the Edit tab shows the colors on a smooth wheel, a segmented wheel or as color bars
Buttons beside the Edit tab's wheel switch between the smooth color wheel,
a segmented wheel of solid patches (24 hues by 5 saturations, its markers
moving as freely as on the smooth wheel) and color bars: one solid bar per
recolored color in row order, where a click selects a bar's row for the
color sliders and a drag moves the row without changing a color. The
dialog opens on the smooth wheel and keeps the choice while it is open;
the dialog field is `display` (`smooth`, `segmented`, `bars`).
2026-11-02 04:35:56 -08:00
dotCooCoo 5ba035aba4 Type: an inline graphic takes its character's manual kerning
A manual kerning value on an inline graphic's character was stored and
shown but did not move the text after it. The graphic's advance now
includes the value, as a letter's does, and in right-to-left horizontal
type its art moves right by the value, so the space goes between it and
the next character in reading order. Auto and Optical kerning still apply
no font pairs to an inline graphic.
2026-11-02 03:45:45 -08:00
dotCooCoo fa8addaa12 Select: Same › Text Fill & Stroke Color selects type whose fill and stroke both match
Select › Same › Text Fill & Stroke Color (select.same.textFillAndStrokeColor) selects the type
objects with a run of characters whose fill and stroke both match the first character of the
first selected type. Type that matches only the fill or only the stroke, or has the fill in one
run and the stroke in another, is left out. The entry follows Text Stroke Color in the menu and
has a row in every catalog.
2026-11-02 03:37:17 -08:00
Rahul Ranjanandrahul05ranjan 3976989cb7 i18n(sv): translate the 22 strings the report listed as missing
The Swedish catalog lacked 22 interface strings: the workspace names
(Essentials, Painting, Typography, Web, Touch), the 1P/2P/3P view
preset names and the Hindi digit-script name.

Adds them with the vocabulary the catalog already uses (Utskrift,
Teckenstorlek). After this the report lists nothing missing for sv.

Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
2026-11-02 11:27:09 +00:00
dotCooCoo 442ae20e14 SVG: made-up ids skip every id the file names
An id the importer writes into an element no longer matches a style sheet's
#id selector or a <use> link that no element matched before. The ids are
read from the decoded attribute values and text of the file, once.
2026-11-02 03:22:10 -08:00
Rahul Ranjanandrahul05ranjan 2a51404e74 i18n(ru): translate the 130 strings the report listed as missing
The Russian catalog covered 6,126 of the 6,256 interface strings: the
workspace names (Essentials, Painting, Typography…), their Reset
entries, the artboard and dimension annotations, the PDF and export
preset names, the library panel sentences and several control-channel
messages still fell back to English.

Adds the 130 missing entries in the catalog's TSV format with the
Illustrator-style vocabulary the catalog already uses (монтажная
область, выносные линии, плашечные цвета, трассировка). After this the
report lists nothing missing for ru.

Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
2026-11-02 11:17:49 +00:00
dotCooCoo 7c7a127ec8 SVG: a thin ellipse far from the origin imports with standard handles
An ellipse whose radius is smaller than the point tolerance at its coordinates, but at least two
f32 steps, now imports as the four-anchor ellipse instead of usvg's curves with rounded handles.
2026-11-02 03:09:08 -08:00
dotCooCoo b4d3ae41bf SVG: made-up ids skip the file's taken ids once
Each id made up on import starts where the last one stopped instead of at
the number of ids made so far, so a file that already holds many of the
made-up ids does not make every new id try them all again. The ids made
up are the same.
2026-11-02 03:04:04 -08:00
dotCooCoo 85b6c2a5eb SVG: ids made up on import never repeat an id in the file
The import gives an element an id when it needs to find it again in
usvg's tree (links, symbol uses, events, data attributes, and now
circles and ellipses). A made-up id could repeat one the file already
had, such as vectorcraft-id-0, so a <use> or paint reference to that
element showed the other one. Made-up ids now skip every id in the file.

Circles and ellipses in a file whose style sheet has an [id selector get
no made-up id and keep usvg's curves, so the selector matches the same
elements as before.
2026-11-02 02:59:51 -08:00
dotCooCoo 820a750310 SVG: a small circle far from the origin imports as a four-anchor ellipse
The tolerance that compares the points usvg makes of a <circle> or
<ellipse> grew with the center's distance from the origin, so a circle
of radius 400 at x 40,000,000 fell back to eight anchors. The tolerance
is now a few f32 steps at the size of the coordinates, and the on-ellipse
check allows the same steps relative to the smaller radius.
2026-11-02 02:53:34 -08:00
dotCooCoo 60e1600caa SVG: circles and ellipses import as four-anchor ellipses
usvg turns each <circle> and <ellipse> into arcs and then into a number
of cubics that depends on f32 rounding: an ellipse that export wrote as
<ellipse> could come back with eight anchors instead of the four it was
written from. The renderer flattens the two outlines differently, so a
round trip of a flat filled ellipse redrew 158 edge pixels by up to 43
levels, and prop_svg::plain_art_survives_visually failed on main.

Import now records the ids of <circle> and <ellipse> elements in the
prepass and rebuilds each as the ellipse VectorCraft draws: four smooth
anchors on the axes, starting at the right as SVG defines, with handles
KAPPA of the radii long, then transformed as before.

Tests: prop_svg::filled_ellipse_on_the_artboard_edge_survives_visually
(the failing case) and basic_shapes::circles_and_ellipses_import_as_the_ellipse_drawn
fail without the change.
2026-11-02 02:49:36 -08:00