Commit Graph
210 Commits
Author SHA1 Message Date
dotCooCoo 449dd5f5ed Preserve SVG styles with encoded attribute selectors 2026-11-03 16:03:40 +07:00
Brandon Thomas c6131cf798 Merge pull request #5243 from dotCooCoo/fix/svg-round-trip-edge-ellipse
SVG: an exported ellipse imports as the four-anchor ellipse it was written from
2026-11-03 00:18:50 -05:00
NetterSilverandClaude Haiku 5.5 6ecf286f6b Drop invalid XML characters from SVG link URLs and aria-labels
Found while testing crates/svg/src/export/nodes.rs:492 (Writer::node, `xlink:href` built with xml_escape)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 21:42:27 +00:00
dotCooCoo 442ae20e14 SVG: made-up ids skip every id the file names
An id the importer writes into an element no longer matches a style sheet's
#id selector or a <use> link that no element matched before. The ids are
read from the decoded attribute values and text of the file, once.
2026-11-02 03:22:10 -08:00
dotCooCoo 7c7a127ec8 SVG: a thin ellipse far from the origin imports with standard handles
An ellipse whose radius is smaller than the point tolerance at its coordinates, but at least two
f32 steps, now imports as the four-anchor ellipse instead of usvg's curves with rounded handles.
2026-11-02 03:09:08 -08:00
dotCooCoo b4d3ae41bf SVG: made-up ids skip the file's taken ids once
Each id made up on import starts where the last one stopped instead of at
the number of ids made so far, so a file that already holds many of the
made-up ids does not make every new id try them all again. The ids made
up are the same.
2026-11-02 03:04:04 -08:00
dotCooCoo 85b6c2a5eb SVG: ids made up on import never repeat an id in the file
The import gives an element an id when it needs to find it again in
usvg's tree (links, symbol uses, events, data attributes, and now
circles and ellipses). A made-up id could repeat one the file already
had, such as vectorcraft-id-0, so a <use> or paint reference to that
element showed the other one. Made-up ids now skip every id in the file.

Circles and ellipses in a file whose style sheet has an [id selector get
no made-up id and keep usvg's curves, so the selector matches the same
elements as before.
2026-11-02 02:59:51 -08:00
dotCooCoo 820a750310 SVG: a small circle far from the origin imports as a four-anchor ellipse
The tolerance that compares the points usvg makes of a <circle> or
<ellipse> grew with the center's distance from the origin, so a circle
of radius 400 at x 40,000,000 fell back to eight anchors. The tolerance
is now a few f32 steps at the size of the coordinates, and the on-ellipse
check allows the same steps relative to the smaller radius.
2026-11-02 02:53:34 -08:00
dotCooCoo 60e1600caa SVG: circles and ellipses import as four-anchor ellipses
usvg turns each <circle> and <ellipse> into arcs and then into a number
of cubics that depends on f32 rounding: an ellipse that export wrote as
<ellipse> could come back with eight anchors instead of the four it was
written from. The renderer flattens the two outlines differently, so a
round trip of a flat filled ellipse redrew 158 edge pixels by up to 43
levels, and prop_svg::plain_art_survives_visually failed on main.

Import now records the ids of <circle> and <ellipse> elements in the
prepass and rebuilds each as the ellipse VectorCraft draws: four smooth
anchors on the axes, starting at the right as SVG defines, with handles
KAPPA of the radii long, then transformed as before.

Tests: prop_svg::filled_ellipse_on_the_artboard_edge_survives_visually
(the failing case) and basic_shapes::circles_and_ellipses_import_as_the_ellipse_drawn
fail without the change.
2026-11-02 02:49:36 -08:00
dotCooCoo 83f2bfe9d6 Symbols: a scaled instance of a symbol with 9-slice guides keeps its corners and edges unstretched
Squashed from #5079 onto main.
2026-11-02 15:22:07 +07:00
Prin.IngandClaude Haiku 5.5 743f8c526b Filter invalid XML characters from SVG Dublin Core metadata
Found while testing crates/svg/src/export/output.rs:360 (dublin_core, around line 81)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 14:36:13 +07:00
dotCooCoo 427bce12b0 Character: Font Size can measure the cap height, x-height or ICF box
Squashed from #5026 onto main.
2026-11-01 11:08:55 +07:00
bflatastic f22caa6108 Fix #4938: Dynamic symbols keep per-instance appearance linked to the symbol 2026-11-01 04:53:38 +07:00
dotCooCoo bb6cbfb2b4 Images: SVG and EPS export write colorized grayscale and 1-bit images in their fill color
SVG export ignored an image's colorize paint and wrote the image's own
black, white and gray pixels. EPS export flattened every colorized image
into a process-color raster, so a spot color lost its plate. Both now
write the image in the fill color, with the coverage the canvas and PDF
export use (darkness times alpha), so white and transparent pixels take
no color.

- SVG writes a coverage PNG as a mask over a rectangle in the color,
  shared between repeated images while each keeps its own color. Linked
  mode writes the coverage PNG as a file, and SVG Tiny 1.2 writes a PNG
  in the color. A colorized image whose pixels are missing, or whose
  grayscale pixels can't be decoded, is left out with a warning.
- EPS writes fully covered or uncovered pixels as an imagemask in the
  color's process or Separation ink (Registration on every plate), and a
  grayscale image as ink samples scaled by coverage. Level 3 masks its
  uncovered pixels out and Level 2 writes them white; gray midtones are
  written over white, with the existing warnings.
- EPS flattening no longer counts a solid colorize paint on a grayscale
  or 1-bit image as transparency by itself. Opacity, blending modes,
  opacity masks, effects and overlapping transparent art still flatten
  it, and so does a colorize paint the writer can't draw.

The svg crate's tests turn on resvg's raster-images feature to render
the embedded PNGs.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-01 00:35:27 +07:00
Shunsuke Yoshizawa aaeb1b2b35 Respect SVG image-rendering pixelated attribute 2026-10-31 20:44:09 +07:00
SusieeTheFemboyandClaude Opus 5.5 2e7b2b7f96 SVG import: an embedded SVG image gets the feConvolveMatrix order check too
Files::svg parsed nested SVG images with usvg without the check the top-level
import makes, so an overflowing order inside an <image> still reached usvg's multiply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:43:17 +07:00
SusieeTheFemboyandGLM-5.3-Flash c7dc443ee1 SVG import: a feConvolveMatrix order that overflows usvg's multiply is refused
usvg 0.48 casts the filter's order attribute through i32, so a value like
1066570220609 saturates and the u32 order_x * order_y multiply overflows.
Only debug and overflow-checks builds panic on that; release builds already
import the file — the multiply wraps, and VectorCraft drops the filter as
an unsupported primitive with a warning, so nothing reaches the document,
render or export (observed on main in a --release build: both repro files
import without error; with a one-entry kernelMatrix the wrapped product
2147483647² mod 2³² = 1 even passes usvg's length check and leaves a filter
claiming a 2147483647×2147483647 kernel, still dropped downstream).

The import now parses order the way usvg does (svgtypes numbers, the same
as-i32 cast with its saturation, the first side defaulting the second, both
positive or both back to the 3×3 default) and refuses exactly the one case:
when the product of the two sides overflows u32 (checked_mul). Any order
usvg can multiply keeps importing as before, however large on its own, and
a missing, unparseable or non-positive order still falls back to 3.

Regression tests cover the overflow repros, the release wrap case, large
but fitting orders (100, 80 80, 65535 65535, 2147483647 1), the
pass-through shapes, and sane kernels, in debug and in release.

Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
2026-10-31 19:43:17 +07:00
Dylan M. Taylor a24d811c7b Update prompt shows the app icon and accent color instead of a letter (#4671)
UPDATE_APP (native builds; craft-update is a native-only dependency) carries our 128 px icon and accent (#e8573f, its colour in ArtCraft Launcher). The updater, the self-update
helper and craft-update's startup prompt all get them through crate::UPDATE_APP; the
prompt draws the icon, the accent and "by ArtCraft" (craft-update's default developer).

craft-update is pinned to storytold/craft-libs main at 1065653e7 (#434).
Clippy fixes for Rust 1.99 in untouched code (same logic):
- crates/svg/src/import/fx.rs
- crates/tools/src/text/tool.rs
2026-10-30 01:27:35 -04:00
bflatastic beb0ae6515 SVG export: rectangles, circles, ellipses, lines and straight paths as SVG shape elements (#4652) 2026-10-30 09:45:59 +07:00
bflatastic 7fd9313d06 SVG: the placeholder comment names the constant 2026-10-29 22:12:34 +07:00
el-j dc4150d7db perf(svg): compute unique ID prefix via single-pass placeholder replacement (#4555) 2026-10-29 22:12:21 +07:00
Ridzzz0Alam e110c6c770 Fix #3209: SVG export writes type sizes and spacing in CSS with px
With Styling set to Internal CSS, Inline Style or Style Entities, font-size
and letter-spacing were written as bare numbers. Only presentation attributes
take a bare number; in CSS a browser drops them, so the type was drawn at its
default size. They are now written in px, SVG's user unit, and presentation
attributes keep bare numbers.
2026-10-29 22:12:07 +07:00
Max MittelstädtandGPT-6-Luna 2b37302fe5 Preserve comment markers inside CSS strings
Found while testing crates/svg/src/import/css.rs:346

Co-Authored-By: GPT-6-Luna <noreply@openai.com>
2026-10-29 18:45:51 +07:00
dotCooCoo dfa9f883c0 Fix Drop Shadow Darkness in source color space 2026-10-29 14:36:41 +07:00
dotCooCoo ab69d8f6d9 Envelopes: Anti-Alias and Preserve Shape Using change how distorted images draw
Envelope Options stored Anti-Alias and Preserve Shape Using, but nothing that
draws an image read them: a distorted image was always smoothed and always
cut into clip groups.

- With Anti-Alias off, each piece of a distorted image is marked pixelated
  (saved as `pixelated`). The renderer draws it with nearest-neighbor
  sampling and without mips, SVG export writes `image-rendering: pixelated`,
  and PDF export writes it without /Interpolate.
- With Preserve Shape Using Transparency, each piece is the image with a
  white opacity mask in the piece's shape instead of a clip group. The
  renderer paints an image whose mask is one plain white path straight into
  that path, as it does for an image in a clip group, when the path lies
  inside the image's frame.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-29 11:07:57 +07:00
bflatastic cfe21231c8 SVG import: import NodeId and SvgError in importer.rs (#4299 made its imports explicit) 2026-10-29 09:57:33 +07:00
el-j 39341ef4c8 Fix #3564: report TooManyImages error when nested SVG image cap is reached 2026-10-29 09:57:33 +07:00
el-j b65fb23338 fix(svg): cap converted objects and nested images (#3564) 2026-10-29 09:57:33 +07:00
Dylan M. Taylor 7e44df55a9 Make renderer and SVG module dependencies explicit 2026-10-28 05:03:00 +00:00
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 412dbcd632 refactor(svg): move import.rs items to import/{convert,importer,layers,prepass}.rs 2026-10-26 22:01:25 +00:00
aiongg 7e52760174 refactor(svg): move export.rs items to export/{nodes,output,paint,text}.rs 2026-10-26 21:45:32 +00:00
aiongg 1101e04da5 refactor(svg): move import/css.rs tests to import/css/tests.rs 2026-10-26 18:18:18 +00:00
NetterSilverandClaude Haiku 5.5 29a98b750a Respect quotes and parentheses when splitting SVG CSS declarations
Found while testing crates/svg/src/import/css.rs:21 (parse_decls)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:43:20 +07: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
NetterSilverandClaude Haiku 5.5 a579ca7073 Require two hex digits in SVG href percent escapes
Found while testing crates/svg/src/import/files.rs:82 (percent_decoded)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 23:32:52 +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
NetterSilverandClaude Haiku 5.5 f8ad2edad8 Match !important case-insensitively in SVG import CSS parser
Found while testing crates/svg/src/import/css.rs:25

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 17:19:59 +07:00
Jürg Lehni deb517fbbe Type: an SVG glyph's image is an SVG image by its plain href first
The check for images of SVG documents looked at href and xlink:href alike, so an image whose href is a PNG was cut for an SVG xlink:href; it now reads href, else xlink:href, as the importer does (SVG 2).
2026-10-24 07:12:16 +07:00
Jürg Lehni 4d1b4a3392 Type: SVG glyphs leave out images of SVG documents
The importer expanded an image whose data is an SVG document into art; the SVG table asks for it to be ignored, and a glyph draws no documents inside it.
2026-10-24 06:51:20 +07:00
Jürg Lehni caaccaf5be Type: images in SVG glyph documents draw with the glyph
They were left out because glyph art had no document to keep them in; since #3879 they hold their own pixels until a document takes them in.
2026-10-24 06:00:22 +07:00
BFlat e0c1548049 Merge pull request #3896 from storytold/fix/3869-svg-type-features
SVG export: type with small capitals, superscript, subscript or tab leaders is written as outlines (#3869)
2026-10-24 04:51:54 +07:00
aiongg 6877d6535f Links: resolve stored paths with craft-docpath's resolve_absolute (#3267)
* Links: use resolve_absolute for stored paths

* CI: release only with git dependencies from storytold

* Release: check git sources from the parsed Cargo.lock and cargo config

The grep let any character stand for the dot in github.com, passed a missing or stale
Cargo.lock, and missed source replacement in .cargo/config.
2026-10-23 17:51:28 -04: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
bflatastic 9827bfbcbb SVG import: the shared appearance needs no conversion (clippy) 2026-10-24 03:52:24 +07:00
bflatastic d9e524df30 SVG entity scan: look for a reference's ';' no further than the longest name 2026-10-24 03:52:24 +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
bflatastic 422ecc0c77 Artboards: Artboard Options shows video safe areas and sets the pixel aspect ratio that View › Rulers › Show Video Rulers counts the artboard in
Squashed from #3831 onto main.
2026-10-24 03:09:07 +07:00