Commit Graph
101 Commits
Author SHA1 Message Date
NetterSilverandClaude Opus 5.5 19d4adb39c Make SVG export def ids linear instead of quadratic
Writer::fresh_id probed prefix-1, prefix-2, ... from 1 on every call,
formatting and hashing each candidate, so writing N gradients, clip
paths or masks cost O(N^2). Ids are never released, so every number
below the last one handed out for a prefix is taken: remember where
the search for each prefix stopped and resume there. The ids written
are the same as before.

examples/still-life.vectorcraft (12k gradients, 6.3 MB SVG) exported
in about 8.6 s (median); it now takes about 0.15 s. Output is byte-identical
for every document in examples/.

Add crates/engine/examples/export_bench.rs to time document.export
on the example documents, and a test pinning the generated id
sequence when object names already take some of the numbers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:52:22 +01:00
aiongg 7e52760174 refactor(svg): move export.rs items to export/{nodes,output,paint,text}.rs 2026-10-26 21:45:32 +00:00
ham9zah e56aa356b5 Type on a path: joined scripts bend along the path so their joins stay connected
On a curved path each glyph was placed rigidly (turned to the tangent at
its middle), so its baseline ends stood off the curve and neighbours
turned by different angles: in scripts whose letters join, every join
opened into a notch. Measured with Arial on circles: at 29 pt on a
radius of 110 pt, 0.64 pt at the baseline and 1.24 pt at the height of
the joining stroke (a quarter of the size); 14.7 pt at 72 pt on a radius
of 60. Placing glyphs on the chord alone closes the baseline but leaves
about h (a_i + a_{i+1}) / 2R above it (0.71 pt here), so joined glyphs
are bent instead.

- Shaping marks each glyph of a segment whose script joins (Arabic,
  Syriac, N'Ko, Mongolian, Adlam and the other cursive scripts), by the
  segment's script, so a word bends as a whole.
- With the Rainbow effect such a glyph's outline is mapped point by
  point: pen space (x, y) -> P(s + x step/advance) + (y + rise) N there,
  each segment cut into pieces short enough for 0.01 pt (capped). A
  glyph the path doesn't turn under stays exactly as it was; Skew, 3D
  Ribbon, Stair Step and Gravity, vertical type, inline art and colour
  glyphs are unchanged. `PositionedGlyph::bend` records it; `xf` stays
  the straight glyph for the caret, hit testing and tools.
- PDF export draws bent glyphs as outlines and writes them again as
  invisible text where they stand, so they can be selected, searched
  and copied. SVG export outlines them (right-to-left type already was),
  never as a <textPath>, and labels the group with its text
  (role="img", aria-label) for screen readers and search.

Tests (crates/text/src/tests_path_joins.rs; installed Arabic font, skip
without one): the gap at the baseline and at join height is <= 0.01 pt
at 12, 29 and 72 pt on radii 60, 110 and 300, on both sides of the
path, with finite outlines; Latin on a curve and joined type on a
straight path are byte-for-byte what they were; the chord-only
prediction is checked.

vectorcraft-cli perf (load average 43 before, 34 after: not trustworthy;
the synthetic document has no type on a path, so bending doesn't run):
render fit 113.8 -> 132.7 ms, pan 115.5 -> 140.0 ms, 400% 32.9 -> 43.3
ms, hit test 1.36 -> 1.59 ms, save 81.5 -> 88.2 ms, open 306 -> 401 ms,
SVG 426 -> 439 ms, Pathfinder 93.7 -> 85.5 ms, handle drag 5.5 -> 6.7
ms, segment drag 5.1 -> 6.7 ms.
2026-10-26 09:09:47 +07:00
dotCooCoo bc041e15ef Type: the Numbered List button numbers paragraphs 1, 2, 3
Squashed from #3960 onto main.
2026-10-24 19:02:08 +07:00
bflatastic 166afce360 SVG export: type with small capitals, superscript, subscript or tab leaders is written as outlines (#3869) 2026-10-24 04:49:08 +07:00
bflatastic c2ad9f8bbb Type: emoji go with type that effects reshape and Expand Appearance
Squashed from #3879 onto main.
2026-10-24 04:49:06 +07:00
Dylan M. Taylor 98d11ad66b Audit: cap SVG entity expansion, filter layer stacks and export id/companion probing; share compound-path appearances; escape-proof font feature tags 2026-10-24 03:52:24 +07:00
dotCooCoo 2a2e63856c Type: the Paragraph panel's Bullets button turns paragraphs into a bulleted list
VectorCraft had no lists. A bullet could only be typed as a character,
and the tab after it did not stop at the left indent. The reference app
turns paragraphs into a bulleted list from the Paragraph panel, the
context menu and Type › Bullets and Numbering › Apply Bullets.

text.setFormat takes list ("none" or "bullet") and bullet ("none",
"dot", "dash", "circle" or "square"). The layout draws the bullet before
the first line of each list paragraph that has text, in the font, size
and color of its first character, and starts the text at the next tab
stop, with the left indent counted as a stop. Wrapped lines start at
the left indent, and type on a path draws the bullet too. In a
horizontal right-to-left list paragraph the indents are mirrored: the
bullet hangs at the right, and the wrapped lines start at the right
where the first line's text starts after the bullet. The bullet is not
a character of the text. A paragraph whose left and first-line indents
are both 0 gets 18 pt and -18 pt when it joins a list. Return continues
the list, paragraph styles carry it, Reset Panel turns it off, and the
native file saves it only when it is set.

The Paragraph panel has a Bullets button with a list of the five
presets, and Type › Bullets and Numbering › Apply Bullets and the
context menu run type.applyBullets. Live SVG text writes the bullet's
character, and SVG writes type on a path with bullets as outlines. DXF
writes the bullet as a TEXT entity of its own, and legacy .ai export
warns that bulleted lists are left out. The caret, selections, Touch
Type and Show Hidden Characters skip the bullet.

Live SVG text of horizontally scaled type now writes each tab in a
tspan of its own. Before, the characters before a tab shared its tspan,
and its textLength made a viewer stretch them across the tab's gap.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-24 03:09:11 +07:00
Jürg Lehni a26ac22794 SVG: type written as outlines stacks colour glyphs under their run's paint
As on the canvas: each run's bitmap, COLR and SVG glyphs come before the run's path, so a plain character set over a colour one stays on top.
2026-10-24 02:06:13 +07:00
Louise FoxandClaude Opus 5.5 8906abf30d SVG: keep other applications' attributes from import to export
Many tools note things on SVG objects in attributes of their own XML
namespace: a plotter's pen, a cutter's passes, a game engine's
collision flag. Import dropped them, so a file that went through
VectorCraft lost what other software had set on its objects. #522
asked for unknown and data-* attributes to survive a round trip; #580
kept the data-* ones, and this keeps the ones in other applications'
namespaces.

- Import keeps every attribute in a namespace that isn't SVG's,
  XLink's, XML's, Inkscape's, Sodipodi's or VectorCraft's own, on the
  object or layer it belongs to, in order, with its namespace URI and
  the prefix the file gave it (ObjectAttributes::foreign, beside the
  data-* attributes and the event handlers). The file is untrusted: at
  most 64 per object and 64 KiB per value, and what is left out is
  warned about. Files that declare no other namespace skip the scan.
- They are the object's own, as its data-* attributes are: a pasted
  copy and a duplicate keep them, and deleting the object takes them
  with it. The native file keeps them in the object's attributes, a
  key written only when an object has some.
- Export writes them back on the same elements and declares each
  namespace once, on the root, under the file's prefix. A prefix the
  export uses itself, or that another namespace already has, becomes
  prefix2, prefix3...; one XML keeps for itself (xml...) becomes ns. A
  name or value XML can't hold is left out with a warning.

Nothing in VectorCraft reads them, and nothing in the UI lists them.

svg/tests/foreign_attributes.rs: a layer's and a shape's attributes
survive import, export and import again, Inkscape's and Sodipodi's
own aside and with no warnings, and the native form round-trips;
prefixes are made unique, and bad names and values are left out with
a warning; the caps hold and are warned about once.
engine tests_attributes.rs: a pasted copy and a duplicate keep them, a
deleted object leaves none behind, and the native file and SVG export
keep them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
2026-10-23 10:56:00 +07:00
Jürg Lehni 9de4b40241 Type: COLR colour fonts draw and export as vector art
Glyphs whose paints documents have no form for (sweep gradients, compositing modes) are drawn as images of the glyph.
2026-10-23 05:14:47 +07:00
Taha-Mahmoodi 2bad646821 Gradients: a Perceptual gradient mixes its colours in OKLab, and SVG and PDF write it as an image
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).
2026-10-23 01:53:29 +07:00
bflatastic 26b9cf84ad Type: colour emoji fonts with bitmap glyphs draw and export
Squashed from #3561 onto main.
2026-10-23 00:44:31 +07:00
Taha-Mahmoodi cc1a660a6d Gradients: Dither in the Gradient panel draws a gradient fill with fine noise that hides banding
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.
2026-10-22 22:38:42 +07:00
Taha-Mahmoodi a1d8952bc8 Gradients: a linear or radial gradient can be drawn as pixels, and SVG and PDF write such a gradient as an image
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.
2026-10-22 22:38:41 +07:00
Taha-Mahmoodi 4f9b620dc6 Fix #3544: an artboard name XML can't hold falls back to the document title
Review follow-up (CodeRabbit): the blank-name check ran on the raw name, so
a name made only of characters XML can't hold (control characters) wrote an
empty <title>. The name is sanitised first, then the fallback applies. The
title test covers such a name; it fails without the change.
2026-10-22 18:56:34 +07:00
Taha-Mahmoodi 439d32fcf8 Fix #3544: an SVG of one artboard on its own has the artboard's name as its title
The SVG writer always wrote the document's title in <title>
(crates/svg/src/export.rs), so every file of Export As with Use
Artboards, Export for Screens or an `artboard`/`range` export was titled
after the document ("Icons"), whatever its artboard was called. The
writer's `artboard` option can't tell those apart from Save: Save writes
the first artboard too, standing for the whole document.

ExportOptions has a new `artboard_title` (set by the caller, as
`artboard` is, not deserialized): the file is that one artboard, exported
on its own, and its <title> is the artboard's name (XML-escaped as
before), or the document's title when the artboard has no name. The
engine's SVG encoder (fileio/svg.rs) sets it exactly when the export
names its artboards, which is also when a file holds only that
artboard's art (#550): Export As per artboard, document.export /
serialize with `artboard` or `range`, MCP export, and Export for Screens'
artboards, assets, layers and selections (each written as an artboard
named after it). Save and Save As keep the document's title. Export for
Screens' Full Document draws the art bounds as one artboard, which is now
named after the document, so its SVG keeps the document's title too.
docs/file-format-parity.md's SVG row says so.

Tests: title_control_chars::an_artboard_exported_alone_is_titled_after_it
(crates/svg: two named artboards, the second named with markup;
exporting either alone gives its escaped name, the default and an
artboard without artboard_title give the document's title, an unnamed
artboard falls back to it) and
tests_svgenc::an_svg_of_one_artboard_is_titled_after_it (the engine:
`artboard` 0 and 1, useArtboards range all, Save's SVG, Export for
Screens per artboard, an asset and Full Document). Both fail without the
change (the title was "Icons") and pass with it.
2026-10-22 18:56:34 +07:00
bflatastic 34cde7c3db Character: Character Direction sets the selected characters left to right or right to left
Squashed from #3436 onto main.
2026-10-22 04:18:35 +07:00
bflatastic 14f804cdee Type: ruby (ふりがな) for live text
Squashed from #3448 onto main.
2026-10-22 00:10:56 +07:00
dotCooCoo 68d8a9c755 Export: area type that Shrink Text to Fit scales down is written at the size it draws at
Shrink Text to Fit scales area type down at layout time, and the canvas,
PDF export and the exports that write type as outlines draw it at that
size. SVG export wrote the live text at its stored font size, letter
spacing and baseline shift, so a viewer drew it larger than the canvas
does. Legacy .ai saves wrote the stored size, leading and baseline shift
into the text objects, DXF export took the TEXT heights from the stored
size, and the CSS Properties panel gave the stored font size, letter
spacing and line height.

The text crate's scaled, which the layout scales shrunk text with, is
now public. SVG export, legacy .ai saves and the CSS Properties panel
take the type's styles from it at the layout's fit scale, and DXF
export multiplies the TEXT heights by that scale. Positions and
textLength in SVG still come from the layout.

Docs: docs/target-app-parity.md.
2026-10-21 16:55:16 +07:00
dotCooCoo b0056a127d Type: Optical Margin Alignment hangs punctuation at the ends of type on a path
Optical Margin Alignment and Roman Hanging Punctuation hung punctuation
in point and area type only. Type on a path kept its first and last
glyphs inside its brackets with either option on.

When Optical Margin Alignment is on, or the first paragraph has Roman
Hanging Punctuation on, the glyphs at the ends of left-to-right type on
a path now hang outside its brackets by the same amounts, measured
along the path. The type hangs at the start bracket when its line is
placed there, hangs at the end bracket when the line is placed there,
and hangs at both when it is centered. Glyphs that hang past the end
bracket do not make the type overflow. On an open path a hang stops at
the path's end, and on a closed path the two hangs together take at
most the stretch between the end and start brackets. SVG export writes
type on a path that hangs as outlines, with a warning.

Docs: docs/mcp.md, docs/type-parity.md, docs/target-app-parity.md.
2026-10-21 16:46:23 +07:00
NetterSilverandClaude Haiku 5.5 05017111ba Filter control characters in SVG export title and description
Found while testing crates/svg/src/export.rs:138 and :143

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 03:15:47 +07:00
EricKuckandClaude Fable 5.1 1b8e3d9ae1 Outer Glow grows its solid silhouette by a quarter raster pixel before the blur, as the glows AI files export measure
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 23:50:57 +07:00
VictorandClaude Opus 5.5 a275496ef4 G4: Paragraph panel menu › Justification… (word and letter spacing, glyph scaling, auto leading, single word justification)
The Paragraph panel menu's Justification… was a disabled stub, and justified
lines stretched word spaces between fixed limits (80% to 150%).

Each paragraph now has Justification options: Word Spacing, Letter Spacing
and Glyph Scaling (Minimum, Desired, Maximum, in percent), Auto Leading and
Single Word Justification. Desired applies to every line; a justified line
fills its measure with word spacing first, then letter spacing, then glyph
scaling, each within its range, and past them its word spaces take the
rest. The Every-line Composer scores lines by that flexibility. Auto
Leading sets the leading of characters whose leading is Auto. A justified
line holding one word is spread between its letters (Full Justify) or
aligned left, centre or right.

The defaults are the documented ones (80/100/133, 0/0/0, 100/100/100, 120,
Full Justify), saved only when they differ. Because the default maximum
word spacing is 133%, justified paragraphs break differently than before;
the Every-line Composer's second pass is now unbounded for justified text
(it used to give up past four times the stretch and fall back to the
Single-line Composer, which the narrower default range would make common),
and lines that can't stretch at all are still refused.

text.setFormat takes wordSpacing, letterSpacing, glyphScaling ([min,
desired, max] or {min?, desired?, max?}), autoLeading and
singleWordJustification; Minimum ≤ Desired ≤ Maximum must hold or nothing
changes. Paragraph styles carry the options (New and Redefine capture
them), and Reset Panel resets them. Justification… opens a dialog
(ui.justification) with Preview; OK is disabled while a row is out of
order. Live SVG text places characters the options move, and a glyph that
Glyph Scaling scales is stretched to its width; PDF uses the glyphs'
transforms. Vertical type isn't glyph scaled; type on a path doesn't use
the options.

Docs: docs/mcp.md, docs/type-parity.md, docs/gaps.md.

Closes #2755.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:40:14 +07:00
EricKuckandClaude Opus 5.5 1f304f6a23 Shadows and glows: each is of the art with the ones before it in the stack, and an Outer Glow blurs a solid silhouette
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 01:11:39 +07:00
BFlat c1c677bc76 Merge pull request #2409 from dotCooCoo/feat/live-rasterize-effect
Effects: Effect › Rasterize draws an object as pixels while its paths stay editable
2026-10-17 20:02:59 +07:00
dotCooCoo 4712fb2c2f Character: variable fonts get a slider for each axis
VectorCraft listed a variable font's named instances as styles, but
type could not be set between or past them. The reference app's
Character panel has a Variable Font control that adjusts a variable
font's axes, such as Weight, Width and Slant.

Type now keeps Variable Font settings by axis tag
(CharStyle::font_axes, saved as fontAxes only when set). text.setFormat
and text.setRangeStyle take fontAxes: each value is clamped to its
axis's range, a tag the font has no axis for is left out, and null
goes back to the style's own setting. Another font or style clears
them, including one from a character style that does not set them. A
character style captured from type sets them, with {} for none.
text.fontList {family, style} lists a font's axes.

Layout, outlines, the substituted-glyph highlight and PDF export use
the font instanced at the settings, and neighboring runs at different
settings are shaped apart, as runs in different fonts are. SVG export
writes them as font-variation-settings, the wght setting as the weight
and the wdth setting as font-stretch, and embeds the font instanced
there; SVG import reads font-variation-settings back. Legacy .ai export
warns that the settings are left out.

The Character panel shows a Variable Font button for a variable font.
It opens a slider and a field for each axis the font does not hide,
labeled with the font's name for the axis, and screen readers get each
slider under that name. A drag is one undo step. Reset Panel leaves
the settings, as it leaves the font and style.

Missing Glyph Protection now keeps the settings and the font version
on the characters that keep their font; before, it kept only the
family and style. A character style applied or redefined with another
font now clears the old font's version, as text.setStyle does, unless
the style sets one. SVG export with embedded fonts warns when faces of
one weight, style and width share characters, which browsers then draw
in one of the faces.

Docs: docs/mcp.md, docs/target-app-parity.md, docs/type-parity.md.
2026-10-17 19:38:46 +07:00
dotCooCoo 04b0be2c9f Merge main into feat/live-rasterize-effect 2026-10-17 04:09:12 -07:00
dotCooCoo be553fa3cf Window: the SVG Interactivity panel attaches JavaScript events and files for SVG export
Window › SVG Interactivity was a disabled menu item. VectorCraft could
not attach JavaScript to an object's events or link JavaScript files
for SVG export.

The SVG Interactivity panel lists 19 events, from onfocusin to
onscroll. Pick one, type its JavaScript and press Enter to add it to
the selected objects; the list shows their events. Remove Selected
Entry removes the picked event, and the panel menu's Clear Events
removes all of them. Link JavaScript Files… opens the JavaScript
Files dialog, which adds a path or URL to the document's list and
removes the selected one. Its Choose… button puts a picked file in
as a file:/// URL. The commands are svgInteractivity.info,
setEvent, removeEvent, clearEvents, addFile and removeFile, and
ui.javascriptFilesDialog. Each edit is one undo step. A document
links at most 64 files.

SVG export writes each event as an event attribute on the object's
element and each file as a <script xlink:href> element. With the SVG
Tiny 1.2 profile, export leaves the events out with a warning. SVG
import reads the listed event attributes and the script links back
as text. It never reads a linked file or runs a script, and it leaves
inline scripts and the links after the first 64 out with a warning.
The native format keeps both.

An unnamed <g> with data-* attributes of its own around a single
object now imports as a group. Before, import folded it into the
object, which then took the group's data-* or, when it had its own,
dropped the group's. An unnamed <g> with event attributes stays a
group in the same way. The SVG import fuzz tests now include event
attributes and <script> elements.

Docs: docs/mcp.md, docs/control-protocol.md, docs/target-app-parity.md,
docs/ui-parity.md.
2026-10-17 15:14:43 +07:00
dotCooCoo 8511d0321b SVG export: justified and Mojikumi lines keep their letter spacing in live text
SVG export wrote each style run of a line of live text as one <tspan>
and positioned the text again only after tabs and, on justified lines,
after word spaces. A viewer then spaced the characters inside the
<tspan> with the font's own advances, so it lost the letter gaps of a
Justify All line without word spaces, the gaps between the characters
of a justified Japanese line and the spacing that a Mojikumi Set
changes. On justified lines, the textLength of horizontally scaled type
also took in the justification space after it, so a viewer stretched
the glyphs into that space.

On a line that is justified or set with a Mojikumi Set, text_lines now
lays the text out again as point type without the Mojikumi Set to get
the advance a viewer gives each character. Each character that the
layout draws away from where a viewer would draw it starts a new
positioned <tspan>, and textLength and line anchors use the viewer's
widths. The last character of a horizontally scaled <tspan> takes its
width without the font's kerning against the next character. Other
lines, and lines written with Fewer <tspan> Elements, are written as
before.

Docs: docs/target-app-parity.md.
2026-10-17 13:52:05 +07:00
dotCooCoo fa46998006 Effects: Effect › Rasterize draws an object as pixels while its paths stay editable
The Effect menu had no Rasterize. Object › Rasterize replaces the
object with an image, so its paths can no longer be edited.

Effect › Rasterize… (`rasterize`) is a live effect with Object ›
Rasterize's options: color model, resolution (the document's raster
effects resolution by default), white or transparent background,
anti-aliasing, Create Clipping Mask and Add Around Object. The canvas
draws the object as square pixels over its bounds, and its paths, type
and other art stay as they are. The effects above it in the stack,
shadows included, are part of its image, and shadows below it are cast
by the image. When Rasterize is an object's only raster effect, Expand
Appearance and PDF and SVG export give the image Object › Rasterize
gives with the same options.

Object › Rasterize now measures the art as drawn, with its geometry
effects, brush art and raster effects, so a Transform effect that moves
the art no longer leaves the image empty. Its Create Clipping Mask
clips the image to the art as drawn. The canvas no longer panics
drawing a pixel effect such as Gaussian Blur on an object about 65,500
screen pixels wide: the raster of such an object is at most 32,768
pixels a side, and the object is drawn into it at a lower resolution.

Docs: docs/mcp.md, docs/effects-parity.md, docs/target-app-parity.md,
ROADMAP.
2026-10-16 05:24:14 -07:00
Gert Pellin ca0673e0bc Align Stroke inside or outside on live type (#2294)
The characters' stroke takes Align Stroke like an object stroke: CharStyle
keeps stroke_align (written only when not centred), stroke.set and
text.setRangeStyle strokeOptions apply align to type, and the Stroke panel
enables the buttons for type. Canvas, PDF and EPS draw it around the glyphs
through stroke_layer(); SVG keeps the text live and writes the aligned
stroke on the glyph outlines, clipped or masked. Type's visual bounds grow
with its character strokes.
2026-10-16 15:27:54 +07:00
Gert Pellin 74c391c7ba G13: Object › Intertwine: Make, Release and arranging overlap areas (#1763)
An Intertwine group keeps its objects as they are, in their own stacking
order; each member can be drawn over the others in an area of its own.
The group draws, hits, saves and exports through its live expansion, so the
members stay editable and Release gives them back unchanged.

object.intertwine.arrange picks an overlap area by a click point or by a
closed path drawn around overlaps and toggles which member is on top
there, or brings a member to the front or sends it to the back where three
or more objects overlap.
2026-10-16 04:44:52 +07:00
dotCooCoo 720ce5ed73 Effects: Drop Shadow has Darkness, the object's own colors with black added
The reference app's Drop Shadow has a Color and a Darkness, the
percentage of black added to the object's own colors. Drop Shadow had
only a Color, so every shadow was one color. Choosing one or the other,
and the defaults, are VectorCraft's own.

Drop Shadow now takes `shade` ("color" or "darkness", default "color")
and `darkness` (%, default 100). Its dialog and the Appearance panel's
inline editor show Color and Darkness as radio buttons. With Darkness
d, a CMYK document keeps the object's cyan, magenta and yellow and
raises its black from K to K + d × (1 − K), and an RGB document
multiplies each channel by 1 − d, so in RGB 0% gives the object's
color and 100% gives black. A shadow saved without these keys draws as
before. SVG export writes a Darkness shadow with feComponentTransfer
and SVG import reads it back; CSS export leaves it out.

Docs: docs/target-app-parity.md.
2026-10-16 02:25:58 +07:00
NetterSilverandClaude Haiku 5.5 62ab3bd797 Make SVG export of type with control characters well-formed
Found while testing crates/svg/src/export.rs:1908

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 02:21:27 +07:00
bflatastic 0efcaeb558 Importers: a brush that draws itself by name no longer crashes
A scatter brush whose art is an instance of a symbol stroked with that brush, or any brush whose
art is painted with a pattern whose tile is stroked with it, drew itself without end: a stack
overflow in a hand-written native file. Brush pieces already strip the brushes of their own
strokes; now what they draw by name that would draw a brushed stroke (a symbol instance, or a
pattern paint, also through the symbols and patterns those use) is left out too, in one place
(`stroke_pieces`, which every renderer and exporter calls with the document).

Probed the rest of item 2 of the depth follow-up with hand-written hostile inputs, kept as tests:
graph designs of deep art and of a symbol holding itself, Affinity groups nested to and past
both of its reader's caps, DXF blocks inserting themselves, EMF/WMF saving states without end,
and plug-in manifests and output nested 30,000 to 100,000 deep. None of them overflowed.
2026-10-14 10:13:45 +07:00
dotCooCoo d9bd9b134d Symbols: instances of imported symbols are measured by their art
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.
2026-10-14 04:56:30 +07:00
Ayanprogrammer11 3af08505f8 Replace linear scan in CSS class dedup with a HashMap lookup
Export::class() deduplicated declaration blocks with
classes.iter().position(), an O(n) scan per style, making exports with
many distinct styles (e.g. large palettes) quadratic overall. Track the
1-based class number per declaration in a HashMap alongside the
first-use-ordered Vec, so each lookup is O(1) while output stays
byte-identical.

Also adds a regression test covering 256 distinct declarations with
recurrence of early styles, for both InternalCss and StyleEntities
styling.
2026-10-13 08:53:06 +07:00
EricKuck 5b413448d9 SVG export: knockout groups of opaque art draw in order, without masks 2026-10-12 23:06:34 +07:00
arhammusheer 5beac4f122 #1244: smaller objects: Node 352 -> 152 bytes, NodeKind 200 -> 80, and a cheaper undo estimate
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
2026-10-12 11:02:32 +07:00
sanyamsingh001andCodex 7a49c49ade SVG.1: preserve data attributes and validate export ranges
Co-authored-by: Codex <noreply@openai.com>
2026-10-12 06:59:49 +07:00
arhammusheerandClaude Opus 5.5 7652439b40 #1172: Pathfinder Outline 19x, SVG export 1.5x and open 1.2x faster, same output
At 50,000 paths unless noted, against main:

- Pathfinder Outline (300 paths): 145 → 7.5 ms. Each segment was tested against every junction
  of the arrangement, and each chain against every chain kept so far. Now a segment tests only
  the junctions inside its control box (junctions are sorted by x; a point within reach of a
  curve is within reach of that box), with the same nearest-point test. Coincident chains are
  found through a grid on their midpoints, taking the first match in the same order a scan does.
- export SVG: 267 → 176 ms. Path data is written straight into one string (`push_num`, the
  same digits and trimming as `fmt_num`) instead of a string per number, point and segment.
- open .vectorcraft: 239 → 195 ms. The JSON was parsed once only to find where it ends:
  - A file with no NUL has no data after it.
  - Otherwise the marker (which holds the first NUL, as JSON can't) is taken when it directly
    follows the JSON's last byte, as the writer puts it.
  - Anything else takes the old parse.

`perf --deep` prints output hashes of an SVG export and a save → open → save round trip; main
and this branch print the same ones. pathfinder_pinned (Outline included) passes, and so do the
pathops, format and svg suites (345 tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
2026-10-11 23:26:53 +07:00
zakariayacineandClaude Fable 5.1 16af3cc3fa Type: Digits — Default, Arabic, Hindi and Farsi numeral systems
Adds the Character panel menu's Digits option (with the Middle Eastern /
Indic options, beside Paragraph Direction in the Paragraph panel) as a
character attribute: CharStyle::digits, saved only when set, with
text.setFormat {digits} and text.setRangeStyle {id, start, end, digits}
("default" | "arabic" | "hindi" | "farsi"; a bad value is an error).

Shaping draws every digit of the Western, Arabic-Indic (U+0660–U+0669)
and Eastern Arabic-Indic (U+06F0–U+06F9) sets in the chosen system,
whatever was typed, before font fallback, so a font without the
Arabic-Indic digits gets a fallback for them. The text, its clusters,
the caret, selection and search stay as typed, as with All Caps; SVG
export writes the digits as drawn.

Catalog rows for Digits, Arabic, Hindi and Farsi in the ten translated
languages. Docs: docs/mcp.md (Digits), type-parity.md (digit types
done; Character Direction, kashidas, composers and the split caret
still missing), target-app-parity.md and gaps.md.

Behaviour from the public documentation (a character-level option with
the values Arabic, Hindi and Farsi in the Character panel menu):
https://helpx.adobe.com/indesign/desktop/language-and-proofing/arabic-and-hebrew/choose-digit-types.html

Tests: doc (the mapping and serialization), text (glyphs of "page 3" in
Hindi/Farsi equal the typed Arabic-Indic digits' on the typed clusters;
skips without a font covering the three sets), engine (setFormat,
setRangeStyle, undo, error), svg (digits written as drawn), ui-egui
(the menu shows with the Indic options).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 22:18:01 +07:00
bflatasticandClaude Opus 5.5 72c9cec0e0 Fix #847: draw Underline and Strikethrough
The styles were stored on the text and written to SVG's text-decoration,
but the type was drawn without the lines. vectorcraft_text::decorations
now gives each underlined or struck-through stretch of a run on a line
its bar, placed and sized from the font's own underline and strikeout
metrics (with fallbacks for fonts that have none). The canvas, Create
Outlines, PDF export (outlined and real text) and SVG's outlined text
draw them in the run's paint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:03:04 +07:00
bflatasticandClaude Opus 5.5 83b51ab9c7 SVG export: name the unsupported pixel effects without a product name
The brands check keeps other products' names out of user-facing text; #694's
SVG warnings called Radial Blur, Smart Blur and Unsharp Mask "Photoshop-style
raster effects". They are "pixel effects" now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:43:49 +07:00
bflatasticandClaude Opus 5.5 1e7f5084d8 Part of ROADMAP gap 2: Radial Blur, Smart Blur and Unsharp Mask on a pixel-filter pipeline
Effect › Blur › Radial Blur (spin/zoom, draft/good/best) and Smart Blur
(Normal mode), Effect › Sharpen › Unsharp Mask: Photoshop-style filters in
vectorcraft_effects::pixel, run by the renderer on the object's pixels in
stack order with Gaussian Blur and Feather, cached while panning; lengths in
points, bounded work per pixel. PDF, Expand Appearance and SVG export write
the effected objects as images. Dialogs with preview (method/quality
dropdowns, fields in documented order), menu entries, translations, tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:30:31 +07:00
bflatasticandClaude Opus 5.5 d5e6be1565 Part of #372: SVG import keeps wrapped layers and round-trips sublayers
Top-level groups already opened as layers named from data-name, else their
id. Now that also holds when one more id-less group wraps them (a transform
on all the art, or a <switch>'s chosen group), and hidden objects inside a
<switch> are kept (hidden) instead of dropped.

Nested groups stay groups unless something marks them as layers: the SVG
export now writes data-vc-layer="sublayer" on layers inside layers, and
import makes such a group (or an Inkscape layer group) a sublayer of the
layer holding it, in that layer's colour. A marked group anywhere else
stays a group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:36:21 +07:00
haven b843fe8af3 M7: inline symbols in text
Document symbols placed inline in a run of type (beyond Illustrator; like
InDesign inline anchored objects): TextRun.inline holds the symbol, scale and
shift; the run is one U+FFFC laid out as a glyph sized to the art, centred on
the cap height. text.insertInline and Type > Insert Inline Symbol; editing
keeps the graphic whole; canvas, SVG (<use>) and PDF draw the art; Create
Outlines turns it into a symbol instance.
2026-10-08 15:45:34 -07:00
haven f7761bf5bc M7: per-paragraph paragraph attributes
Each paragraph has its own alignment, indents, spacing, hyphenation, tab
stops and paragraph style (TextObject.paras; para stays paragraph 0's, so
old files and readers work). Layout, editing (split/merge), threads, the
Paragraph/Tabs/Paragraph Styles panels, ranged text.setStyle/setFormat/
tabs/paraStyle.apply, SVG export/import and the fuzzers follow.
2026-10-08 15:07:13 -07:00
bflatasticandClaude Opus 5.5 2a35c4a82f Fix #522: SVG round trips keep hidden objects and data-* attributes
An SVG used as an animation rig lost its hidden alternates and their
rotation pivots: the exporter left hidden objects out (only hidden layers
could be kept), and the importer dropped data-* attributes.

- SVG export: hiddenLayers (on by default for document.save, as before)
  now keeps hidden objects too, each written display="none" on its own
  element (path, image, text or group). Exports still leave them out
  unless asked; the CLI's convert asks, as a conversion keeps the
  document.
- An object's own data: ObjectAttributes.data, the data-* attributes in
  order (ours, data-name and data-vc-*, aside). The importer keeps them on
  the objects and layers they belong to (at most 64 per element), the
  exporter writes them back, the native format saves them (missing in
  older files), and object.setProps {data: {key: value|null}} sets or
  removes them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:36:08 +07:00