Commit Graph
151 Commits
Author SHA1 Message Date
NetterSilverandClaude Haiku 5.5 7c8e772b46 Quote CSS font-family names that are not plain identifiers
Found while testing crates/svg/src/css.rs:112

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 15:20:11 +07:00
bflatastic 04139ad0b6 fmt (#3157) 2026-10-20 03:42:11 +07:00
MarekSpodBiedry 0f855be77f Add a generic family to the font-family of SVG export and CSS (#3135) 2026-10-20 03:41:17 +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
bflatastic eec304dde1 SVG export: a Japanese face's numbered weight (W0–W9) is its font-weight, W6 600 (#2873)
style_weight reads W0–W9 style names (Hiragino, Morisawa…) as their weight classes before the English words, so SVG and CSS export write font-weight 600 for W6, and nearest-style matching weighs them too.
2026-10-18 19:02:52 +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
dotCooCoo c73945d244 SVG import: hrefs that need no folder stay as written, and images in a non-absolute folder are read
SVG import checks each linked image's path with craft-docpath, and two
of its results were wrong, most visibly on Windows. A missing image's
link kept the path that craft-docpath returned, with its separators
rewritten: an href of /abs/photo.png became \abs\photo.png on Windows,
and SVG export with linked images writes that back as the href. A
folder that is not absolute, such as /tmp on Windows or a relative
folder on any system, made each relative href, and on Windows each
href rooted without a drive, resolve to a path that is not absolute,
so those images were not read. Two vectorcraft-svg tests failed on
Windows because of this.

A link now keeps the href as written (decoded) when the href resolves
to the same path without the SVG's folder, and keeps the path found
when it needs the folder: a relative path, or on Windows a path rooted
without a drive. A file: URL keeps the path it names. The folder is
made absolute against the working directory when it is not, as the
engine's reader does with the document's path. Hrefs that were refused
are still refused: paths on other computers, devices and URLs are not
read.
2026-10-18 12:15:29 +07:00
skylar taylor-barrick 299a4b36ad Merge pull request #2777 from skylartaylor/open-original
Open originals and linked files only on this computer or the document's share
2026-10-17 15:45:22 -06:00
skylar taylor-barrick d44dc983e6 Merge storytold/main into open-original
Conflicts were 17 i18n .tsv files where both sides appended new strings;
resolved as a union, verified to introduce no duplicate keys.
2026-10-17 12:15:10 -06: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 24b6486ebd Artboards: the doc comment names the shared corner the first artboard holds
On an edge two artboards share, the artboard to the right of it or below
it holds a point, except at the end of the edge that lies on that
artboard's bottom or right edge, such as where both artboards' bottom
edges meet. There the first artboard holds it, or the last when
topmost. The doc comment and the test comments now say so, and the test
asserts that corner.
2026-10-17 17:13:02 +07:00
dotCooCoo 8f87ad53dd Artboards: art centered on an artboard's right or bottom edge is on that artboard
Rearrange All Artboards, Select › All on Active Artboard (for objects
with no area), Copy's source artboard for Paste in Place, Align to
Artboard without an index, Crop Image, the Crop Image tool, Place with
`at`, CSS positions and a filter plug-in's artboard decided whether a
point or an object's center is on an artboard with Rect::contains,
which leaves out the right and bottom edges. A line along an artboard's right or
bottom edge was on no artboard: Rearrange left it behind, and All on
Active Artboard did not select it. Document::art_on_artboard counts
art flush with those edges (#890).

They now count the edges as art_on_artboard does, within a millionth
of a point. On an edge two artboards share, the artboard to the right
of it or below it still holds the point. All on Active Artboard
selects a line on a shared edge from either artboard. A press exactly
on a right or bottom edge now picks that artboard, and the Artboard
tool's cursor shows Move there.
2026-10-17 16:38:43 +07:00
NetterSilverandClaude Haiku 5.5 a631a0df6c Escape quotes and backslashes in exported CSS font-family
Found while testing crates/svg/src/css.rs:84 (font_props)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 16:38:42 +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
vangel76andClaude Opus 5.5 8875b3fbca Live Corners: a corner rounds until its cut reaches its neighbours (#2332)
A corner's radius was capped at half its shorter side whatever its neighbours did, so one corner of a 100 pt square rounded alone stopped at 50 pt. Each corner now rounds until its cut reaches the cuts its neighbours ask for: the whole side when the neighbour is square, halfway when both ask for more. The corner widget drag, the red limit outline and CSS border-radius export follow the same limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:20:26 +07:00
aiongg ef5084f48b Links: follow linked file paths only on this computer or the document's share
Paths stored with linked images and placed documents, and the hrefs of
SVG images, go through fileio::document_path (craft-docpath): a relative
path is taken from the document's folder, and only local files or files
on the document's own network share are looked at. Opening a document,
Link Info, Package, the linked-file watch and SVG import all use it; the
watch stats files through fileio, so automation roots apply to it too.

A path that is not followed counts as missing; document.open,
links.check and links.info say why in `refused`.
2026-10-16 17:55:48 +00: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
Guillem Hermida d05ccffebc effects: a Transform effect copies a group's symbol instances too, and stops image copies at MAX_REACH
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.
2026-10-16 06:16:35 +07:00
Marco Arnulfo 789a151ff9 SVG import: an alpha mask counts by its art's alpha, not its luminance (fixes #2210) 2026-10-16 06:16:35 +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
Lucas LerouxandClaude Opus 5.5 1e8f5025c6 Render, SVG: vello_cpu and usvg without their text features: one skrifa, read-fonts and harfrust fewer in the engine
The default `text` features of vello_cpu (glyph runs, through glifo) and
usvg (fontdb, harfrust 0.12, skrifa 0.44 and its read-fonts 0.41) build
another copy of the font stack into the engine, next to the workspace's
skrifa 0.47 and harfrust 0.13. Neither is used: the renderer draws the
glyph outlines vectorcraft-text gives it, and SVG import reads <text>
from the XML itself and never gives usvg a font (usvg leaves out text it
has no font for).

- vello_cpu: its default features but `text` (std, png, u8_pipeline),
  plus f32_pipeline as before.
- usvg: `svgz` only, for gzipped SVG images inside an SVG; `text`,
  `system-fonts`, `memmap-fonts` and `writer` are off. text_layout.rs uses
  usvg as a reference viewer with a font, so crates/svg turns `text` on as
  a dev-dependency feature, as crates/pdf does for krilla's `simple-text`.
- A test pins `svgz`: SVGZ images, linked or as a data URL, import as
  vector art.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 04:19:05 +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 b5b9264d71 Revolve: the Cap option is saved as cap, as Extrude & Bevel and Inflate save it 2026-10-16 01:36:23 +07:00
alpha-rock f703e6bd85 M9: smooth Revolve rendering and default Cap
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.
2026-10-16 01:36:23 +07:00
bflatastic f774968b20 xtask: the wasm check compiles the web app; the web's SVG depth cap follows from its stack
`cargo xtask wasm` (part of `cargo xtask ci`) checked the wasm-safe crates but not
apps/vectorcraft-web, so web-only code was never compiled before a merge. It now runs
`cargo check --target wasm32-unknown-unknown -p vectorcraft-web` too (no Trunk, no network).

The SVG importer's cap on the web was 48, sized from native measurements. rustc links the web
build with a 1 MiB stack (`-z stack-size=1048576`; the app passes no linker arguments of its
own) and usvg uses about 15 kB a level: half the stack at that cost gives 34, set in one place
with the arithmetic.
2026-10-14 10:13:45 +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
Saad Abdullah ad165a5a00 fix(svg): memoize descendant selector matching to stop combinatorial backtracking (#1703) 2026-10-14 08:01:50 +07:00
bflatastic c10da25ce4 Importers: nesting is capped, so a deeply nested file can't overflow the stack 2026-10-14 07:37:52 +07:00
bflatastic 301a99d91c SVG import: nested :not() selectors are bounded 2026-10-14 05:16:03 +07:00
Gert Pellin 6f9595542d SVG import: no slice indexing when writing a new style attribute 2026-10-14 05:16:00 +07:00
Gert Pellin 5f8c4e9ab5 SVG import: style sheet rules with :not() selectors apply (#1557)
usvg's style sheets (simplecss) drop rules whose selector has a :not(), so
rect:not(.keep) { fill: green } left the rect as it was. The text pass's own
cascade now reads :not() of a list of compound selectors (counted as its most
specific argument), and the import pre-pass writes what such rules set on an
element into its style attribute, with the value that wins the cascade there,
so usvg reads what CSS would: against other rules, the style attribute and
!important, inherited from groups, display: none and CSS transforms included.
Selector lists are split at top-level commas only.
2026-10-14 05:15:59 +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
dotCooCoo 21a386a6d4 SVG import: the style cascade looks up each element's rules by id, class and type
The SVG import's style cascade tested every <style> rule against each
element it was asked about. The import asks about the objects outside
<defs> and the like when the file contains `display`, about each shape
when the file contains `non-scaling-stroke`, and about each <text>, its
spans and its ancestors. On a file with 2,000 class rules and 100,000
rectangles, `vectorcraft-cli info` took 3.54 s with one `display`
attribute in the file and 2.03 s without it.

The rules are now filed under the id of their selector's rightmost
compound, else under its first class, else under its type. An element
is tested against the rules filed under its id, its classes and its
type, and the rules filed under none of these, in source order. It
matches the same rules in the same order and gets the same
properties. The file with the `display` attribute now takes 2.10 s.

Docs: docs/target-app-parity.md.
2026-10-14 04:56:30 +07:00
bflatastic 04bb6d6f27 SVG import: an Inkscape page whose box can't be placed is skipped 2026-10-13 13:36:41 +07:00
Gert Pellin ef0ab7ba9d SVG import: Inkscape's pages become artboards
Inkscape 1.2+ saves a multi-page document as <inkscape:page> elements
in its <sodipodi:namedview>; VectorCraft opened it with one artboard
the size of the root, leaving the other pages' art outside any. Each
page with a size now becomes an artboard at its place (through the
viewBox, as the art is), named by its inkscape:label or "Artboard N".
Files without pages keep their one artboard.

Fixes #1601
2026-10-13 13:36:41 +07:00
Gert Pellin 546c81624a SVG import: an unsupported filter's warning names the filter and the object
A filter that can't become a live effect warned "filter on a group
ignored", even on a plain <rect>: usvg wraps every filtered element in
a group of its own, which the message named. The warning now names the
filter and its primitives and the element (by id, or "an object"), and
says it is drawn without it: "filter 'f' (feColorMatrix) on an object
isn't supported; drawn without it".

Fixes #1598
2026-10-13 13:36:40 +07:00
bflatastic db85d465c8 SVG import: flowed text with a region too large to place is left out with a warning 2026-10-13 10:52:37 +07:00
Gert Pellin e794c1ac63 SVG import: Inkscape flowed text (flowRoot) becomes area type
<flowRoot> (SVG 1.2 flowed text, which Inkscape writes for text typed
into a frame) was dropped without a word: usvg doesn't draw it. The
text pass now reads it as area type: the first <rect> of its
<flowRegion> is the frame (with the rect's transform), each <flowPara>
a paragraph (white space collapsed), styled through the same cascade
as <text>. Another kind of region leaves the text out with a warning.

Fixes #1568
2026-10-13 10:52:37 +07:00
bflatastic 5fe916a425 SVG import: percentage lengths in a symbol with a viewBox are of the viewBox (#1561) 2026-10-13 09:58:14 +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
Gert Pellin 2c01a42b35 SVG import: CSS transforms in CSS syntax move objects; the font shorthand styles type
usvg takes a `transform` CSS property as a presentation attribute that
overrides the element's own, but reads its value as SVG: CSS units
(`translate(100px, 0)`, `rotate(45deg)`) don't parse, and the object
was left with no transform at all. Before usvg runs, transform
declarations in `style` attributes and `<style>` rules are now written
in SVG syntax (lengths in px, angles in deg/rad/grad/turn, translateX/Y,
scaleX/Y and skew as SVG's functions); values that can't be are left.

The text pass's own cascade now expands the `font` shorthand into its
longhands, as usvg does for the rest of the drawing, so text styled
with `font: italic bold 30px …` keeps its size, weight and style.

Refs #1557
2026-10-13 08:53:06 +07:00
Gert Pellin 3239e8b97e SVG import: <textPath> follows basic shapes (circle, ellipse, rect, line, polyline, polygon)
A <textPath href="#id"> pointing at a basic shape imported as point type
with "textPath has no usable path", as only <path> was followed. SVG 2
allows any basic shape, and Inkscape writes one whenever text is put on
a circle. The shape now becomes its equivalent path as SVG 2 defines
it: circles and ellipses start at 3 o'clock and rounded rects after the
top-left corner, all clockwise; lines, polylines and polygons follow
their points. The shape's transform applies as for a <path>. Shapes
that render nothing (zero radius or size) still fall back to point
type with the warning.

Fixes #1527
2026-10-13 06:56:37 +07:00
EricKuck 5b413448d9 SVG export: knockout groups of opaque art draw in order, without masks 2026-10-12 23:06:34 +07:00