A pixel on the edge of a cut-out image with an alpha of 128 or more was
traced with its own color. When that color was a darkened edge tone, it
took whichever palette color lay nearest, and the edge traced as 1 px
slivers of a color that appears nowhere near it.
In Color and Grayscale, a pixel with an alpha of 128 to 254 next to a
pixel under half opaque now takes a color of the fully opaque pixels
within two pixels of it when its own color is that color or a darker
shade of it, counting only pixels it reaches without crossing a
transparent gap; of several such colors, it takes the one the most of
those pixels have. It keeps its own color when one of those pixels has
it, or when its color is not a shade of any of theirs, so a thin
semi-transparent outline of another color stays as it is. Ties go to the color nearest its own, by its gray
in Grayscale. Opaque images trace as before.
recolor.colors and recolor.reduce were unavailable with nothing selected, so
listing a document's colors needed a select.all first, which also changed the
selection. Both now take ids and read the selection when ids is left out.
With nothing selected they read the objects Select All takes and the objects
inside them: hidden and locked objects are left out with what they hold, also
inside a group, as are the objects of hidden, locked and template layers, and
in isolation mode only the isolated art is read. The selection is left
unchanged. An object named in ids or selected is read with everything it
holds, and an object listed twice, or listed together with a group that holds
it, counts once. recolor.apply still recolors only the selection.
The params descriptions of Same Fill Color, Stroke Color, Fill & Stroke,
Graphic Style and Appearance Attribute say that a member path of a
compound path selected first compares as its compound path, except in
isolation mode inside that compound path.
Fitting straightens the pieces of an outline that lie close to a line, and
Snap Curves To Lines straightens more of them. The outline of a single
pixel, or of a region one pixel thick along an edge such as an opaque row
of a mid tone under a light shape, can then have all its pieces on one
line. The trace held a selectable path with no area, for example a path
140 x 0 px with Snap Curves To Lines.
In a Color, Grayscale or Black and White trace with Fills, a fitted
outline or hole whose area is below 1e-6 px^2 is now left out. A path
with no area paints nothing, so the traced result paints the same pixels
as before. Without Fills, the 1 px stroke that outlines each area draws
such a loop, so it is kept. A single closed loop keeps its area as
before.
Review of #3925: the spooled job turned every sheet wider than tall, so
custom paper that is wide in Portrait (a 4 × 2 in label) went to the queue
tall, and a Portrait + Transverse page was turned back. Only the sheets
whose paper the layout turned to landscape (not transverse) are written
upright now.
The desktop print service asked for upright pages on every platform, but
craft-print already turns each sheet to its own orientation on Windows:
only macOS asks for them now; Linux keeps main's behaviour.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Pencil and PencilClose entries in cursors.toml, the PencilClose doc
comment and the closing-cursor line in docs/target-app-parity.md said Alt
held while drawing closes the path. A drag started with Shift or Alt
held draws an open straight line and does not show the closing cursor,
so the texts now say Alt is pressed after the drag starts and name that
exception.
Select > Same > Fill Color, Stroke Color, Stroke Weight, Fill & Stroke,
Opacity, Blending Mode, Appearance and Shape skipped containers, so they
never selected a compound path, even when the compound path was the
object selected first. With a member path of a compound path selected
first, these commands and Same Graphic Style and Appearance Attribute
compared the other objects with the member, which has no paint or style
of its own. Graphic Style then reported that the selection had no
graphic style, and Appearance Attribute that the object had no fill,
stroke or effect.
The eight commands now match a compound path on its own paint and
attributes, as select.color does, and skip its member paths. When a
member path is selected first, all ten commands compare the other
objects with its compound path. In isolation mode inside a compound
path, the member paths are matched as before.
A landscape print job was a page wider than tall, and `lp` on macOS puts
each page on the paper as it is: on upright Letter it printed unturned,
cut off on the right (checked with cupsfilter through an AirPrint
driver). Saving the PDF and printing it from Preview worked because
Preview turns the page.
`file.print` takes `forPrinter`: a page wider than tall is written
upright, the art turned a quarter anticlockwise as a printer driver turns
a landscape job. Page by page, so auto-rotated jobs that mix wide and tall
artboards print right too. The desktop print service asks for it
(`PrintService::turns_landscape`); saved PDFs, PostScript, the preview and
the browser's print dialog keep landscape pages as they are.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With Alt key toggles to Smooth Tool off, holding Alt when a Pencil drag
starts now draws one straight segment from the press point to the
pointer, at the angle the pointer takes, as Shift does at 45° steps.
Pressed on an end of a selected open path, the line continues the path
from that end. Both ends snap to Smart Guides as a stroke's ends do, and
the line stays open when Alt is still held at the release. Shift and Alt
together keep the 45° steps.
Before, a drag started and released with Alt held drew a closed freehand
path. Alt pressed after the drag has started still closes the path at
the release, and with Alt key toggles to Smooth Tool on, Alt still switches
to the Smooth tool. Before a Pencil press, Alt no longer shows the closing
cursor.
Docs: docs/target-app-parity.md, docs/ui-parity.md.
A save without options reuses the options the document was last saved
with in that format. Their artboard, artboards and range name artboards
by number, so once artboards were deleted or moved, the save wrote
other artboards or was refused: with three artboards, a PDF saved with
range "2" wrote the third artboard after the first was deleted, and one
saved with range "1, 3" was refused after the third was deleted. The
same held for SVG saves, a legacy .ai, and .ai and native files saved
with separateArtboards. file.formatOptions and SVG Options showed the
same numbers.
The document now keeps the ids of the artboards it had when it was
saved, and the remembered pick follows them: an artboard deleted since
is left out, one that moved is named by its new number, and the rest of
the pick is kept. A range open at its end ("2-") is written out as the
artboards it named once artboards are added. When none of the artboards
the save used is left, the pick is left out, and the save writes the
artboards it writes when none is named. A range of "all" stays as it
is. An artboard given to the save is still refused when the document
does not have it.
A new artboard no longer takes the id of an artboard deleted earlier in
the same document (Document::retire_artboard_id), or of one an undo took
away: the highest id given out is shared with the undo history, so a
new artboard is not taken for an earlier one.
file.revert keeps the pick only when the file read back has the
artboards the document had at the save, with the same ids in the same
order. A PDF saved with a range holds only those pages, and one saved
without preserveEditing reads back with new ids in page order, so their
picks are left out.
Create Outlines on a frame of threaded text removed the frame, and the
thread's re-flow then added the frame's text to the story, as it does
for a frame that was deleted. The other frames held that text a second
time while the outlines drew it. Object › Expand, and Expand Appearance
where it outlines type or groups it with a shadow, outer glow or Crop
Marks, did the same.
An outlined frame now leaves its thread with the text it shows, and the
other frames keep the text they hold. When the thread's last frame is
outlined, the text it has no room for goes after the text of the last
frame that stays. When Expand Appearance keeps a frame as type in a
group, the type takes the frame's place in the thread. A thread left
with one frame is no longer a thread, and undo restores the thread.
Docs: docs/mcp.md, docs/target-app-parity.md.
Review of #3924: in a flat area of ` <<(` (CMYK or RGBA 32, 60, 60, 40) in an uncompressed image, each `<<` after white space was read as a dictionary, and each read its string, which never ends, to 64 KiB past it: about 16 K bytes a pixel, so a few hundred bytes of it spent the budget and the file was refused.
Whether a byte in a literal string is escaped doesn't depend on where reading began (the run of `\` before it lies after any `(` before it), so the string from any `(` ends after the first `)` that takes the file-wide count of `(` less `)` below the count at that `(`. Those ends are found for the whole file in three linear passes the first time a string read in skipped text runs past 64 bytes: forward for the final count, back for the `(` whose string never ends, forward pairing the rest. Ends further than 64 bytes are kept (at most one for every 16 bytes of the file; a file built to nest past that is refused); nearer ones are read again. So entering a string costs at most 64 bytes, charged as one. A comment's end of line is kept for every comment read, not only the last.
CodeRabbit findings #32, #38, #41, #44, #102:
- A frame (and a PDF export) draws symbol instances holding at most 16M objects together
(`vectorcraft_doc::MAX_SYMBOL_NODES`): nested instances multiplied past the depth limits.
- A dotted guide whose control polygon would make more than 100,000 dashes is drawn solid.
- Groups blending with their backdrop, drawn twice each, are drawn isolated past 4 nested.
- An image is decoded only while it and the images paints hold fit twice the decode allowance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #3924: every `<<` in a string, comment or stream's data was read as a dictionary of its own, and in a run of `<` (an uncompressed image's flat area of grey 60) every position is one, each read to past the run, so about N²/2 bytes for N `<` and a 1 MiB run refused the file.
The reader starts a dictionary there only where its repair scan tries one (after white space or a dictionary's `>>`) or after an object's `N G obj`: so only those `<<` are read, in a run of `<` only the first. A reading goes on as far as 64 KiB past the last `<<` opened on the way, looks at each nested dictionary's `>>` for a stream and marks each `<<`, so it is not read again; a comment's end of line is kept, as many readings cross the same comment; the `<<<` second-`<` reading in the main pass goes (the reader never starts there). The dictionary text a stream check parses is charged to the budget.
CodeRabbit findings #2, #16, #31, #103, #114, #124, #162, #179, #182, #183, #185:
- Shortcut import review: example names for each code in one pass over the rows.
- Gradients: a midpoint stop is mixed from its two stops; freeform recolor samples its
increasing positions with one advancing search (`Gradient::sample_increasing_with`).
- Recovery: image entry names are looked up by address, and dead ones swept when the map
doubles, not on every image.
- Script checker: include cycles from strongly connected components (iterative Tarjan), not
a reachability row per file.
- Batch step references copy at most 256 MB of values for one step.
- DOCX: style ids over 256 bytes are ignored where defined and where used.
- DXF: layouts keep at most 4 MB of names; a hatch's loops count against the node limit.
- Freehand and Liquify: past 1000 samples a drag previews every n/1000 samples, strokes keep
at most 100,000 samples, and the stroke kept is previewed in full before it is committed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit findings #7, #72, #92, #94, #95, #99, #100, #140, #158, #174:
- Link search hashes at most the size it looked at (a growing file no longer matches).
- Resolving links reads, decodes and previews each linked file once, shared by every key.
- Placed documents read inside one another resolve links at most 8 deep, never one already
being read.
- A data set's image path and the placed-document rereader follow stored paths through
`document_path` (no other computer's share).
- The Links panel looks at a linked file within the roots of the automation caller that
opened the document.
- A fonts folder set by a confined caller is scanned without following links out of it.
- Recovery refuses an area folder that is a link or another account's.
- Atomic saves keep the replaced file's group and Linux access ACL, or make the replacement
owner-only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #3919: the scan raised its walk-back floor at every `>>stream`, so one inside a literal string moved the floor past the `<<` of the cross-reference stream around it, and that stream was never looked at. The backward walk also counted a `>>` inside a string, and missed a comment between `>>` and `stream`.
The scan now reads the file forward once, as the reader does: literal strings (nested, escaped), hex strings and comments are skipped, `<<` and `>>` pair on a stack, and a dictionary followed by `stream` is a stream's, whose data is skipped. Because the reader can still start at a `<<` inside skipped text (a cross-reference offset or its repair scan), each such `<<` is read as a dictionary of its own, up to 64 KiB, within a budget of four times the file plus 1 MiB; a crafted file that spends it is refused. The scan stays linear.
CodeRabbit findings #4, #34, #46, #51, #77, #101, #170, #171, #188:
- `vectorcraft_geom::flatten`: kurbo's flatten with each curve's tolerance raised so it makes
at most ~4096 segments (non-finite curves become a line); the canvas outline and
variable-width strokes use it.
- Plug-ins: a heavy filter's budget is the host's `Limits::heavy_fuel`/`heavy_wall_time_ms`,
never more; a start function runs on one fuel slice; the effect cache holds at most ~1M
anchors of inputs and results, and doesn't keep a result past a quarter of that.
- Loaded textures are cached by their PNG (base64 spacing doesn't matter), and live textures
hold at most 512 MB together.
- Revolve draws the source path when the faces' paint copies would pass 256 MB.
- Color books: names with prefix and suffix take at most 64 MB.
- ICC: localized-name (`mluc`) records over 1 MB together are refused before moxcms copies them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit findings #58, #75, #82, #113, #159, #175, #187:
- Live evaluation (expansion, envelope mapping and everything inside) makes at most 4M
objects (`MAX_GENERATED`); deep expansion counts what each live object makes, and envelope
image pieces count before they are cut.
- Clipping runs copy at most 8M clip path elements together; past it runs are plain groups.
- Exports read at most 256 placed documents; the rest show their previews.
- Graph design and marker budgets count opacity-mask art (`Node::count_with_masks`).
- Native files read at most 32M anchors (4M on wasm) of paths; the web build allows 1 GiB of
entries after the JSON.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hayro-postscript now comes from craft-libs' vendored copy, whose token scanner skips << and >>
runs without recursion, instead of crates.io 0.1.0. The rev also brings the reader input bounds
and crash fixes from craft-libs #257 and #261. The pdf-deps check requires hayro-postscript from
craft-libs; ASSETS.md, NOTICE and docs name the new rev. The asset hashes are unchanged.
* Build: craft-pdf, hayro and krilla at craft-libs main
Picks up the input bounds that landed after ca50d63b: zero-width xref streams, the JBIG2
referred-to segment count, the graphics-state stack depth and decoded PNG sizes.
* Build: craft-libs 9f75d3dc, with hayro-cmap from craft-libs
Adds the /W2 CID-width bound (#254) and takes hayro-cmap from craft-libs' vendored copy,
which skips malformed CMap parts, instead of crates.io 0.1.0.
* Build: the pdf-deps check, ASSETS.md, NOTICE and docs name craft-libs 9f75d3dc
The check also requires hayro-cmap from craft-libs. The asset hashes are unchanged.