Commit Graph
927 Commits
Author SHA1 Message Date
BFlat 9800d48e58 Merge pull request #7571 from dotCooCoo/fix/cmyk-tiff-alpha-16-bit
TIFF: a 16-bit CMYK or gray TIFF with alpha converts at 16 bits
2026-11-14 20:37:26 +07:00
dotCooCoo fe82f68d6f TIFF: a 16-bit CMYK or gray TIFF with alpha converts at 16 bits
Place, Open and relink read a CMYK or grayscale TIFF with an alpha
channel through extra_samples_tiff_rgba, which cut every 16-bit sample
to its high byte before converting it. A faint ink or a low alpha was
lost: cyan 129 at full alpha showed as white, and an alpha of 129
became fully transparent.

The samples are now normalized at the depth the file stores them, and
only the resulting color and alpha are rounded to 8 bits (a 16-bit
alpha to the nearest 8-bit value). Associated alpha is divided out at
16 bits. A pixel whose associated alpha is zero now has no color,
instead of keeping its stored color. TIFF 6.0's Associated Alpha
Handling section states this arithmetic and the zero-alpha rule for
RGB data; applying them to CMYK and gray samples is VectorCraft's own
rule.
2026-11-14 03:44:41 -08:00
dotCooCoo fb13d2045d Merge main into feat/cjk-tsume 2026-11-14 01:56:33 -08:00
BFlat 3c7d72f8bc Merge pull request #7333 from dotCooCoo/fix/graph-series-gradient-per-mark
Graphs: a kept gradient takes the same place on every mark of its series
2026-11-14 16:54:26 +07: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
BFlat be93e4b3f2 Merge pull request #7377 from dotCooCoo/fix/symbol-expand-reset-bbox
Symbols: Break Link and Expand keep an instance's bounding box
2026-11-14 13:42:23 +07:00
dotCooCoo 2a4185d6ca Merge main into fix/graph-series-gradient-per-mark 2026-11-13 22:15:43 -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 20fc6a0225 Symbols: Break Link and Expand keep an instance's bounding box
After Reset Bounding Box on a turned symbol instance, Break Link to
Symbol and Object > Expand gave the art a box turned with the instance's
transform again. The art's boxes now turn with the instance's own box,
so they stand square when the instance's box did, and the angles the
art has inside the symbol are kept relative to it. The group that an
instance's opacity, blending mode or opacity mask adds around the art
takes the instance's box angle instead of standing square to the page.

A reflected instance can hold a box 180 degrees from the box its
transform gives in one step; the art now takes the instance's box
there too, the same rectangle with its handles where the instance had
them.

Compound shapes and Pathfinder receive an instance's art through the
same rule.

The clip groups and clip paths that 9-slice guides make for gradient
and pattern art take the angle of the cell that places them, and the
outer group keeps the object's angle turned by the instance, instead
of standing square to the page. This also changes the result for an
instance that was never reset: a 20 degree sliced instance gave clip
groups at 0 degrees and now gives them at 20.

Docs: params text, docs/mcp.md, docs/target-app-parity.md.
2026-11-13 18:26:00 -08:00
BFlat 3f7bbd4494 Merge pull request #7329 from dotCooCoo/fix/ruler-guides-nodes-one-pass
Guides: stripping and mapping ruler guides look at each child of a layer once
2026-11-14 08:39:32 +07: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 78e7227358 Merge main into fix/graph-series-gradient-per-mark 2026-11-13 17:01:05 -08:00
BFlat d8ae18c02f Merge pull request #7318 from dotCooCoo/fix/guides-layers-one-pass
Guides: removing and editing artboard guides visits each layer once
2026-11-14 07:33:26 +07:00
dotCooCoo d5509c3b6c Guides: stripping and mapping ruler guides look at each child of a layer once
Node::strip_ruler_guides and Node::map_ruler_guide_positions asked a
layer whether its whole subtree held a ruler guide, then asked each
sublayer the same question again when filtering the children and again
when the recursive call started. A layer nested d deep was tested
2d + 1 times, so a chain of N layers cost about N^2 layer tests.

strip_ruler_guides runs when a document's objects are placed or pasted
(Clipboard::from_document), when a copy of the document is saved
(Document::guides_to_saved) and when a temporary editing layer is
removed (Document::remove_edit_layer). map_ruler_guide_positions runs
when an imported EPS is scaled.

A new held_guides reads the layer once and records which children are
ruler guides and which sublayers hold one; strip_held and map_held then
visit and copy only those sublayers. The guides removed, the order f is
called in, the art and its order, and the sharing of layers without a
guide are the same as before. Neither method records an undo step.
2026-11-13 15:58:43 -08:00
dotCooCoo 252ff7782a Merge main into fix/graph-series-gradient-per-mark
# Conflicts:
#	docs/mcp.md
2026-11-13 15:49:27 -08:00
BFlat b6fb2212c4 Merge pull request #7308 from dotCooCoo/fix/canvas-range-guides-one-pass
Canvas range: removed guides are looked up through sets, and nested layers are walked once
2026-11-14 06:27:05 +07:00
dotCooCoo 419698529c Merge main into fix/graph-series-gradient-per-mark
# Conflicts:
#	crates/engine/src/cmd/gradient.rs
#	crates/engine/src/cmd/graph.rs
#	docs/target-app-parity.md
2026-11-13 14:54:23 -08:00
dotCooCoo e00e5c9cd9 Guides: removing and editing artboard guides visits each layer once
Deleting ruler guides and moving or scaling an artboard's guides asked
each layer whether its whole subtree held a matching guide, then asked
every sublayer the same question again on the way down. A layer nested
N deep was scanned N times, so a chain of N layers cost N(N+1)/2 layer
scans.

One read walk now records which children match and which sublayers hold
a match, and the edit visits and copies only those layers. The guides
removed, the count returned, the selection, the order the guides are
changed in and the sharing of untouched layers are the same as before.

The same walk replaces the repeated subtree scans in the other guide
edits in this file:

- tidying the ruler guides of an opened file (guides below art, in
  objects, in opacity-mask art, in symbols and patterns)
- saving the guides as a list
- removing an editing layer while keeping its guides
2026-11-13 14:49:19 -08:00
BFlat 2fe70b2483 Merge pull request #7258 from dotCooCoo/fix/graph-label-appearance-kept
Graphs: Appearance edits on a label are kept for every label of its kind
2026-11-14 05:42:08 +07:00
dotCooCoo ac5e9c6618 Graphs: a kept gradient takes the same place on every mark of its series
A graph series kept a gradient with the document coordinates it had on
the one mark it was stored from, and gave that paint unchanged to every
mark when the graph was drawn again. The other marks showed the gradient
placed over the edited mark, and the placement stayed behind when the
data or the graph moved the marks.

The graph now stores a gradient relative to the unit box of the mark it
comes from (its geometric bounds, or its stroke's box for a stroke).
Each path of the series, the legend swatch and a Filled Lines band
included, takes it on its own box. Every store route does this:
paint.setFill and paint.setStroke, the Eyedropper, graphic styles,
Appearance commands, paint.setGradientGeom and the freeform point
edits. A command that writes a mark's paint as the graph drew it leaves
the stored gradient as it is and records no undo step.

The series paints save this as fillUnitBox and strokeUnitBox. A
gradient in a file saved without these flags is drawn where it was
stored. So is a gradient stored from a path with no width or height.

Docs: docs/mcp.md, the paint.setGradientGeom params text and the Graphs
line in docs/target-app-parity.md.
2026-11-13 14:41:12 -08:00
dotCooCoo 9fd0ede79f Canvas range: removed guides are looked up through sets, and nested layers are walked once
When a read document is brought within the canvas, each saved guide was
checked against the removed artboard ids and the removed layer ids with
linear scans, and each guide node against the removed artboard ids, so
the work grew with the number of guides times the number of removed ids.
The ids are now collected into ordered sets, and each guide is checked
in logarithmic time.

Removing what lies beyond the canvas scanned each container's whole
subtree to see whether anything reached beyond, then scanned each child
again while removing, one level down at a time, so a deeply nested
object was measured once for every group around it. Removing guide nodes
probed each layer's subtree the same way before descending into its
sublayers. Both now make one read-only walk that records, by index,
which objects reach beyond (or which guides are removed), and the
removal follows that record. The objects clipped by a clipping path
beyond the canvas are still not measured.

Containers with nothing to remove are still not copied. The objects and
guides that remain, their order, the counts, the retired artboard ids
and the saved selections are unchanged.
2026-11-13 14:15:25 -08: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
BFlat 200b65feea Merge pull request #7247 from dotCooCoo/fix/blend-spine-measured-once
Blend: spreading keys along a replaced spine measures it once
2026-11-14 04:31:35 +07:00
dotCooCoo 5df5acd7f9 Merge main into fix/graph-label-appearance-kept 2026-11-13 13:31:34 -08:00
BFlat f05cb5e7a4 Merge pull request #7223 from dotCooCoo/fix/inline-graphics-in-symbol-art
Type: inline graphics in symbol, mask and pattern art follow their symbols
2026-11-14 03:24:03 +07:00
BFlat a3ab152a0a Merge pull request #7246 from dotCooCoo/fix/objects-on-path-flatten-once
Objects on Path: each path is flattened once per placement or transform
2026-11-14 03:23:37 +07: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 fdedfb0e59 Merge main into feat/warichu 2026-11-13 11:59:22 -08:00
dotCooCoo 8091e4861e Blend: a spine whose length overflows gives two keys no anchors
Spreading two keys along a spine whose length overflows splits nothing, so
it returned the end anchors, and a closed spine was then opened at its start.
spread_anchors now rejects a length that is not finite before it makes any
target, so two keys get no anchors and the path stays as it was, as for
three or more keys.
2026-11-13 11:47:15 -08:00
BFlat a105e14aef Merge pull request #7220 from dotCooCoo/feat/image-indexed-color
Edit Colors: convert embedded images to indexed color
2026-11-14 02:40:36 +07:00
BFlat 1e385ba06e Merge pull request #7210 from dotCooCoo/fix/envelope-empty-type-box
Envelope: type that draws nothing adds no box next to other content
2026-11-14 02:38:27 +07:00
dotCooCoo 5c0f22b345 Graphs: Appearance edits on a label are kept for every label of its kind
A fill or stroke row that the Appearance commands gave a graph label, its
object opacity, and the characters the Eyedropper gave it lasted only
until the graph was drawn again: the graph stored nothing for a label's
rows, and the Eyedropper's character attributes were not stored.

- The graph keeps, for value axis numbers, category labels and legend
  labels, the paint, weight, opacity and blend mode of a label's topmost
  visible fill and stroke row, their order and place relative to the
  Characters row, and the label's object opacity and blend mode
  (labelStyles valuesRows, categoriesRows, legendRows). Every label of
  the kind gets those rows when the graph is drawn again, with a placed
  gradient fitted to each label.
- Every appearance.* command stores them, and so do graphicStyle.apply
  and redefine and the paint, stroke, gradient and transparency edits
  aimed at a row, even when the label already showed them.
  transparency.set without a row and object.setProps store only the
  object opacity and blend mode. With several labels of a kind, the last
  one edited wins.
- appearance.copyFrom onto all of a label's text stores the character
  attributes it applied and, from type, the paragraph attributes; a
  transparency pick stores the object opacity and blend mode, and
  append stores the rows.
- paint.setFill and paint.setStroke store a character fill or stroke
  only when the characters take the paint (the whole label, or all of
  its text highlighted with the Type tool); aimed at a row, they store
  the row.
- Document Color Mode and swatch edits reach the kept rows.

Docs: docs/mcp.md, the commands' params text, docs/target-app-parity.md.
2026-11-13 11:35:27 -08:00
dotCooCoo 6326b17b0e Blend: spreading keys along a replaced spine measures it once
Giving the keys of a blend whose spine came from Replace Spine anchors of
their own flattened and measured the whole spine again for every key,
then walked its segments one by one. 1,000 keys on a spine of 4,096
segments flattened it 999 times.

The spine is now flattened once into one buffer. A split flattens only
its two halves and measures the segments after it again, in order, only
as far as the next key, so the arc lengths are summed as before and the
keys get the same anchors. A binary search finds each key's segment.

A spine so large that its length overflows no longer gets an anchor
split at a parameter that is not a number: spreading keys along it
returns nothing and leaves it as it was.
2026-11-13 11:21:31 -08:00
dotCooCoo 444f50a4c4 Objects on Path: each path is flattened once per placement or transform
stops and stop flattened the path twice, once for the points and once
for its length, and stop did both before rejecting an index past the
last object. place validated the path with attachable_path first, and
transform measured the old and new paths up to three times each.

A private Measured value now holds the flattened first subpath with its
length and closed flag, and path_length, stops, stop and transform use
it. stop returns None for an out-of-range index without measuring, and
a move of the group measures no path.

attachable_path now uses path_length, so a path whose length overflows
to infinity is no longer offered as one objects can attach to when
placement cannot measure it.
2026-11-13 10:50:24 -08:00
dotCooCoo 4d4d7ee1a7 Envelopes: opaque pixels of a warped image stay opaque where its pieces overlap
In SVG and PDF output, each piece of a warped image whose pieces can show
through was drawn through a mask of every later piece that reaches it. On a
later piece's antialiased edge the viewer multiplies the earlier piece's
coverage by the mask and draws the later piece over it, so an image with
both opaque and partly transparent pixels came out lighter at its opaque
pixels along the triangles' edges (alpha 184 of 255 in the new SVG test).

A later piece now goes into an earlier piece's mask only when a pixel of
either piece's image within 2 pixels of their overlap is less than opaque
(vectorcraft_effects::WarpSeams). Where two pieces overlap over opaque
pixels, the later one is drawn over the earlier one without a mask.
2026-11-13 10:36:16 -08:00
dotCooCoo 19be0bb880 Type: inline graphics in symbol, mask and pattern art follow their symbols
Type with an inline graphic was resolved only in the layers, so type in
the art of another symbol, a graph design, a pattern tile or an opacity
mask kept the size its graphic had when it was made. The symbol holding
that type, its instances and the inline graphics that show it were
measured from the stale layout.

- Inline graphics are resolved in every tree that holds type. Symbols
  are resolved in dependency order, so a symbol's type is laid out
  before the type that shows that symbol inline. Edits, opening,
  reverting and Refresh Font List use the same pass.
- The pass visits each object once: it resolves the children first and
  copies a node only when something below it changed. It measures each
  symbol's art once per pass.
- An inline graphic of a symbol whose art shows that symbol inline,
  directly or through other symbols, is an empty slot. The symbols on
  such a cycle are found once and cached until the symbols change.
- A changed inline graphic also clears the text's cached baselines.
2026-11-13 10:17:33 -08:00
BFlat e41ef1ea62 Merge pull request #7148 from dotCooCoo/fix/blend-without-spine-widget
Blend: a blend without a spine shows the Appearance widget
2026-11-14 00:51:09 +07:00
BFlat d58452f287 Merge pull request #7167 from dotCooCoo/fix/image-icc-16bit-kept
Images: an image stored again keeps its ICC profile and 16-bit samples
2026-11-14 00:49:03 +07:00
dotCooCoo 81d19f1718 Edit Colors: convert embedded images to indexed color
Edit > Edit Colors > Convert to Indexed Color... (edit.colors.toIndexed)
stores the embedded images in the selection as indexed-color PNGs: a
palette of at most 256 RGB colors, each pixel the index of one. The
dialog previews live and OK is one undo step.

- The palette is chosen as for a PNG-8 export (selective, perceptual,
  adaptive, web, gray, black and white), as evenly spaced levels of red,
  green and blue (uniform), or from the document's swatches. Dithering
  is diffusion, pattern, noise or none, with an amount.
- Pixels under half opacity become one transparent entry; the others
  become opaque in their own color. Linked images, colorized images and
  color glyph images keep their pixels.
- An image that is already an indexed PNG and would show the same pixels
  is left as it is, and no undo step is recorded when no image changes.
- An image of more than 134,217,728 pixels (33,554,432 in the web app)
  is refused, read from its header before decoding.
- With Select All, locked and hidden objects that hold an image that is
  not an indexed PNG yet are counted in notChanged, without converting
  them.
- The quantizer gains quantize_to, which maps pixels to a given palette.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-13 09:49:00 -08:00
dotCooCoo b53cd7b52e Envelope: type that draws nothing adds no box next to other content
An envelope around type of only spaces and a rectangle mapped from the
union of the rectangle and the type's laid-out box, so the empty type
moved and scaled how the rectangle was distorted. Type that draws
nothing now adds no box while other content in the envelope has one,
in groups and layers and as a clipping path too. When nothing else in
the envelope has a box, the type keeps its laid-out box, so an envelope
of only empty type keeps its frame, Make with Mesh and Reset with Mesh
still work, and Release still gives back the warp shape.

This applies to Make with Warp, Make with Mesh, Make with Top Object,
Reset with Mesh, Edit Contents and the stroke margins. Type that has
not been measured yet keeps its laid-out box as before.

The PDF/.ai import's boxes for envelopes with Distort Appearance
measure each object as the whole contents measure it, so the margins
the import tries match the contents' geometric box. The three import
tests that measure type without registering the glyph measure now
register it first, so another test registering it while they run no
longer changes their result.
2026-11-13 09:37:03 -08:00
BFlat 6a1b4c42de Merge pull request #7040 from dotCooCoo/fix/nested-link-ids
Links: list and manage linked art in symbols, graph designs, patterns and masks by id
2026-11-13 23:58:20 +07:00
dotCooCoo 0663c76be1 Envelopes: a partly transparent image keeps its alpha where its warp pieces overlap
An envelope or the Warp effect cuts an image into triangle pieces that
reach a little past their edges, so neighbors overlap and no seams show.
On an image with alpha, the overlap drew that alpha twice: a 50% image
came out about 75% opaque along every cell edge and diagonal.

- The pieces now form a knockout group, and each piece is marked as a
  warp piece (`warp_piece`) whose knockout shape is its whole triangle,
  so where pieces overlap only the top one draws.
- The canvas knocks each piece out by its triangle at full coverage.
- SVG and PDF draw the pieces of a partly transparent image (its pixels,
  opacity, color mask or colorize paint) through one mask each, of the
  later triangles that reach it; pixels on a triangle's antialiased edge
  come out slightly lighter there. Pieces of an opaque image are drawn
  one over another as before.
- SVG writes one copy of the image for pieces cut by opacity masks
  (Preserve Shape Using Transparency) too.
- A cell count past the generated-object budget makes an empty group
  instead of overflowing.

Docs: docs/target-app-parity.md.
2026-11-13 08:38:48 -08:00
dotCooCoo 9b027bb43a Merge main into fix/image-icc-16bit-kept 2026-11-13 08:25:53 -08:00
BFlat 9395fb9cec Merge pull request #7160 from dotCooCoo/fix/svg-image-orientation
SVG import: raster images follow their EXIF or TIFF orientation
2026-11-13 23:16:49 +07:00
BFlat 43d0b50760 Merge pull request #7157 from dotCooCoo/fix/remove-anchors-one-pass
Paths: Remove Anchor Points removes many anchors of a subpath in one pass
2026-11-13 23:15:10 +07:00
dotCooCoo e13384ebb4 Merge main into fix/blend-without-spine-widget 2026-11-13 08:06:40 -08:00
BFlat e0e97e8ed1 Merge pull request #7153 from dotCooCoo/fix/blend-spine-binary-search
Blend: find the spine segment for an arc length with a binary search
2026-11-13 23:04:50 +07:00
BFlat b17a62be34 Merge pull request #7109 from dotCooCoo/fix/painted-fx-brush-reach
Effects: culling a painted piece leaves room for its own brush art
2026-11-13 23:01:55 +07:00
dotCooCoo d7740ff587 Images: an image stored again keeps its ICC profile and 16-bit samples
Open and Place store some images again instead of keeping their bytes:
a photo turned by its orientation, a TIFF the image decoder reads, and
a CMYK TIFF with one plane per ink. Those images lost the ICC profile
their file embeds, and 16-bit samples were cut to 8 bits.

- raster_image keeps gray and RGB samples at 8 or 16 bits in the
  stored PNG, with the gray or RGB profile in its iCCP chunk. A
  profile of another color space, such as the CMYK profile of a CMYK
  image read as RGB, is left out. A TIFF's profile is read with the
  tiff decoder, which reads it whatever the image's size.
- stored_cmyk_tiff and turned_cmyk write CMYK8 or CMYK16 as the file
  has it, with the CMYK profile as TIFF tag 34675 (UNDEFINED). A CMYK
  JPEG's profile is put together from all its APP2 segments.
- The docs/target-app-parity.md line on EXIF orientation says 8 or 16
  bits, and a line after it describes the kept profile.
2026-11-13 07:19:15 -08:00