Commit Graph
83 Commits
Author SHA1 Message Date
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
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
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
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 0724aafbb6 #1172: share an object's appearance with its undo history until it changes
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
2026-10-11 23:27:21 +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
Biromedic f4035c98d1 Fix #1141: SVG import keeps a mask whose art is all hidden, so its object stays hidden
Mask art that is all visibility:hidden imports as nothing, and opacity_mask
then returned no mask: the object was drawn whole. An empty mask is what the
file means (black all over); the renderer and the PDF and SVG exports already
draw empty mask art by its clip and invert options.
2026-10-11 16:00:38 +07:00
imachusandClaude Opus 5.5 621e1a6640 Type: Proportional Metrics — full-width glyphs on the font's proportional widths, palt (#966)
A character attribute (Character panel menu with the East Asian options;
`text.setFormat` / `text.setRangeStyle {proportionalMetrics}`), saved only
when on. In horizontal type it applies `palt`; vertical type sets as before
until the vertical shaping change (`vpal`) follows.

Line-end Punctuation Half Width takes nothing more off the punctuation
`palt` re-spaced (the flush opening bracket, consecutive punctuation, the
line-end half width): the segment is shaped once more without `palt`, and
the glyphs whose advance differs are marked. Punctuation the font leaves
full width is trimmed as before. SVG writes `"palt" 1` with the
character's other features.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:00:35 +07:00
bflatastic a0438f73f9 #1010: SVGs with one absolute and one unitless root side import unstretched
A root such as width="96pt" height="96" took CSS pixels (0.75 pt) along x and points along y,
so a square came in 24 x 32 pt and the artboard 96 x 96 instead of 96 x 72; with a viewBox the
art was stretched too.

When either root side is absolute, both axes now take CSS pixels at 96 per inch (usvg already
parses at that resolution): the unitless or px side is the same CSS length it would be in a
browser. The root scale is one number.

The test opens width="96pt" with height 96, 96px and 72pt, and width 128 with height 72pt: a
96 x 72 artboard and a 24 x 24 square; with viewBox 0 0 32 32 a 72 x 72 square at x 12 (meet),
and with preserveAspectRatio="none" the whole artboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	ROADMAP.md
2026-10-11 03:51:43 +07:00
alpha-rock c36b6976e3 M9: add live Revolve with artboard rotation and lighting controls 2026-10-10 23:44:46 +07:00
bflatasticandClaude Opus 5.5 a2e4206155 Part of #372: SVG import names the one layer from the root element
Other editors export a document with a single layer without a layer group:
the layer's name goes on the root <svg> (id, or data-name) and its art sits
directly inside. Such a file opened as "Layer 1"; now its one layer takes the
root's name. Files whose top-level groups are the layers are unchanged, and an
Inkscape root's generated id (with inkscape:version) isn't taken as a name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:00:24 +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
haven d2bb95daab M7: area type vertical alignment
Area Type Options > Align: Top / Center / Bottom / Justify (AreaOptions.verticalAlign,
LayoutOptions.vertical_align). Each row/column cell is aligned on its own; rectangular
cells shift their lines, other frames and text wrap reflow until settled without
losing text. Justify shares the leftover space equally between line gaps. Area Type Options
shows Align and First Baseline as dropdowns (the parameter dialog's choices table).
2026-10-08 14:28:02 -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
bflatasticandClaude Opus 5.5 44c7e64e1b Fix #442: alike circular corners on rectangles that aren't square
Live rectangles drew elliptical corners after an uneven scale in v0.4.0;
inconsistent on a rectangle that isn't square:

- doc: LiveShape::fold_scale moves an uneven scale left in `xf` (files
  saved before #291) into the size and radii; `transform` uses it, and
  `folded()`/`corner_style` give Live Corners document units.
- engine: object.setLiveShape folds that scale first, so setting a radius
  or kind (fields, dialog, widgets, MCP) gives circular corners on such
  shapes instead of keeping the ellipses.
- geom: max_corner_radius/fitted_corner_radius are the one limit every
  corner shares (half the shorter side).
- tools: corner widgets sit at the centres of the corners as drawn (a
  radius past the limit put them off the arc, further in along the long
  side), drag in document units on stretched shapes, and outline the
  edited corners in red at the limit, as the reference app does.
- svg: CSS export writes the drawn radii (CSS would draw a radius past the
  limit when its neighbours leave room).
- docs: mcp.md Live Corners, ROADMAP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:34:37 +07:00
bflatasticandClaude Opus 5.5 1706e84829 Fix #429: Type on a path brackets, flip and Type on a Path Options that keep the type in place
Type on a path could only be repositioned by Type on a Path Options, whose menu item sent
`{start: 0}`, so OK threw the text to the start of its path; nothing showed or dragged its
brackets. Now, as in Illustrator:

- Type on a path flows between a start and an end bracket (`TextKind::OnPath { end }`, None:
  the end of the path or once round a closed one; `TextKind::path_span`). Flipping
  (`TextObject::flip_on_path`) reverses the path keeping a closed path's first anchor and swaps
  the brackets, so the type keeps its stretch of the path.
- Type on a Path Options gains Align to Path (ascender, descender, center, baseline) and
  Spacing (glyphs spaced as if set that far above the path: closed up round the outside of
  curves, opened up round the inside), laid out by the text engine.
- `type.pathOptions {start?, end?, flip?, effect?, alignToPath?, spacing?, ids?}` sets them in
  one undo step; with none of them it queries. The menu item opens the dialog with the selected
  type's Effect, Flip, Align to Path and Spacing (dropdowns for the choices) and leaves the
  brackets alone, so OK as opened changes nothing.
- The Selection and Direct Selection tools show selected type on a path's path and its start,
  centre and end brackets (`tools::pathtype`, with a bracket cursor): dragging the start or end
  bracket sets where the type runs, dragging the centre bracket slides it along the path and,
  across the path, flips it (Cmd/Ctrl held: it only slides). Each drag is one undo step.
- Arc-length measuring moved to `vectorcraft_geom::ArcPath` (layout, the tools and
  `text.createInPath` share it). SVG export outlines path type whose end bracket, alignment or
  spacing `<textPath>` can't express.
- docs/mcp.md documents the command and the drags; ROADMAP updated; dialog strings translated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 12:50:25 +07:00
bflatasticandClaude Opus 5.5 55543da0cb Fix #420: SVG import keeps clipped layers as layers with a clipping mask
A top-level <g id> becomes a layer only when it was plain, so an Inkscape
layer (or our own export of a layer with a clipping mask) carrying a
clip-path came in as a clip group, and a file whose layers were all
clipped to one frame opened as one "Layer 1" of clip groups with the
layer names lost.

A top-level group that does nothing but clip its art (no mask, filter,
opacity or blend) is now a layer too: its clip path becomes the layer's
clipping mask (the clipping path first, as Make Clipping Mask in the
Layers panel leaves it), a clip path clipped in turn nesting a clip
group inside the layer as before. The clip building is shared with clip
groups. Top-level text that joins a clipping layer (before or after it)
isn't in its clip: the mask then becomes a clip group inside the layer
and the text sits beside it. A clip path with no shape leaves the layer
empty.

Tests: the issue's Inkscape layers (names kept, each a clipping layer,
drawn as resvg draws the SVG), a nested clip on a layer, the export ->
import round trip of clipping layers, and text joining clipping layers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:29:38 +07:00
bflatasticandClaude Opus 5.5 ffed92aba5 Fix #396: SVG type opens at the size it draws at
SVG import kept a <text>'s nominal font-size and left the scale of the
transforms and viewBox above it in the type's transform, so the Character
panel showed one size and the canvas drew another (7 pt under scale(3)
drew at 21 pt).

The importer now moves that scale into the type: the scale across the
baseline (the mean scale for type on a path) multiplies the size,
leading, baseline shift, character stroke weights and dashes and the
text path, and the transform keeps only the rest (a stretch along the
baseline, a skew, a rotation or a reflection). The type draws exactly as
before. Paints resolve in the grown text space, non-scaling strokes and
the stroke under the characters are worked out before the fold, the
factor is rounded to 6 significant digits (usvg's transforms are single
precision) and never pushes sizes past the Character panel's
0.1–1296 pt (the rest stays in the transform).

Tests: a group's, the text's own and a viewBox scale; rotated and scaled
text equal to the same text written at twice the size; stretch and skew;
type on a scaled path; the size limits; gradients on scaled text; SVG
export → import of scaled type (stable over two round trips).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:18:34 +07:00
bflatasticandClaude Opus 5.5 b05c14f8eb Fix #381: round single corners with Direct Selection (per-corner Live Corners)
A live rectangle stored four corner radii but drew them all with the largest,
and the corner widgets always rounded all four corners, so one corner could not
be rounded on its own.

- geom: rectangle_with_corners draws each corner with its own radius and kind
  (Round, Inverted Round, Chamfer); a sharp corner is one anchor, a cut one two.
  Uniform corners keep the exact rounded-rectangle anchors.
- doc: LiveShape::Rectangle gains `kinds` (saved only when a corner isn't round;
  older files read as round), plus helpers mapping anchors to corners.
- engine: object.setLiveShape takes `corners` and `kind`; without `corners` it
  edits the Direct-Selected corners, else all four, and keeps the selected
  corners selected as their anchors change.
- tools: with Direct Selection, only the selected corners show widgets and a
  drag rounds just those; Alt-click cycles the corner kind; double-click opens
  the new Corners dialog (also `ui.corners`).
- ui: Properties/Transform radius fields show and set the selected corners;
  CSS export keeps per-corner radii and skips cut-in corners.
- docs: mcp.md (Live Corners), control-protocol.md (Corners dialog), ROADMAP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 06:56:32 +07:00
bflatasticandClaude Opus 5.5 ab62b5f8e8 Fix #320: CMYK images stay CMYK from PDF import to PDF export
A PDF's DeviceCMYK image opened as RGB (the interpreter's pixels as a PNG),
so every export wrote it as DeviceRGB or ICC RGB, silently. Images now keep
their CMYK samples, as CMYK colours already did:

- Model: CMYK images are blobs holding CMYK samples, a four-component JPEG
  (Adobe's inverted samples) or a Deflate CMYK TIFF. vectorcraft-doc's cmyk
  module reads their ink amounts (ImageBlob::is_cmyk, ::cmyk, capped at 64
  megapixels) and writes the TIFF (ImageBlob::cmyk_tiff).
- PDF import: DeviceCMYK, CalCMYK and four-component ICC images keep their
  samples. A JPEG with Decode [1 0 1 0 1 0 1 0] is kept byte for byte; any
  other is decoded (8 or 16 bits, its Decode array applied) and stored as a
  CMYK TIFF. Masked CMYK images and 1/2/4-bit ones open in RGB with a warning.
  Placing a CMYK TIFF keeps it CMYK too.
- PDF export: CMYK images are written in CMYK (DeviceCMYK, or ICC-based with
  the CMYK profile when colours are tagged: PDF/X-3, PDF/X-4, PDF/A). A CMYK
  JPEG neither resampled nor recompressed passes through unchanged; others are
  resampled in CMYK and compressed as ZIP or as a CMYK JPEG (jpeg-encoder,
  which also replaces the ZIP fallback and its warning for images converted to
  CMYK). Output conversion treats them like CMYK colours: Preserve Numbers and
  PDF/X-1a keep their numbers, Convert to Destination converts them to another
  CMYK profile or to RGB through a 17^4 lookup table (cms::CmykLut, now also
  behind the renderer's ink table). A CMYK image that can't be decoded is
  reported in the warnings.
- Canvas: CMYK images show through the working CMYK profile, as CMYK fills
  do, and paint their own inks in ink planes (CMYK raster exports, CMYK
  blending). SVG export writes TIFF images as PNG.

Illustrator's Output › No Conversion keeps colour data as it is
(https://helpx.adobe.com/illustrator/using/pdf-options.html).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 06:04:07 +07:00
Daniel Efrat edc7c931dc Type: Hebrew and Arabic text laid out right to left (bidi), aligned by its direction
- Unicode bidi (unicode-bidi, unicode-script, unicode-general-category):
  each paragraph's direction from its first strong character, Hebrew and
  Arabic shaped right to left, embedded Latin and numbers keep theirs,
  lines wrapped in logical order and reordered per line by whole
  clusters; source text, undo and style ranges stay logical.
- Caret, hit testing, selection and the arrow keys follow the visual
  order.
- New text aligns automatically: right-to-left paragraphs right, others
  left (Justify::Auto, text.setStyle {justify: "auto"}); the Paragraph
  panel highlights the resolved alignment and has an Automatic alignment
  toggle. Explicit alignments, and older documents', are kept.
- SVG export outlines bidirectional text (with a warning) until it can
  be written as directional live text.
2026-10-08 05:24:44 +07:00
BFlatandClaude Opus 5.5 079f9a8b9a Fix #296: variable fonts list and draw every named style
The font database made one style per font file, so a variable font
such as Figtree[wght].ttf listed only its default instance (Light).
Faces now also yield their fvar named instances, each a style of the
family named by its subfamily name (the default instance is the face
itself; unnamed or duplicate instances are left out; axes and
instances read are capped). The system font scan reads the fvar table
beside the name table, so installed instances are listed before the
file is loaded and found by name and by PostScript name.

An instance face carries its axis settings: skrifa draws its outlines
and reads its metrics and advances at that location, harfrust shapes
with a ShaperInstance, so layout, the canvas, Create Outlines and hit
testing all use the instance. PDF export embeds it through krilla's
Font::new_variable, SVG @font-face through subsetter's
subset_with_variations: both write a static font of that instance.

Tests build a small variable font in code (test_fonts, via
write-fonts; no font file is committed) and check listing, resolving,
glyphs and advances, PDF and SVG export and damaged fvar/gvar tables;
Bahnschrift and Segoe UI Variable are checked where installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:01:26 +07:00
bflatasticandClaude Opus 5.5 30550f038a Fix #136: SVG export and CSS write each style's numeric font-weight
font_props wrote `bold` for any style whose name contained bold, black
or heavy and nothing for the others unless fonts were embedded, so
Semibold came back Bold and Light or Medium came back Regular. It now
writes the style's numeric weight (font_descriptor, from style_weight)
for every writer: 600 for Semibold, 300 for Light, 900 for Black, `bold`
for 700 and nothing for 400. The SVG import already reads these numbers
back to the matching face.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:36:29 +07:00
bflatasticandClaude Opus 5.5 535220cf34 Fix #139: SVG import paints paint-order="stroke" text strokes under the letters
The text import reads <text> from the XML and never looked at
paint-order, so character strokes always painted over the fill. A text
whose characters all share one stroke painted first now gets that
stroke as the object's own stroke below the Characters row (as the
canvas, SVG and PDF export already draw it), with its gradient or
pattern resolved in the document; characters with differing strokes
keep them and the import warns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:36:29 +07:00
bflatasticandClaude Opus 5.5 65dd757d59 Fix #131: SVG import keeps non-scaling strokes at their screen width
usvg knows vector-effect but drops it from the tree it returns, so every
stroke was scaled by its transforms. The importer now records the shapes
that ask for vector-effect="non-scaling-stroke" in its XML prepass (by id)
and gives their strokes and dashes the width they have on screen; type
does the same on its character strokes. Where the width can't be kept
(art in patterns, markers or <use> copies, stretched or skewed type) the
import warns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:36:29 +07:00
bflatasticandClaude Opus 5.5 ed4eceb837 M4.96: Window › CSS Properties shows, copies and exports the CSS of the selection
The svg crate's new CSS writer (css_rules) gives the CSS web pages style objects
with: rectangles and ellipses as background-color / linear-/radial-gradient(),
border and border-radius; type as font, color, spacing and alignment; shadows,
glows and blur as box-shadow, text-shadow and filter. It shares SVG export's
declarations (transparency, fonts, type spacing, colours, gradient stops), which
moved into svg/src/css.rs. Options: units, absolute position, width and height,
unnamed objects, rasterize unsupported art (as a PNG background-image).

Commands css.selection, css.generate and css.export; UI commands css.copy and
css.exportFile; the CSS Properties panel with a highlighted code view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:03:11 +07:00
bflatasticandClaude Opus 5.5 33023c5a2b M4.98: freeform gradients export to SVG as images clipped to their shapes
SVG has no freeform shading: an object's freeform fill or stroke is now
written as an <image> of the colour field over its painted box, sampled
on the grid the canvas uses (twice the box's size in points), clipped to
the shape or to the stroke's outline, with the fill or stroke opacity and
blend mode on the group. Linked image mode writes the PNG next to the
SVG. Character paints keep the linear approximation (with a warning);
gradients along or across strokes stay vector slices. The grid sizing
moved to the colour crate so the canvas and the export share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 13:25:16 +07:00
bflatasticandClaude Opus 5.5 fa3dcab411 M4.97: SVG Options choose the encoding, SVG Tiny 1.2 and embedded font subsets
SVG export writes UTF-8, UTF-16 (big-endian with a byte order mark) or
ISO 8859-1 (other characters as character references) and SVG/SVGZ
open in any of them. The Tiny 1.2 profile (simplified) writes
presentation attributes only and leaves out filters, masks, blend
modes and symbols with warnings. embedFonts embeds the fonts type uses
as @font-face subsets of the characters used, honouring the fonts'
fsType (whole when subsetting is forbidden, left out when embedding
is). New options: encoding, profile, embedFonts (SVG Options dialog,
document.formats); document.serialize also returns dataBase64 for
non-UTF-8 files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 13:25:15 +07:00
bflatasticandClaude Opus 5.5 2925e7f9b5 M4.63: Links panel with status, Relink, Go To, Update, Edit Original, Embed/Unembed, Link Info and Placement Options
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>
2026-10-05 12:40:05 +07:00
bflatasticandClaude Opus 5.5 303fc3cbca M4.42: SVG import reads linked images from the SVG's folder, intersects nested clips, keeps hidden objects and expands reflected gradients
document.open {path} (and File > Place) find an SVG's relative <image>
links in its folder: raster files stay linked images (LinkInfo with size,
time and hash), SVG files and data: SVGs import as vector art, and a file
that can't be read becomes a placeholder in its box that keeps the link,
with a warning and a missingLinks row (Update Links / Relink read it
later). The SVG library itself reads no file unless given a reader.
Nested clip paths become clip groups in clip groups; undisplayed objects
(display:none) come back hidden; spreadMethod reflect and repeat are
expanded into stops over the painted area (matching resvg). Midpoint stops
and focal points keep folding back as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:12:06 +07:00
bflatasticandClaude Opus 5.5 0d75df4da8 M4.41: SVG import keeps symbols, patterns and blur/shadow filters live
<symbol> with <use> imports as a symbol (named after data-name or its id)
with one instance per <use>; a <use> that shows the symbol differently
(inherited paint, a viewport that cuts it) or that the canvas would paint
differently under its transform stays plain art. feGaussianBlur,
feDropShadow and the usual shadow chains (offset, blur, flood or colour
matrix, composite, merge) become Gaussian Blur, Drop Shadow, Outer/Inner
Glow and Feather effects; other filters are still reported. Export records
the symbol name and the shadow/glow blend mode so export -> import keeps
them. <pattern> keeps importing as a pattern swatch (now tested).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:11:27 +07:00
bflatasticandClaude Opus 5.5 2e5ec7b251 M4.53: Linked images remember their file and keep a preview; missing links ask Replace or Ignore, modified ones follow Update Links
A linked image (`ImageObject.link`, now a LinkInfo: absolute path, path relative to the document,
size, modification time, hash, page; old path strings still load) keeps a low-resolution preview
(`ImageBlob.proxy`, at most 256 px a side) that a native save writes in place of the pixels of
images only linked images show. Opening a native document reads the linked files again, looking
for each at its path, then relative to the document and by name in its folder: missing files
show their preview, modified ones are read again only with Update Links: Automatically.
`document.open` reports `missingLinks`, `modifiedLinks` and `updatedLinks`.

links.check reports each link's status, links.update reads modified files again and
links.relink points images at another file or at same-named files in a folder (each keeps its
bounds; one undo step). In the app, a document opened without its linked files asks about each
(Replace…, Ignore, Apply to All), then offers to update modified links when the preference asks.
The renderer caches previews apart from the file's pixels; the Eyedropper samples previews in
the object's pixel space.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:07:10 +07:00
bflatasticandClaude Opus 5.5 74b5a904b7 M4.40: Preserve Editing SVGs reopen exactly, or as plain SVG once edited elsewhere
The editing data now sits in CDATA inside <metadata> with a hash of the
markup around that element (whitespace between tags and line endings
don't count). document.open restores the native document when the hash
matches (data written without a hash is trusted as before); when another
app changed the SVG, or the data can't be read, it imports the SVG and
says why in its warnings. vectorcraft_svg::editing reports the data and
whether it is intact; editing_data stays. Live effects, symbols and
patterns come back exactly; the import fuzz test now mutates saved SVGs
with editing data and hidden layers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:03:52 +07:00
bflatasticandClaude Opus 5.5 370c6e2fc0 M4.39: SVG symbols as <symbol>/<use>, hidden layers on Save, Dublin Core metadata
Symbol instances export as one <symbol> def per symbol and a <use> per
instance. An instance the def can't stand for exactly (stained by its fill,
or a transform under which the canvas paints differently: scaling with
strokes, effects or live objects, turning with effects or brushes, any move
with pattern paints or unlinked masks) is written as its own art, now
stained like the canvas and with its patterns and unlinked masks left on
the page. Symbols nested in themselves stop at a fixed depth.

New SVG option hiddenLayers keeps hidden layers as display:none groups;
document.save (and Save As / Save a Copy) turns it on unless told
otherwise, exports leave hidden layers out. Import shows display:none
layer groups to usvg and marks them hidden, so they reopen hidden
(groups a <use> refers to stay templates). File Info metadata goes through
one Dublin Core list. The symbol stain helper moves to vectorcraft-brush
so the renderer and the SVG writer share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:03:52 +07:00
bflatasticandClaude Opus 5.5 b75ed0877d M4.29: object ids and names survive the SVG round trip
Layer Names writes readable ids plus data-name with the original name
whenever the id differs; Minimal writes only the ids definitions need;
Unique prefixes every id and CSS class with a content hash, so SVGs
inlined in one page never collide. Import names objects and layers from
data-name, inkscape:label, serif:id or aria-label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:57:44 +07:00