Gujarati text from a PDF came in jumbled even with its fonts installed:
- A glyph with no advance (a vowel sign or anusvara set over its letter)
was taken for vertical type and opened as an object of its own, which
type then drew on a dotted circle. In a font the file writes
horizontally it now stays on its line.
- A line read glyph by glyph is not always the text that was set: the
file draws a vowel sign before the consonant it follows, and may not
name a conjunct. A line with characters of U+0900 to U+0DFF is now set
again and its glyphs compared with the ink the file drew; when they
differ the line keeps the file's glyphs as outlines, with a note.
Tests: crates/pdf/src/tests/reshaped.rs (two fail without the change).
A linear, radial, conic or diamond gradient has a spread: Pad (the
default, as before), Repeat or Reflect, with a count of 1 to 100
(VectorCraft's own limit). Repeat and Reflect run the stops that many
times along the vector (around the turn for a conic) and keep going past
both ends, Reflect running every other run backwards, as SVG's
spreadMethod does.
The Gradient panel has a Spread menu and a Repeats field, and the
Gradient tool marks where the runs meet. paint.setFill and
paint.editGradient take `spread` and `repeats`; bad values are errors.
Presets, swatches, blends, strokes along or across a path, Outline
Stroke and Object › Expand follow the spread; Expand gives each run the
same whole number of objects, and Outline Stroke's mesh keeps the color
on each side of a sharp step between two stops at one offset.
SVG writes and reads spreadMethod: SVG import now reads a repeating or
reflecting gradient as one that repeats or reflects once, instead of
expanding its stops over the shape. CSS writes repeating gradients.
PDF writes a conic's sweep shading over one run. PDF, EPS, legacy .ai
and the .ai editing data write the other kinds with their stops
repeated across the shape: at most 256 runs and 16,384 stops (256 stops
in .ai files), and for a radial or diamond no farther than 65,536 runs
from its center, with a warning past those. PDF import now finds sharp
steps in any axial or radial shading instead of blurring them.
Docs: docs/mcp.md (Spread), docs/target-app-parity.md, docs/gaps.md.
A new gradient kind, `diamond`, runs its stops from the center (the
start) out to a square with a corner at the end of the vector; the
aspect stretches the square across the vector into a rhombus. The
Gradient panel's Type row has a Diamond button, the Gradient tool draws
the square with the aspect handle and center ring, and a gradient
fitted to an object covers its box.
- Model and canvas: the level is |x| + |y| in the square's frame; a
square with no area (no vector, an aspect of 0, numbers out of range)
paints the last stop. The canvas draws it as pixels, out to a
stroke's miters, caps and arrowheads. Transforms keep the vector and
take the mapped size across it as the aspect.
- Commands: `paint.setFill`, `paint.editGradient` and
`paint.setGradientGeom` take `kind: "diamond"`; a negative aspect or
a focal point is an error. A conic or diamond gradient that a plug-in
returns with a focal point loses the focal point.
- Gradient tool: dragging the aspect handle of a radial or diamond
gradient whose file gave it a negative aspect now sets a positive
aspect; only a conic's negative aspect mirrors it.
- Export: PDF, SVG, EPS and legacy .ai write four linear gradients, one
for each quarter of the square, clipped to the shape (PDF as axial
shadings). PDF and SVG write an image for translucent stops, a
dithered fill or a flat square, and for the Perceptual method unless
PDF writes the stops as spot color shadings. SVG writes a diamond on
type characters as a radial gradient through the corners, and so
does CSS. A swatch library saved as CSS gets four corner layers, a
value for the `background` property (a circle when the swatch is
turned by an angle that is not a multiple of 90 degrees). The .ai
editing data holds a radial gradient through the corners. The
document exports give a warning.
- SVG export: a stroke drawn as an image, as conic wedges or as diamond
quarters now covers the miters, caps and arrowheads that reach past
its paint box.
- PDF import reads a function-based shading whose color is the same
along each square about the origin of its space as a diamond.
- Object > Expand gives nested squares or a mesh of rings.
Docs: docs/mcp.md, docs/target-app-parity.md.
A fourth gradient kind, conic (also accepted as `angle` or `conical`),
sweeps the stops once around the center (the gradient's start),
clockwise on the page from the vector, with a seam where the last stop
meets the first. A negative aspect mirrors the sweep, and reflecting an
object turns its sweep the other way, also for a conic without a
vector or with a flat extent.
- Gradient panel: a Conic button in the Type row; the Aspect Ratio
field works for conic gradients and keeps a mirrored sweep mirrored.
- Gradient tool: the extent ellipse, aspect handle and center ring of a
radial gradient, without a focal point.
- Rendering, swatch chips, transforms, warps, plug-in round trips, and
Object > Expand into wedges or a mesh of columns around the center.
- Commands: a negative `aspect` is an error for linear and radial
gradients, and so is a `focal` point for a conic gradient.
- PDF export writes a function-based shading whose calculator function
turns the angle into the ramp, and PDF import reads a function-based
shading that turns around the origin of its space back as a conic
gradient, also when that origin lies outside the painted area. A
PDF/A file, or a conic with more than 97 stops, gets an image
instead.
- SVG export writes 360 solid wedges clipped to the shape for opaque
stops and an image otherwise, and a linear gradient on characters.
- CSS export writes `conic-gradient(from ...)`, with the stops mirrored
for a mirrored sweep.
- EPS and legacy .ai export write 360 solid wedges; the .ai editing data
holds a linear gradient along the vector with the same stops.
- .af files: conical gradients open as conic gradients.
- An export that writes the gradient in another form gives a warning.
Supersedes #2326 and includes switch87's code from it.
Co-authored-by: Gert Pellin <gert@pellin.be>
Each artboard can carry a guide label: one of the layer colors or a
custom RGB. Guides that belong to an artboard draw in its label color,
and guides without an artboard, or on an artboard without a label, keep
the guide color from the preferences. Selected guides still draw in the
selection color.
- Artboard Options has a Guide Label row (None, the layer colors with
swatches, or Custom with a color swatch).
- artboard.new and artboard.setProps take guideLabel: null, a layer
color's name, or #rrggbb. Any other value is an error. The inspect
summary lists guideLabel when one is set.
- The native format saves the label and skips it when unset; a preset
index outside the layer colors reads as no label.
- artboard.setProps no longer adds an undo step when nothing changes.
Docs: docs/mcp.md, docs/target-app-parity.md.
A group with a visible member that carries a geometry, Pathfinder or 3D effect, also in an opacity mask, keeps its previous copies. Copies stop at the budget with what each copy's own effects make, and a group's copies count the art of opacity masks.
The PDF and SVG tests for a group's image copies now expect the original on top.
Editable PDF import derived characters from glyph Unicode/CID mappings
even when the enclosing marked-content sequence supplied /ActualText.
A span that paints "xy" but says "AB", followed by "C", imported "xyC".
Track the outermost replacement owner per span (absent, explicitly
empty, consumed), decode it with the existing PDF text-string decoder,
emit it once at the first placeable glyph, and let the remaining glyphs
of the span advance the pen without phantom character slots or
inferred spaces. Authored stretches keep their logical order while
adjacent ordinary text still goes through visual-to-logical
reconstruction. Glyph identity still drives font matching. Depth and
size are bounded; oversized or unbalanced spans fall back to glyph
mappings with a warning.
Outlines mode and PDF export are unchanged. Parity docs updated,
scores unchanged.
Fixes#6165.
The walk measured every object's visual box to leave out what is off
the page. A plain group or layer is measured as the union of its
members, and each member then measured itself again when it was
written, so an object in plain groups was measured once for each level
it sits at.
Measuring a plain group or layer now keeps its members' boxes in a
tree, and each member uses its box when it is written. A container
written whatever its box is (a PDF layer's group, placed symbol art) is
not measured. A knockout group's elements are measured for the group's
box and again for the list of elements on the page, and that second box
is used for the element and for its mask. Objects in a live container
(Blend, Envelope, Repeat, Intertwine) are still measured with the
container and again after it is expanded. Node::visual_of_members holds
the formula that Node::visual also uses, so the boxes and every decision
are unchanged.
The output is byte for byte the same on one open path of 200,000
anchors, 20,000 rectangles in 200 groups of 100, and 5,000 point type
objects, and on a document of plain, see-through, clipping and knockout
groups on and off the page, each exported with default settings, with
uncompressed text and with PDF layers.
Each visible object with a URL (Attributes panel) is written as a Link
annotation with a URI action over its bounds on the page, trimmed to its
clipping groups. Hidden objects, non-printing layers and printed sheets get
none; PDF/X files leave them out with a warning. URIs are percent-encoded
to 7-bit ASCII.
An unclipped opacity mask is drawn on a white backdrop. When an image in
the mask art could not be written (a colorized image whose color channel
is over the size limit, an image that can't be decoded, or one placed
beyond the coordinates a file holds), the backdrop stayed white and the
masked object showed in full where the image should hide it.
The exporter now records an image left out while it writes mask art,
and leaves the masked object out instead of applying the incomplete
mask, with a warning. A failure in a nested mask makes the outer mask
incomplete too.
PDF export (and the PDF-compatible .ai it writes) cut each gradient mesh
patch into 8 x 8 flat-coloured quads. Viewers showed colour bands and
hairline seams between them, and files with many meshes became very
large and slow to write.
An opaque mesh is now written as one Type 6 (Coons patch) shading
(ISO 32000-1, 8.7.4.5.7) filling its outline, through the PDF writer's
MeshGradient: each patch blends its corner colours bilinearly, as the
canvas draws it. Colours go through page_color as other fills do, and a
mesh mixing RGB with other models goes through RGB. A mesh with
see-through points, an invalid one, or one past the coordinate range
keeps the quads.
Envelope Options stored Anti-Alias and Preserve Shape Using, but nothing that
draws an image read them: a distorted image was always smoothed and always
cut into clip groups.
- With Anti-Alias off, each piece of a distorted image is marked pixelated
(saved as `pixelated`). The renderer draws it with nearest-neighbor
sampling and without mips, SVG export writes `image-rendering: pixelated`,
and PDF export writes it without /Interpolate.
- With Preserve Shape Using Transparency, each piece is the image with a
white opacity mask in the piece's shape instead of a clip group. The
renderer paints an image whose mask is one plain white path straight into
that path, as it does for an image in a clip group, when the path lies
inside the image's frame.
Docs: docs/mcp.md, docs/target-app-parity.md.
dithered_perceptual_gradients_export_as_images_with_noise_on_the_fill
compares two exports byte for byte: a stroke's file with dither and
without. The export dates a file by the clock, in whole seconds
(metadata::output_time, unless SOURCE_DATE_EPOCH is set), and the
tests' `uncompressed` helper gave no date of its own. So the test
failed whenever a second ticked between the two exports: the files
then differed in /CreationDate, /ModDate, the XMP dates and the IDs
made from them, and in nothing else. CI hit it on #3807.
With the wall clock moved a second on each read (an LD_PRELOAD shim
on clock_gettime), the test failed 5 runs of 5, and it was the only
one of vectorcraft-pdf's 380 tests that did. With the helper's date
fixed it passes, as do the other 379.
The helper now dates its exports `created: Some(0)`, as the crate's
other tests that compare whole files do (tests_editing, tests_pdf13,
tests_webview).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
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).
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.
The engine built the PDF options without a creation date, so the
writer dated every PDF and PDF-compatible .ai file with the time it
was written. A document created on 2020-01-01 (File › File Info
shows the date) exported as a PDF with today's date in /CreationDate
and xmp:CreateDate. SVG export with the metadata option already
writes the document's own date.
The options now carry the document's File Info created date and the
time of writing as the modification date. A document without a
creation date is dated with the time of writing, as before, so PDF/X
files still get their required date. The PDF writer writes one date
for both, so the export then sets /ModDate, xmp:ModifyDate and the
PDF/A history dates to the time of writing; /CreationDate and
xmp:CreateDate keep the document's date. The page information mark
and the embedded editing data's modification date keep the time of
writing.
Docs: docs/mcp.md (File Info), docs/target-app-parity.md.
When the PDF writer keeps a PNG, GIF or WebP image as it is, it accepts
the image once its header reads, and it reports pixel data that it
can't decode only when it finishes the file. An image whose pixel data
was cut short or damaged therefore failed the whole export with "PDF
writer error: Image(...)", and nothing was written. This happened in
Save as PDF, .ai saves, Save a Copy, Print, Export for Screens and
native files with PDF compatibility, and with On Copy: PDF turned on,
Copy left the PDF off the clipboard.
When the writer fails on an image's pixel data, the export now runs
again with that image left out. Its warnings hold the note that raster
exports give, once: "an image couldn't be decoded and is missing from
the picture". This note replaces the writer's "image '<key>' could not
be decoded and was skipped", which named the image by an internal key.
An export without such an image runs once, as before. A .ai file keeps
the image in its editing data and reopens with it.
Docs: docs/mcp.md (PDF options).
Symbol instances are placed through a transform as images are, so the
Transform effect now moves and copies both (transform_placed), sharing one
copy budget (MAX_COPY_OBJECTS). Copies of either stop before one reaches
past MAX_REACH, as a path's do (distort::within_reach).
Adds end-to-end export tests for a group's image copies in svg and pdf.
hayro-syntax's Dict::entries still unwraps each value it reads, so a
dictionary cut short (or with a stray token) aborts the import. The
craft-libs ArrayIter now stops at the first unreadable item instead of
panicking, which also means a hostile item after a self-referencing
function or colour space is silently dropped from nests_without_end.
- import_color: `readable(array)` checks every item parses; `items()`
yields nothing for an unreadable array; `entries()` reads a dictionary
key by key through get_raw, skipping values that don't parse.
- pages/editing/import_scan: use the helpers instead of raw_iter() and
Dict::entries().
- nests_without_end refuses a file with an unreadable array.
- Tests: two refusal cases in pdf tests and an .ai cut inside an array
or dictionary in engine/tests/import_fuzz.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcHisW1vt6pkVPYbzL6Cpg
Use automatic vector smooth shading without a separate control.
Enable Cap by default and keep continuous rim curves and bounded seam joins.
Integrate the shared dialog with the other three 3D effects.
The shared PDF reader (craft_pdf::syntax, craft-libs ca50d63b) indexes
a dictionary by skipping its values. Its skip accepts some values that
its parse refuses, such as a name with a broken # escape (/#zz) and a
number such as 12-.5. Dict::entries unwraps the parse of each value
(vendor/hayro-syntax/src/object/dict.rs:104). The check in pages.rs
that refuses functions and color spaces built on one another without
end (nests_without_end) calls entries on every dictionary of a file
when the file opens. So document.pdfInfo, PDF import,
clipboard.importPdf and loading a .ai panicked on such a file, and
three import fuzz tests fail on main with PROPTEST_RNG_SEED=1182 and
PROPTEST_CASES=20000.
The check now reads a dictionary's values with keys and get_raw and
leaves out a value that doesn't parse.
A test in crates/pdf puts each of those values in a file with a plain
function, which opens, and in a file with a function that stitches
itself, which is still refused.
hayro-syntax 0.8.0 reads arrays and dictionaries 64 deep and no deeper wherever they are (the
file's text, object streams, the content of pages, forms, patterns, glyphs and masks), walks the
page tree without recursion and follows a chain of object streams once: what 0.7.2 overflowed
the stack on. hayro-interpret 0.8.0 comes with it (a new device interface: the importer's device
takes it as before). Our raw pre-scan of the file's text and page content goes: the reader's own
cap covers more, and the scan miscounted the bytes of unfiltered binary streams.
What 0.8.0 still builds by recursion with no cap is refused when the file opens: functions and
colour spaces built on themselves or on one another more than 16 deep, and calculator functions
whose procedures nest more than 64 deep. The import runs on a 16 MB thread, as the SVG importer
does.
hayro-syntax 0.8 turns on flate2's zlib-rs backend, which the whole app then deflates with: a
compressed native file is the same gzip stream as before (header and data), but its deflate
blocks now depend on how the data is fed, so the streaming test checks the header and the
unpacked data instead of the bytes.
Every symbol instance was measured as its transform applied to the
20 × 20 square around the origin, which symbols made in the app keep
their art in. The SVG and DXF importers keep symbol art as drawn, so
the box of an instance of an imported symbol did not match its art.
The SVG Star instance at x 20, y 30 draws from (20, 30) to (40, 50)
and was measured from (10, 20) to (30, 40). Align moved such an
instance by the wrong amount, clicks and marquees beside its art
selected it, and the canvas, exports and prints left it out when its
art reached the view or the page and the square did not.
An instance is now measured by its symbol's art, which
Document::symbol_boxes looks up in the document each time
(Node::geometric_bounds_with, visual_bounds_with and
reach_bounds_with). With Use Preview Bounds on, the strokes in the art
count at their own weight, as the canvas draws them, and
object.setBounds counts them at that weight when it sizes an instance
or an object that holds one. When strokes do not scale and a width or
height of 0 or more is smaller than what a path's stroke adds,
object.setBounds now gives the path no width or height, as it does
for an instance whose art has strokes; before, it mirrored the path.
With Use Preview Bounds on, object.setBounds now measures the margins
that strokes add object by object, the members of groups and the
objects in symbol instances' art included, and solves for the width,
height and reference point asked for when the width and height are 0
or more. Before, it measured the margins of the whole selection and
could miss the size or the reference point asked for when strokes did
not scale and the objects' strokes added different margins, and when
strokes scaled and the width and height were out of proportion, even
for one path. A clip group, a Shaper Group, a compound path, type, and
a group or instance with effects or an opacity mask of its own are each
measured as one object, and so is an instance in perspective or one
turned or slanted relative to the bounding box, other than by quarter
turns. When the selection holds more than 4,096 objects to measure this
way, each selected object is measured as one. Instances inside symbol
art are measured by their own symbol's art. An instance is measured as
the square when its symbol is missing, when its symbol or a symbol
nested in its art holds an instance of itself, and when symbols nest 32
deep in its art.
Align and Distribute, the Transform panel, the bounding box and the
selection outline, clicks, the marquee, document.inspect and
document.node, Fit to Artwork Bounds and Fit to Selected Art, canvas
culling, thumbnails, the SVG filter region of an instance with a
raster effect, raster effect images, Export Selection, the SVG of an
artboard, printing, and the PDF, EPS, EMF/WMF and DXF writers use the
new box. The renderer also culls an instance by the effects and
brushed strokes in its art, and it culls a blend, envelope or repeat
by the brush art of what it holds and the effects in the art of the
instances in it. It culls an instance with geometry effects of its own
by the effects in its art after those geometry effects reshape the art.
It measures the effects in a symbol's art once for each symbol and
linear part of the transform that places it. For symbols whose art
holds instances, it measures at most 4,096 placements a frame, and at
most 4,096 more for each pattern tile it rasterizes and each placed
document it draws. Past that, it estimates the reach from the symbol's
reach with no transform, which it measures once. When the drawn
document's symbols change, the renderer drops the cached bounds of the
instances and of the objects that hold them.
The PDF and EPS writers, the SVG of an artboard and Skip Blank
Artboards grow the box of an instance whose art holds brushed strokes
by 16 times the widest stroke (at least 1 pt) of the objects in that
art that have a brushed stroke, plus 8 pt, as the renderer grows the
cull bounds of a brushed path. They keep an instance whose brush art
reaches the page. The SVG of an artboard also keeps an object that
holds an instance when what the canvas paints for it, the effects in
the symbol's art and the instance's own included, reaches the artboard.
Not yet: other commands that measure objects, such as Select › All on
Active Artboard, Transform Each and Convert to Artboards, still measure
an instance by the square.
Docs: docs/mcp.md, docs/target-app-parity.md.
In builds with overflow checks (`cargo run`, `cargo test` and
`cargo xtask ci`), PDF export aborted the process when a number it
wrote rounded to -2,147,483,648 as an f32. pdf-writer 0.15 writes
-2,147,483,648.0 as an integer, and `Limits::register_int` overflows
when it takes the absolute value. krilla 0.8.2's `Surface` then fails
an assertion in its `Drop` while that panic unwinds.
`svg_hostile_markup_never_panics` in `import_fuzz` hit it with type
shifted 2,147,483,649 pt down. Paths, gradients, dash offsets, images
and artboards that far out hit it too, and so does a group around a
stroke wide enough to reach that far. Release builds have no overflow
checks.
PDF export now leaves out art whose coordinates or transform entries
lie beyond 1,000,000,000 in its own space or in the content stream it
is drawn into, and glyph lines whose spacing lies beyond 1,000,000,000
thousandths of an em. That covers paths and clipping paths, glyph
lines, images, gradients, strokes whose width, caps or miter spikes
reach that far, and the art and marks of a page whose artboard lies
that far, which is written blank. A dash offset beyond the bound is
written as its remainder after whole periods of the pattern.
The type's transform is now pushed inside the form that marks a fill
or stroke of type to overprint. The exporter checks what that form
holds through the type's transform, as it checks the same fill or
stroke without overprint.
Rarely set fields move into one optional boxed NodeExtras, and large NodeKind payloads (images,
live shapes, blends, envelopes) are boxed. Every object, and every undo step that copies it,
shrinks: a 50k-path move keeps ~24.5 MB per step instead of ~40 MB. Files stay byte-identical
(Node keeps its flat JSON through a borrowed Serialize and a from-wire Deserialize, no flatten),
and loading is slightly faster. Undo and redo reuse their step's size estimate instead of
walking the document again (8.3 -> 5.6 ms for a 50k-path move).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
# Conflicts:
# crates/engine/src/cmd/graph.rs
# crates/svg/src/export.rs
Every edit of an object copied its whole appearance (its fills, strokes and effects, about
540 bytes for a basic object) into the new node, though a move leaves it as it is. Over 45
whole-selection moves of 10,000 paths, undo history grew by 589 MB; now 349 MB (41 % less), and
a nudge or a drag of the selection takes 12 ms instead of 15.
- Node::appearance is a vectorcraft_doc::Shared<Appearance>: copy-on-write. A clone shares it,
and the first change through DerefMut copies it (Arc::make_mut). Reads, writes, equality (also
against a plain Appearance) and the saved form are those of the Appearance itself, so call
sites read and change it as before. Sites that build or assign a whole appearance add .into(),
and the few that need a plain copy take (*n.appearance).clone().
- Node::transform changes the appearance only when it has gradients to transform, and pattern
paints are transformed only when there are some: a move no longer copies an appearance it
doesn't change.
The workspace suite (5,301 tests) and clippy pass with the change; the doc and engine suites
pass again after the transform guard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
The PDF writer defaulted to Convert Text to Outlines, so every glyph of
every page became a path: a text-heavy PDF grew several times larger on
export (a 7-page document went from 27 KB to 226 KB) and its text could
no longer be selected or searched. Illustrator's PDF presets embed font
subsets and keep live text; that is now the default too
(advanced.outlineText: false). Outlining stays available as an option,
fonts that can't be embedded still become outlines with a warning, and
printer's marks (page information) are drawn as shapes as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Presentation Mode hides the menus and panels, had no Escape and was saved with the
preferences, so it came back on every restart with no way out. Escape now leaves it (as in
Illustrator), F from it goes back to Normal Screen Mode, and the screen mode is no longer
saved: the app always starts in Normal Screen Mode, and preferences saved in Presentation
Mode by older versions open normally. ui.inspect reports `screenMode` on its own.
Desktop file drags carry no pointer position or modifiers (winit 0.30 on Windows, macOS
and X11), so every drop on a window with a document open was taken for the middle of the
canvas and placed. Where the platform doesn't say, a dropped document (native, SVG, PDF,
.ai, EPS, DXF, EMF/WMF) now opens as a tab of its own and a picture or text is placed in
the middle of the view; pictures dropped together with documents are placed first, so the
last document opened is shown. The web, whose drag events give the position, keeps
Illustrator's choice: placed on the canvas, opened off it (the tab bar).
Illustrator's PDF-compatible part leaves out art outside the artboards (it lives in the
private data alone, which isn't read), so it can't open; importing a file with that
private data now says so in a note. Off-page art a PDF does draw already opens on the
pasteboard; a test keeps it so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
document.exportPdf {standard: pdfX1a|pdfX3|pdfX4} and the PDF/X presets now
write conforming files instead of being refused:
- every PDF/X file gets a /GTS_PDFX output intent (by default the CMYK
profile in effect, embedded), TrimBox, Trapped, GTS_PDFXVersion (and
GTS_PDFXConformance for X-1a), a title and dates; editing data,
passwords and, in X-1a/X-3, PDF layers are refused
- PDF/X-1a converts every colour and image to a CMYK destination,
untagged, with CMYK spot alternates; X-3 and X-4 tag colours with ICC
profiles
- X-1a and X-3 are written as PDF 1.3 and their transparency is flattened
first (High Resolution preset; images with see-through pixels count,
rasters stay opaque); X-4 keeps transparency, is PDF 1.6 at most and
names the standard in its XMP metadata
- the written file is checked against its standard (soft masks,
transparency groups, opacity, blend modes, RGB in X-1a, fonts not
embedded) and refused rather than mislabelled
- choosing a standard sets the latest compatibility it allows and turns
off what it forbids (engine and Save PDF dialog)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Links panel lists every image (thumbnail, name, page, missing/modified/embedded
badge) with Show filters, sorting and thumbnail sizes; its buttons and flyout relink
(to a file or a folder), go to, update, edit the original, embed and unembed, show in
folder and open Placement Options. Link Info shows format, colour space, location, ppi
and effective ppi, dimensions, scale, rotation, size and dates. Edit > Edit Original
and the Properties panel's image section reach the same commands.
Engine: links.list, links.goTo, links.embed, links.unembed, links.info and
links.placementOptions; links.relink and links.update now place the new file as the
image's Placement Options say (transforms, bounds, file dimensions, fit, fill, the
alignment point, clip to the old bounds). Model: ImageObject.placement.
UI: links.editOriginal, links.reveal, ui.placementOptionsDialog; Services.open_file
and pick_folder on the desktop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>