Commit Graph
403 Commits
Author SHA1 Message Date
BFlat ccb62b0912 Merge pull request #7660 from dotCooCoo/fix/svg-use-chain-bounded
SVG import: refuse an SVG whose use copies and styles expand past the limits
2026-11-15 05:27:22 +07:00
dotCooCoo df257c2d53 Merge main into fix/svg-raster-keeps-resolution 2026-11-14 12:12:06 -08:00
dotCooCoo 8ea413f054 SVG import: measure what an SVG expands to before the style cascade reads it
The depth check and the prepass read each element's declarations through
a cascade that keeps a copy of them per element, so one 100 kB style
sheet rule on 16,000 groups still took 1.5 GB before the SVG was refused.
The SVG and each SVG image it shows are now measured before that cascade
runs. Copies nested past the depth cap, and loops of <use> references,
are found as too deep: an SVG is refused with the depth error, and an SVG
image is left out with the depth warning, as before.
2026-11-14 11:19:42 -08:00
dotCooCoo 2c5f3b9d20 SVG import: refuse an SVG whose use copies and styles expand past the limits
usvg copies the element a <use> shows, and every copy gets its own copy of
the declarations its style attribute and matching style sheet rules give
it. A chain of <use>s that doubles at each level made usvg build those
copies before the import's object limit was checked: a 101 kB file whose
rectangle has a 100 kB style took 4.7 GB at 14 levels, and one 100 kB
style sheet rule on 16,000 groups took 1.5 GB without any <use>. A plain
chain stopped at usvg's own 1,000,000-node limit with a parse error.

The import now measures, before usvg reads the SVG, how many elements it
expands to and how many bytes of style values those elements copy, each
element measured once and each <use> counted as one addition. References
follow usvg's rules: first element of an id, href before xlink:href, and
the self and back references usvg leaves out. An SVG past 999,999
elements (500,000 on the web) is refused as too large, a loop of <use>
references usvg would copy without end is refused too, and one past 64 MB
of copied style values is refused with the new SvgError::StylesTooLarge.
SVG images count toward the same 64 MB as the SVG showing them, an image
past either limit refuses the whole import, and an image that doesn't
decode does not count.
2026-11-14 10:33:00 -08:00
BFlat 7a808e2378 Merge pull request #7536 from dotCooCoo/fix/svg-rtl-absolute-chunks
SVG import: absolutely placed characters in right-to-left text land on their x
2026-11-15 00:38:32 +07:00
dotCooCoo 96a8a0c6d6 Merge main into fix/svg-raster-keeps-resolution 2026-11-14 07:23:45 -08:00
BFlat 0994e5332a Merge pull request #7564 from dotCooCoo/fix/svg-direction-any-case
SVG import: direction keywords match in any letter case
2026-11-14 21:51:39 +07:00
BFlat d31457281a Merge pull request #7436 from dotCooCoo/fix/svg-masked-shadow
SVG export: shadows in a masked group show in readers that bound it by its shapes
2026-11-14 21:48:10 +07:00
BFlat 6c1f0d8824 Merge pull request #7570 from dotCooCoo/fix/svg-nested-image-links-read
SVG import: linked rasters inside a linked SVG file are read
2026-11-14 21:47:30 +07:00
dotCooCoo e2a8b9f66d Merge main into fix/svg-rtl-absolute-chunks 2026-11-14 05:58:49 -08:00
BFlat 035d22b3e2 Merge pull request #7476 from dotCooCoo/fix/svg-clip-path-effects
Effects: a clipping path's geometry effects shape the clip on the canvas
2026-11-14 20:38:37 +07:00
BFlat 48b947692b Merge pull request #7576 from dotCooCoo/fix/svg-path-type-before-start
SVG import: type on a closed path wraps around past the path's start
2026-11-14 20:35:12 +07:00
dotCooCoo 1ec44403c1 SVG import: reading the links of linked SVG files stops at the read and text limits
Reading an SVG file that an <image> links to, and gathering its own
links, counts toward the 1,024 reads and 512 MB of text, wherever the
file is linked from. Inside a linked SVG file, every file read for its
links, raster or SVG, also counts toward the 512 MB before it is
decoded, and once either limit is passed no file is read there: the
image is left out with a warning. An SVG file the imported SVG links to
is still shown past the limits, but the files its own links name are
not read, with the same warning. Rasters the imported SVG links to are
read as before.

Every SVG file read is kept by its file, wherever it was read, so
another href to it (another spelling, or the same file from another SVG
file) does not read it again, past the limits too. An SVG file whose art
depends on where it is shown (too deep there for its links to be read,
or with a link that leads back) is not kept, so a link to it from a
shallower place shows the rasters it links to.
2026-11-14 05:20:31 -08:00
dotCooCoo 7dbbd8334f Merge main into fix/svg-nested-image-links-read 2026-11-14 05:20:28 -08:00
dotCooCoo 41853dbe4d Merge main into fix/svg-direction-any-case 2026-11-14 05:15:42 -08:00
dotCooCoo b3c2fdab21 Merge main into fix/svg-raster-keeps-resolution 2026-11-14 05:11:34 -08:00
dotCooCoo 0bdf6823f3 Merge main into fix/svg-masked-shadow 2026-11-14 05:11:15 -08:00
dotCooCoo 8de86c9144 Merge main into fix/svg-rtl-absolute-chunks 2026-11-14 05:10:03 -08:00
BFlat 95c9f347d6 Merge pull request #7507 from dotCooCoo/feat/cjk-tsume
Type: Tsume narrows the space around each character's ink
2026-11-14 19:38:02 +07:00
dotCooCoo 490c803080 SVG import: type on a closed path wraps around past the path's start
Text on a path of one closed subpath that is anchored to start before
the path's start point (text-anchor end or middle, right-to-left start,
or a negative startOffset) started at the start point, and text offset
past the path's end started at its end. It now wraps around the path,
as SVG 2's startOffset attribute describes for a single closed subpath
(https://www.w3.org/TR/SVG2/text.html#TextPathElementStartOffsetAttribute):
text-anchor end at startOffset 0 ends at the start point. Open paths
and paths of several subpaths still start the text at the path's start
or end.
2026-11-14 04:04:48 -08:00
dotCooCoo 4eb1b1cf99 SVG import: linked rasters inside a linked SVG file are read
An SVG file that an <image> links to was parsed with usvg's
from_data_nested, which drops every href to a file, so a PNG or JPEG
that the SVG file linked to disappeared without a placeholder or a
warning.

The files a linked SVG file's own <image> elements link to are now read
the way the imported SVG's links are, relative paths from that file's
folder: a raster stays linked, an SVG file imports as vector art, and
one that can't be read (missing, or refused by the reader) gets the
placeholder stretched to its box, its link and a warning. The same href
in SVG files of two folders names two files. A link that leads back to
an SVG file showing it gets the placeholder and a warning. Reading the
links of linked SVG files stops at the element depth cap and at 1,024
reads and 512 MB of text per import, the same limits decoded nested SVG
images have, counted separately from them. An SVG data URL still reads
no file links, having no folder of its own.

SVG 2 tells a viewer to ignore external references in an SVG shown by
<image>; reading them on import is VectorCraft's own rule.
2026-11-14 03:53:02 -08:00
dotCooCoo c764c59825 SVG import: plaintext finds its first strong character across tspans
With `unicode-bidi: plaintext`, SVG import looked for the first strong
character of an element in each text node on its own and kept the
first answer. An isolate that opened in one text node and closed in a
later one hid the rest of that node, and the node then answered left
to right. For `&#x2067;ab<tspan>&#x2069;&#x5D0;</tspan>` the element
ran left to right, where UAX #9 P2 skips the isolate and finds the
right-to-left letter after it
(https://www.unicode.org/reports/tr9/#P2).

The characters of the whole `<text>` are now read once, in order, and
each isolate initiator is matched with its PDI (UAX #9 BD9). Each
element's first strong character is then found in constant time, so
nested `plaintext` elements do not read the same characters again. An
isolate that opens inside an element and closes after it hides the
rest of that element.

vectorcraft-svg takes `unicode-bidi` 0.3.18, which vectorcraft-text
already uses, for the bidi classes.
2026-11-14 03:50:57 -08:00
dotCooCoo 78dd7dd765 SVG import: direction keywords match in any letter case
SVG import compared `direction` with `rtl` case-sensitively, so
`direction="RTL"` or `style="direction: Rtl"` read as left to right.
CSS keywords are ASCII case-insensitive
(https://www.w3.org/TR/css-values-3/#keywords), and `unicode-bidi` was
already read that way.

The `<tspan>` embedding direction, the `<text>` paragraph direction and
the anchoring of each chunk now read `direction` through one helper
that compares without regard to case.
2026-11-14 03:47:38 -08:00
dotCooCoo 48bbcb115d SVG export: masked groups span their region only for effects that are written
A fill or stroke that the export leaves out (hidden, unpainted or zero
width) no longer adds the hidden rectangle for an effect on it. An
instance of a dynamic symbol is checked in its overridden art, so an
override that paints a fill or stroke with an effect still adds it.
2026-11-14 03:43:24 -08:00
dotCooCoo 14076169fc Merge main into fix/svg-masked-shadow 2026-11-14 03:21:12 -08:00
dotCooCoo 1c973dc141 SVG import: absolutely placed characters in right-to-left text land on their x
An absolutely placed character starts its own text chunk, anchored on its
own x (SVG 1.1 text chunks). In a right-to-left line the import kerned the
character before each placed one as if kerning moved the placed character
right, but a right-to-left glyph takes its kerning at its left, so the
correction moved the wrong glyph. In <text direction="rtl" font-size="20">,
<tspan x="100 160" y="80 80">אב</tspan> on a second line put the right
edge of א at 393.48 instead of 100.

The placement now works in drawing order: each kerning is a cut after its
character (before it when it runs right to left), each gap between two
placed neighbors takes one cut, and the line's left edge is measured again
every round. Corrections are divided by the character's horizontal scale,
so text stretched with lengthAdjust="spacingAndGlyphs" lands on its x too.

A left-to-right character drawn between two placed right-to-left ones has
no kerning slot before the next one, which then keeps its place in the
line; the import warns about it.
2026-11-14 02:41:16 -08:00
dotCooCoo e7feeb311d SVG import: a raster stored again as PNG keeps its resolution
A raster that SVG import decodes and stores again (a TIFF, a BMP or any
other format that is not PNG, JPEG, GIF or WebP, or an image whose EXIF
or TIFF orientation turns or flips it) was written as a PNG with no pHYs
chunk, and a turned or planar CMYK TIFF was written without resolution
tags, so the image lost the resolution its file declares.

The converted PNG now carries the source's resolution in a pHYs chunk,
and the CMYK TIFF in its resolution tags, as Open and Place store them.
A quarter turn (orientations 5 to 8) swaps the horizontal and vertical
values. An image that declares no resolution is stored without one, and
an image stored unchanged keeps its own bytes.

The PNG, JPEG, TIFF, WebP and BMP resolution readers and the pHYs writer
move from the engine to vectorcraft-doc, so the SVG importer can use
them; the engine and renderer entry points forward to them unchanged.
2026-11-14 02:40:45 -08:00
dotCooCoo a9fbb7aa50 Merge main into fix/svg-clip-path-effects 2026-11-14 02:24:38 -08:00
dotCooCoo fb13d2045d Merge main into feat/cjk-tsume 2026-11-14 01:56:33 -08:00
BFlat ddfa381c91 Merge pull request #7273 from dotCooCoo/fix/knockout-groups-flat-masks
PDF/SVG export: knockout groups nest no deeper with more elements
2026-11-14 16:51:33 +07:00
BFlat e419f931ff Merge pull request #7496 from dotCooCoo/fix/svg-tspan-bidi
SVG import: a nested tspan keeps its direction and unicode-bidi
2026-11-14 16:49:38 +07:00
dotCooCoo 3c4170fa25 Merge main into fix/svg-masked-shadow 2026-11-13 23:05:34 -08:00
dotCooCoo ae05540f70 Merge main into feat/cjk-tsume 2026-11-13 22:46:57 -08:00
BFlat 0ca2952093 Merge pull request #7363 from dotCooCoo/fix/stained-instance-export
Symbols: a stained instance keeps its stain in SVG, PDF and the other exports
2026-11-14 13:42:39 +07:00
dotCooCoo a008a0d68e SVG import: a nested tspan keeps its direction and unicode-bidi
A <tspan>, <textPath> or <a> inside a <text> whose unicode-bidi is embed
now gives its characters its direction as their Character Direction. An
element with normal stays in its parent's embedding, unicode-bidi is not
inherited (inherit takes the parent's value), and the <text> keeps its
own paragraph direction. A run that SVG export writes with direction and
unicode-bidi="embed" now opens with its Character Direction again.

Character Direction holds one level of embedding and no override or
isolation, so bidi-override, isolate and isolate-override are imported
as embeddings in their direction, plaintext as an embedding in the
direction of its first strong character, and an embedding inside
another as an embedding of its own, each with an import warning.
unicode-bidi: plaintext on the <text> runs and anchors each paragraph
in the direction of its first strong character.

https://www.w3.org/TR/SVG11/text.html#RelationshipWithBi-directionality
https://www.w3.org/TR/css-writing-modes-3/#unicode-bidi
2026-11-13 22:21:42 -08:00
dotCooCoo bc6c48367e Merge main into fix/knockout-groups-flat-masks 2026-11-13 22:15:33 -08:00
dotCooCoo 12640be278 Merge main into feat/cjk-tsume 2026-11-13 21:50:56 -08:00
dotCooCoo 1494e2c230 Type: Tsume narrows the space around each character's ink
Character panel > Tsume, shown with Show East Asian Options, takes away a
percentage of the blank space on each side of a character's ink, inside
its own width, in horizontal type. The value is from 0 to 100, 0 by
default. 50 halves that space and 100 sets the ink against the
characters before and after it; the glyph keeps its shape. A character
without ink, such as a space, keeps its width, and a ligature or a
letter with its marks takes the value of its first character. The
font's pair kerning, tracking, manual kerning and Insert Aki are added
after it, and justification keeps it. Vertical type keeps its spacing.

- text.setFormat and text.setRangeStyle take `tsume`; a value that is
  not a number or is outside 0 to 100 is refused, naming the key.
  Character styles carry it, and a file holding a value outside the
  range opens with the nearer end.
- The Character panel's Tsume field has 0, 25, 50, 75 and 100 in its
  menu, and Reset Panel sets it to 0. Character Style Options shows it
  in Advanced Character Formats.
- Live SVG text places each character Tsume moves, and type on a path
  with Tsume is written as outlines.

Docs: docs/mcp.md (Tsume, Character Style Options fields),
docs/target-app-parity.md.
2026-11-13 21:50:05 -08:00
dotCooCoo 6bd6755283 Effects: a clipping path's geometry effects shape the clip on the canvas
A clipping path with Roughen, Transform or another effect that reshapes
a path drew its own fill and stroke reshaped, but the canvas and raster
export still clipped the art by the plain path. SVG and PDF export bake
the effect into the path before writing the clip path, so the screen
and a saved SVG showed different art.

effects::clip_outline now applies the live geometry effects in the
clipping path before taking its region: the path's or compound path's
own effects, the effects of its fills and strokes (the region is then
the union of the paths they give, as an export bakes them), the effects
of a compound path's paths, of a group's visible members and of a
nested clip group's clipping path, shown or hidden, and a group's own
effects as an export evaluates them. Raster effects such as Gaussian
Blur leave the region as it is. The renderer, the engine commands that
measure a clip region (flatten, arrowheads, cutting) and the writers use
it; the writers receive the baked document, so their output for a path
with effects does not change.

The renderer measures a clip group by the region its clipping path's
effects give it, so a view or tile that shows only the moved part of
the clip still draws the clipped art, and it drops cached clip regions
that hold a plug-in effect when plug-ins change.

Two rules change in every export as well as on the canvas:

- When the clip group, or a group around it, has a geometry effect of
  its own, the clipping path's effects apply first and its gradients
  move with them. #7114 left the clipping path's effects live under the
  group's effect, so they applied after it.
- Type used as a clipping path keeps its own effects when a group's
  effect outlines it. #2073 and #2290 outlined it without them, to
  match the plain clip the canvas drew.
2026-11-13 21:18:18 -08:00
Dylan M. Taylor 2ce569a6a7 Format svg adapter after craft-libs cutover 2026-11-14 05:14:16 +00:00
dotCooCoo 1fbbb0a4e7 SVG export: shadows in a masked group show in readers that bound it by its shapes
Some SVG readers draw a masked group only inside the bounding box of its
shapes. A drop shadow, glow, blur or feather inside the group paints past
those shapes, so these readers left it out, while others drew it.

A masked group now starts with a rectangle over the mask's region,
unpainted (fill="none") and hidden (visibility="hidden"), when what it
holds has a visible raster effect: on a member at any depth, on a fill or
stroke, in a symbol's art, or in the art a blend or other live object is
written as. The rectangle widens the group's bounding box without drawing
anything, and import leaves it out. The masked groups of a knockout group
get the same rectangle when an element inside them has such an effect.
Other masked art, brushed strokes included, is written as before, and so
is an object whose own effects are written outside its mask.

Tests: a_masked_group_spans_its_region_when_its_art_paints_past_its_shapes
and a_knockout_group_spans_its_masks_when_an_element_paints_past_its_shapes
in crates/svg/tests/it/blend_export.rs.
2026-11-13 20:23:15 -08:00
Dylan M. Taylor e927729f25 SVG: delegate entity expansion bounds, sniffing, and SVGZ to craft-svg 2026-11-14 01:40:55 +00:00
dotCooCoo de8ac51502 Symbols: a stained instance keeps its stain in SVG, PDF and the other exports
A symbol instance colored with the Symbol Stainer lost its stain in
export whenever the canvas's stained art was not used:

- An instance with geometry effects was reshaped for export from the
  symbol's art with only its dynamic changes, so SVG, PDF, EPS, .ai,
  DXF, EMF/WMF and HPGL wrote the unstained colors. A color adjustment
  on an instance recolored the unstained art the same way.
- PDF, EPS, legacy .ai, DXF and EMF/WMF wrote an instance without
  effects from the unstained art.

tint_node, stain and instance_art now live in
vectorcraft_doc::symbol_overrides, which both the effects and the
exporters can use; vectorcraft_brush re-exports them. Baking, color
adjustments and the writers above now use instance_art: dynamic changes
first, then the stain.
2026-11-13 17:24:06 -08:00
dotCooCoo 2ee29aefc6 Merge main into fix/knockout-groups-flat-masks 2026-11-13 16:59:50 -08:00
BFlat 00d2f6d24b Merge pull request #7283 from dotCooCoo/fix/nested-svg-image-count
SVG import: broken nested SVG images do not use up the image allowance
2026-11-14 05:39:43 +07:00
BFlat 4558196e16 Merge pull request #7053 from dotCooCoo/feat/warichu
Type: Warichu stacks selected characters on lines inside their line
2026-11-14 04:32:40 +07:00
dotCooCoo f27324ace5 Merge main into fix/knockout-groups-flat-masks 2026-11-13 13:01:18 -08:00
dotCooCoo 9d9e0c2f8e SVG import: broken nested SVG images do not use up the image allowance
An SVG shown as an image that could not be read (text that isn't XML,
an SVG without a size) still took one of the 256 nested SVG images an
SVG may show, so an image after 256 broken ones was left out. Only
images that decode now count toward that limit; a failed image gives
its place back.

Every nested SVG image read, decoded or not, counts toward new limits
of 1,024 reads and 512 MB of text per import, so a broken image shown
many times through <use> is still read a limited number of times. Past
them, nested SVG images are left out with a warning.

A nested SVG image with text that usvg then refuses no longer warns
that its text isn't imported.
2026-11-13 12:38:00 -08:00
BFlat 9107dad274 Merge pull request #7194 from dotCooCoo/fix/item-3829
Envelopes: a partly transparent image keeps its alpha where its warp pieces overlap
2026-11-14 03:23:28 +07:00
dotCooCoo 88b587baa6 PDF/SVG export: knockout groups nest no deeper with more elements
A knockout group was written as a chain of soft masks, one inside the
next for each element, so a group of about 200 elements made a PDF that
VectorCraft's own PDF import refused (form nesting past 50) and an SVG
that the web build refused as too deep.

- PDF writes a knockout group as one transparency group with /K true,
  with /I true when the group isolates, and PDF import reads it back as
  a knockout group. An element with several paints is drawn as one
  group inside it, so its own fills and strokes do not knock each other
  out.
- Where an element knocks out less than the area it paints (its opacity
  or mask defines its knockout shape, or it paints translucently), each
  element is drawn through one soft mask of the overlapping elements
  above it. Past 65,536 objects in a group's masks, or 2,048 elements,
  the group is a PDF knockout group, with a warning.
- SVG draws each element through one mask of the overlapping elements
  above it, built from one shared copy of each element's shape, so the
  elements stay siblings. Past 65,536 shapes in a group's masks, or
  2,048 elements, the group is written as a plain group, with a warning.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-13 12:21:39 -08:00