Commit Graph
73 Commits
Author SHA1 Message Date
Dylan M. TaylorandClaude Sonnet 5.5 a7910edd64 Review fixes: keep later CAD layouts, ordered-stop gradient sampling, skip full-stroke previews, cache shift line
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 17:19:59 +07:00
Dylan M. TaylorandClaude Opus 5.5 20bba9ebae Security: linear work for shortcut review, gradients, recovery names, include cycles, batch references, DOCX style ids, CAD layouts and long strokes
CodeRabbit findings #2, #16, #31, #103, #114, #124, #162, #179, #182, #183, #185:

- Shortcut import review: example names for each code in one pass over the rows.
- Gradients: a midpoint stop is mixed from its two stops; freeform recolor samples its
  increasing positions with one advancing search (`Gradient::sample_increasing_with`).
- Recovery: image entry names are looked up by address, and dead ones swept when the map
  doubles, not on every image.
- Script checker: include cycles from strongly connected components (iterative Tarjan), not
  a reachability row per file.
- Batch step references copy at most 256 MB of values for one step.
- DOCX: style ids over 256 bytes are ignored where defined and where used.
- DXF: layouts keep at most 4 MB of names; a hatch's loops count against the node limit.
- Freehand and Liquify: past 1000 samples a drag previews every n/1000 samples, strokes keep
  at most 100,000 samples, and the stroke kept is previewed in full before it is committed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 17:19:59 +07:00
bflatastic 8a73ed9963 Security bounds: fix on landing #3936 (see the PR comment) 2026-10-24 17:19:58 +07:00
Dylan M. TaylorandClaude Opus 5.5 e2a870e602 Security: bounded flattening, plug-in budgets and caches, loaded textures, Revolve paint copies, color book names and ICC names
CodeRabbit findings #4, #34, #46, #51, #77, #101, #170, #171, #188:

- `vectorcraft_geom::flatten`: kurbo's flatten with each curve's tolerance raised so it makes
  at most ~4096 segments (non-finite curves become a line); the canvas outline and
  variable-width strokes use it.
- Plug-ins: a heavy filter's budget is the host's `Limits::heavy_fuel`/`heavy_wall_time_ms`,
  never more; a start function runs on one fuel slice; the effect cache holds at most ~1M
  anchors of inputs and results, and doesn't keep a result past a quarter of that.
- Loaded textures are cached by their PNG (base64 spacing doesn't matter), and live textures
  hold at most 512 MB together.
- Revolve draws the source path when the faces' paint copies would pass 256 MB.
- Color books: names with prefix and suffix take at most 64 MB.
- ICC: localized-name (`mluc`) records over 1 MB together are refused before moxcms copies them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 17:19:54 +07:00
Dylan M. Taylor e3223a419d Audit: Scribble rows counted, Outer Glow dilation in linear time, CSS export names in a set 2026-10-24 04:49:07 +07:00
Jürg Lehni 7efd8f941a Type: colour glyphs keep each fill's own colour, also between colours that look alike
The colour glyph cache keyed the text's colours by their 8-bit RGB look, so a fill that only looked like an earlier one (a grey and the same RGB grey, a spot colour and its process match) took that one's colour, and its separations. It now keys them exactly.
2026-10-24 03:52:15 +07:00
Taha-Mahmoodi 6fd1f566e1 Gradients: the Perceptual test describes the method in our own words
Review follow-up (CodeRabbit): the test's doc comment quoted a phrase from
the public help page. AGENTS.md allows feature names and a cited URL, not
copied wording; the comment now describes the behaviour itself and keeps
the URL.
2026-10-23 01:53:30 +07:00
Taha-Mahmoodi 2bad646821 Gradients: a Perceptual gradient mixes its colours in OKLab, and SVG and PDF write it as an image
Illustrator 30.0's gradient Method, from the public documentation
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-perceptual-interpolation-in-gradients.html):
Classic or Perceptual; perceptual interpolation matches colour transitions to
how the eye perceives differences between colours, and perceptual gradients
are rasterised when saved as PDF or SVG.

- Gradient gains `method` (Classic | Perceptual; serde default Classic,
  written only when Perceptual, so older files open as Classic and Classic
  files are written as before). The colour crate's OKLab helpers open up for it.
- Perceptual mixes neighbouring stop colours in OKLab (sample, and
  sample_keep_model for the stroke meshes, Expand and blends); opacity stays
  linear. Renderers that mix linearly get stops on the perceptual curve,
  placed by halving each half segment where a straight line strays more than
  0.4 of a colour level (a nearly straight segment needs none). Classic
  gradients expand exactly as before.
- draws_as_pixels includes Perceptual gradients, so SVG and PDF write a
  Perceptual fill or stroke as an image at the document's raster effects
  resolution, clipped to what it paints. A gradient along or across a stroke
  stays slices (PDF no longer draws it as a within-stroke image).
- With Dither: a dithered Perceptual fill is the perceptual mix with dither's
  noise (the canvas's dithered grids are keyed by method too); SVG and PDF
  write it as an image with the noise, and a dithered Perceptual stroke as an
  image without it, as the canvas draws it (GradientPaint::pixels, SVG's
  raster_paint and PDF's area are told whether they paint a fill).
2026-10-23 01:53:29 +07:00
NetterSilverandClaude Haiku 5.5 4a29fd4112 Wrap to_hsb hue of 360 back to 0 for near-red colours
Found while testing crates/color/src/lib.rs:142

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 01:06:55 +07:00
Taha-Mahmoodi 7c9edfaf7f Gradients: an outlined stroke leaves dither behind, and paint.setStroke refuses it
Review follow-up (CodeRabbit): paint.setStroke kept `dither: true` from its
gradient, and a stroke can hold dither anyway (swapped from a fill). Outline
Stroke and appearance expansion copied the stroke's paint into the outline's
fill, which then drew dithered although the stroke never did. The outline's
fill now goes without dither (Paint::stroke_as_fill), and paint.setStroke
refuses a gradient with `dither: true`, as paint.editGradient does.
2026-10-23 00:08:53 +07:00
Taha-Mahmoodi 4062e74ff3 Gradients: a dithered fill samples only the part of its grid the view shows
Review follow-up (CodeRabbit): a large dithered gradient mostly off-screen
sampled its whole grid (up to 4096 x 4096 cells, 64 MiB) to draw a small
visible part, and did it again on each zoom step. The renderer now samples
only the cells the render target can show, a cell more each way for the
bilinear mix, snapped out to 256-cell tiles so a pan within them reuses the
grid. Cells keep their place in the whole grid, so the gradient and its
noise are the same pixels as before (GradientPaint::pixels_within).
2026-10-23 00:08:52 +07:00
Taha-Mahmoodi cc1a660a6d Gradients: Dither in the Gradient panel draws a gradient fill with fine noise that hides banding
Illustrator 30.0's gradient Dither, from the public documentation
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-dither-in-gradients.html):
a Dither checkbox in the Gradient panel; dither converts the gradient into
pixels with subtle noise, works with gradient fills only, and follows the
Document Raster Effects resolution.

- Gradient gains `dither` (serde default false, written only when on, so
  older files open unchanged and new files keep it).
- The renderer draws a dithered linear or radial fill as an image paint: the
  gradient sampled a cell per device pixel or finer (quarter-octave steps,
  cached per thread), each channel given seeded triangular noise of up to a
  level before rounding. Same pixels every run; strokes and undithered fills
  draw exactly as before. PNG and other raster exports go through it.
- SVG and PDF write a dithered fill as an image at the document's raster
  effects resolution, clipped to the shape (the freeform-gradient path);
  characters' fills stay vector gradients in SVG, with a warning.
- paint.editGradient and paint.setFill take `dither`; strokes refuse it.
  The Gradient panel's Dither box is dimmed behind the Stroke proxy.
- i18n rows in all 24 catalogs; docs: G13, target-app-parity, ui-parity,
  mcp.md, plugins.md.
2026-10-22 22:38:42 +07:00
Taha-Mahmoodi a1d8952bc8 Gradients: a linear or radial gradient can be drawn as pixels, and SVG and PDF write such a gradient as an image
Shared groundwork for gradient options SVG and PDF can't express (Illustrator
30's dither and perceptual gradients): GradientPaint::pixels samples a placed
linear or radial gradient on a grid (Ramp: its stops, midpoints as stops, mixed
as the renderer mixes them), and draws_as_pixels says which gradients are drawn
that way (freeform ones, so far). The SVG and PDF writers' freeform-gradient
image path takes any such gradient, a linear or radial one at the document's
raster effects resolution. No change in output yet.
2026-10-22 22:38:41 +07:00
Taha-Mahmoodi 4bb6f32911 Gradients: built with Gradient::new, so a new option touches one place 2026-10-22 22:38:41 +07:00
bflatastic 62776d5f46 Recolor: recolor.apply takes a model option for the new colour's model (#2170)
Squashed from #2520 onto main.
2026-10-22 02:01:57 +07:00
KnottyDyesandClaude Opus 5.5 bc3351eacc ICC: all built-in profiles are checked for aligned sizes and tags
The #1584 regression covered the four built-in RGB spaces. It now also
checks gray and both CMYK profiles: header size equal to the length, a
length that is a multiple of 4, tag offsets on 4-byte boundaries, tag
data within the profile, and that the profile still parses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 23:14:28 +07:00
Mex 97d133d9d4 M3.57: preserve fractional CSS gradient stop positions 2026-10-21 15:20:15 +07:00
NetterSilverandClaude Haiku 5.5 acee6cccd8 Keep fallback library name for blank GPL Name line
Found while testing crates/color/src/palette_io.rs:373

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 15:20:13 +07:00
Higgs32584andClaude Opus 5.5 c5798f446e CMYK profiles carry every tag an ICC output profile requires (#2707)
The built-in CMYK profiles embedded in CMYK exports had only the
perceptual and colorimetric tables. ICC.1 requires an output profile to
also have the saturation tables (A2B2, B2A2) and the gamut tag (gamt).
Windows colour management rejects the profile without them ("Bitmap color
context is not valid"), so Microsoft Photos couldn't show a CMYK TIFF
that embedded it, while the RGB export opened fine.

Write the saturation tables (same as colorimetric) and a 9x9x9 gamut
table (in gamut where a Lab colour separates and returns within dE 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:48:56 +07:00
dotCooCoo 24d4777d1a Color: freeform gradients keep CMYK inks when expanded, meshed or edited
Object > Expand and Object > Create Gradient Mesh mixed a freeform
gradient's points in display RGB and turned the result back into the
points' model with the profile-free formula. CMYK inks changed: points
of C50 M20 Y0 K10 gave mesh points of C45 M24 Y0 K22. RGB points were
stored as display sRGB, so with a working RGB space other than sRGB
the mesh showed other colors. Points colored along CMYK stops, a point
paint.freeform.splitLine added between CMYK points and a point
paint.freeform.addPoint added without a color came out as RGB.

Points that share a color model now mix in it: the C, M, Y and K
values mix as inks, and Gray, RGB and Lab components mix as they are.
Points in different models still mix in display RGB. Object > Expand
and Object > Create Gradient Mesh store the result in the working RGB
space, and in a CMYK document they store it as CMYK through the color
settings, as paint.setFill stores an RGB color. Freeform::recolor and
split_line mix with the new Color::lerp_keep_model, which
vectorcraft_doc::blend::lerp_color now calls.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-18 12:36:25 +07:00
Marco Arnulfo 2f24495ffe Color effects recolour the points of a freeform gradient (fixes #2390) 2026-10-17 14:46:21 +07:00
vangel76andClaude Opus 5.5 a8e45f0e2d Persistence: saved PDF presets and loaded ICC profiles survive a restart (#2168, #2174)
PDF presets are kept one file each in a PDF Presets folder, ICC profiles as copies in a Color Profiles folder, next to the preferences; the desktop app and vectorcraft-cli use them. Adds color.removeProfile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:49:48 +07:00
vangel76andClaude Opus 5.5 8ac4c9d268 Color: Generic CMYK compensates black both ways so black round-trips (#2493)
sRGB to Generic CMYK applies black-point compensation (black separates to the
darkest rich black) but CMYK to display did not undo it, so hex 000000 became
C67 M67 Y67 K100 and then showed as #1b1818. The display XYZ now maps the press
black to black when the colour settings use black-point compensation, in the
sRGB conversion, the ink lookup table the canvas uses, Lab of a CMYK colour and
the gamut warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:06:51 +07:00
NetterSilverandClaude Haiku 5.5 3688ad7a46 Reject '+' and other non-hex characters in Color::from_hex
Found while testing crates/color/src/lib.rs:184

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 12:50:37 +07:00
Fabian GrajalesandClaude Opus 5.5 5252c3d86a Recolor Artwork: library colours apply as their swatches; import a library
With Limit to Library, a row's new colour is the library swatch itself: art
links to a global or spot colour (a Pantone-style spot colour stays a spot
colour in its own model, tints keep their tint) and a process swatch gives
its colour in its own model, instead of being converted to the model of the
colour it replaces. The swatches used join the document's swatches (one the
document already has, same name and colour, is reused); recolor.apply
reports them as `swatches`. A folder button next to Limit to Library loads a
library file (.ase, .acb, .gpl, .vcswatches or a document's swatches) and
limits to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 04:08:04 +07:00
dotCooCoo 35f43d631f View: Proof Setup simulates tritanopia
View › Proof Setup had two color blindness items, protanopia and
deuteranopia. A third item, Color blindness – Tritanopia-type, proofs
the canvas with a tritanopia simulation, and the Separations Preview
panel's Proof Setup list offers it too. `view.proofSetup` takes
`target: "tritanopia"`.

The simulation follows Brettel, Viénot and Mollon (1997): a color
keeps its L and M cone responses, and its S response is replaced so
that it lies on one of two half-planes. Both contain the neutral
axis; one passes through the 485 nm color and the other through the
660 nm color. The neutral axis runs to the display's white, so grays
keep their value. The matrices are computed at
compile time from the published inputs cited in the code. The
protanopia and deuteranopia matrices are unchanged.
2026-10-16 02:26:04 +07:00
dotCooCoo c44ece3113 Render: the art prepared for Separations Preview and Overprint Preview is kept between frames
With Separations Preview on, the renderer copied every node of the
document in every frame and separated each of its colors again. With
Overprint Preview on, it copied each object with an overprinting fill
or stroke in every frame, with the groups and layers that hold it. The
copies were new Arcs, so the geometry, stroke outline and raster
caches missed for each of them in every frame. In a release build,
drawing a 2880x1800 frame of a 50,000-path document again with the
same view and Separations Preview took 795 to 908 ms.

The renderer now keeps the art it prepared and what it was made from,
and in the next frame it uses that art again for each layer, symbol
art and member that has not changed. Only the objects that changed are
prepared again, with the layers and groups that hold them. All of the
art is prepared again when Discard White Overprint changes and, with
Separations Preview, when the plates shown, the proof setup, the
swatches, the Spot Colors option or the color settings change or a
profile is registered (cms::profiles_generation). The renderer now
draws that frame again in 98 to 107 ms, with the same pixels.

Docs: docs/target-app-parity.md.
2026-10-14 11:22:41 +07:00
Gert Pellin 7b0164e4e7 ICC profiles written here have their tags 4-byte aligned
moxcms's encode packs tagged elements back to back, so the built-in
sRGB profile embedded in JPEG, TIFF and PSD exports was 634 bytes with
`cprt` at offset 558, where ICC.1 §7.1.2 requires every tagged element
on a 4-byte boundary (stricter readers reject the files). encode now
lays the tag data out again with each element padded, updating the tag
offsets, the size and the profile ID (zeroed, "not computed"); tags
that share data keep sharing it. Reported to moxcms as
awxkee/moxcms#198.

Fixes #1584
2026-10-13 10:52:38 +07:00
bflatasticandClaude Opus 5.5 c7b46dbb3e Review: palette writing keeps the swatches PaletteFormat::holds names; ROADMAP (#1110)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:33:07 +07:00
dotCooCoo 2b93e8a930 Swatches: swatch.library.save counts only the swatches the file holds
swatch.library.save replied with the number of solid colors and
gradients it was asked to save, including the gradients the format
leaves out. For a new document saved as .ase, count was 50 and the file
held 46 colors. The .gpl and .ase writers keep solid colors only, and
the CSS writer leaves freeform gradients out.

count is now the number of swatches written. The new
PaletteFormat::holds returns whether a file of a format keeps a swatch
with a given paint, and swatch.library.save counts the swatches for
which it returns true. The reply has the same fields as before.

Tests: after a freeform gradient is added to a new document's swatches,
the test checks that count has these values: 51 for .vcswatches, 46 for
.gpl and .ase, and 50 for CSS. Each file holds that many swatches.

Docs: docs/mcp.md.
2026-10-11 11:33:07 +07:00
bflatastic 0497c9e46d #1007: Reverse Gradient mirrors off-centre midpoints
Reversal read the midpoints after Gradient::reverse had reordered the stops, so each stop
took the wrong segment's midpoint: red-to-blue with its midpoint at 20% reversed to a 50%
midpoint instead of 80%, moving the transition.

Gradient::reverse now mirrors each segment's midpoint with it, so a reversed gradient is the
same ramp the other way round; paint.editGradient's own (wrong) pass is gone, and the SVG
importer's reflect spread, which had a correct copy of the same mirroring, uses it too.

Tests: two- and three-stop gradients with off-centre midpoints sample the same reversed (t vs
1 - t) and come back after two reversals; paint.editGradient reverse turns a 0.2 midpoint into
0.8.

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

# Conflicts:
#	ROADMAP.md
2026-10-11 03:51:31 +07:00
bflatasticandClaude Opus 5.5 9a0183a02d Fix #831: color books (.acb) open as swatch libraries
Color books such as the Pantone books a user owns (.acb) couldn't be
opened. They are now read (never written), from public descriptions of
the format: their RGB, CMYK or Lab colors, named with the book's prefix
and suffix ("PANTONE 185 C"), as spot colors unless the book marks
them as process colors, leaving out the pages' unnamed padding. They
open from Window › Swatch Libraries › Other Library…, File › Open and
the user library folder like .ase files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 10:27:58 +07:00
dotCooCoo a61d058f1b Swatches: Save Swatch Library writes swatch exchange (.ase) files
The Save Swatch Library dialog lists Swatch Exchange (.ase) as a
format, and swatch.library.save takes format: "ase" or a path ending
in .ase. Each solid color keeps its model (RGB, CMYK, Lab or Gray) and
its type (spot, global or process), and color groups stay groups.
Gradients and patterns are left out, and a tint swatch is written as
the color it shows. Without a path or user: true, the command returns
the file as dataBase64.

A swatch exchange library opens under the name of its file. When Save
Swatch Library or Save Graphic Style Library saves to a file, the save
panel suggests the Name from the dialog as the file name, as a save to
User Defined does, and the web app downloads the file under that name.
A name made only of dots and spaces, such as "...", gives the file name
Library (Library.vcswatches) in place of a hidden file.

palette_io::write returns the file's bytes. A property test writes
generated libraries and reads them back, and an import fuzz test loads
mutated copies of a file the writer wrote.
2026-10-09 08:15:09 -07:00
fastmikey99andClaude Opus 5.5 2cdf4e3de0 Colour: ICC CMYK converts straight into the RGB profile, with black-point compensation
CMYK went to RGB through display sRGB: CMYK -> sRGB (clamped) -> working RGB.
Into a wider space (Wide Gamut RGB, the 1998 primaries) that clipped every
colour sRGB can't hold - CMYK cyan's red channel landed at 99 instead of 0 -
and the black-point compensation setting had no effect (moxcms leaves it
unimplemented). An ICC CMYK profile now converts directly into the RGB
profile: into its primaries with linear curves, where compensation is a
linear remap from the profile's black (found by sending black through the
profile and reading it back), then onto its own curves. The canvas uses the
same conversion into sRGB, so what shows is what an RGB export writes.

Checked against Ghostscript with the same profiles (relative colorimetric,
compensation on and off): exports into Wide Gamut RGB and the sRGB display
agree within 1 level in 255. The built-in CMYK spaces are unchanged (Generic
CMYK compensates in its own model).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:11:34 +13:00
dotCooCoo e0b1f55250 Swatches: swatch exchange (.ase) files open as swatch libraries
File › Open, Other Library… and the User Defined library folder read
.ase files. Colors keep their model, global and spot colors stay global
and spot, and color groups stay groups. The library readers take a
file's bytes. On the web, File › Open passes a swatch library file to
swatch.library.load as dataBase64 instead of as lossy UTF-8 text.

Swatch and group names in a library are now unique, in .gpl files too.
Add to Swatches puts each color group of a library into one document
group when the document already has a swatch with the group's name.
The library fuzz helpers fail when a command panics.
2026-10-08 15:18:57 -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
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 ffc60d5ff8 M4.59: pattern strokes and freeform gradients keep their look in PDF
Pattern strokes are written as the pattern's tiles clipped to the stroke's
outline (dashes, caps, profile, arrowheads and alignment included) instead
of mid-grey; freeform gradients on fills, strokes and type as an image of
their colour field at the document raster effects resolution, clipped to
what they paint, instead of their average colour. Both read back within
2% of the canvas at 150 ppi with no warning. Text strokes are covered for
cap, miter limit, dash array and alignment; knockout groups keep their
soft-masked form with a warning. The freeform box and grid sampling are
shared with the renderer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:52:35 +07:00
bflatasticandClaude Opus 5.5 ba4ed2ff93 M4.48: JPEG Options — RGB/CMYK/gray, baseline/optimized/progressive, embedded ICC profiles and image maps
document.export {format: "jpg"} takes colorModel (rgb|cmyk|gray), quality 0–100, method
(baseline|optimized|progressive), scans (3–5), embedIcc and imageMap (none|client|server).
JPEGs are written with jpeg-encoder. CMYK files hold ink amounts drawn as the renderer's ink
planes, so CMYK colours keep their inks (black type stays black ink alone). Profiles are made
in code from the CMS (cms::icc_bytes): the RGB spaces from their primaries, the CMYK spaces as
Lab lookup tables sampled from their model, a grey with sRGB's tone curve, and loaded
profiles as loaded. Image maps write <stem>.html or <stem>.map beside the image for objects
with a URL and an Image Map shape. The JPEG Options dialog gains Color Model, Quality 0–10,
Method and Scans, Image Map and Embed ICC Profile; CMYK documents default to CMYK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:25:09 +07:00
bflatasticandClaude Opus 5.5 32252c68ea M4.28: SVG gradient export fidelity
Character gradients on live type export as url(#…) spanning the
laid-out text (point, area, on-path and outlined type, as on the
canvas); character stroke gradients span the text grown by half the
weight. Identical gradients share one def, and a swatch gradient takes
the swatch's name as its id. Midpoints are written as extra stops marked
data-vc-midpoint, which import turns back into midpoints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:56:54 +07:00
bflatasticandClaude Opus 5.5 0744347065 M14.4: tests main gained follow the no-panic lint rules
As the branch does for older tests: `panic!("…")` instead of
`unreachable!()` in test code (clippy.toml only allows unwrap, expect and
panic there), and the integration tests main added (CLI file I/O, render
gradient text and stroke geometry, SVG export suites) opt out of the
unwrap/expect/panic lints. No test logic changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:40:34 +07:00
Brandon ThomasandClaude Opus 5.5 3e323bfa2a Robustness: VectorCraft never crashes — no panics in shipped code
Shipped code no longer unwraps, expects or panics: every one of the 124
unwrap()/expect()/panic!/unreachable! sites outside tests now handles the
failure (Result/Option, ?, let-else, defaults) and reports errors to the user.

- Lints: workspace clippy denies unwrap_used, expect_used, panic, todo,
  unimplemented and unreachable; clippy.toml allows them in tests, and
  integration tests, examples and testkit opt out explicitly.
- Safety net (vectorcraft_engine::guard): a panic in a top-level command rolls
  the document back and returns EngineError::Internal; a panicking tool is
  reset; the MCP server answers with an internal error and keeps serving; the
  UI frame and each control request are guarded.
- APIs that hid failures now return them: Rendered::to_png/to_jpeg/to_webp
  return Result (JPEG/WebP errors used to write empty files),
  FontDb::face returns Option, Region::top returns Option.
- Fuzzing: engine/tests/import_fuzz.rs feeds garbage, hostile and mutated SVG
  and PDF through import → render → export (30k cases locally, no panics).
- Docs: AGENTS.md/CLAUDE.md rule, docs/development.md "Robustness" section,
  README pointer and a PR template checklist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:28:59 +07:00
bflatasticandClaude Opus 5.5 025d758abe M3.83: gradients within, along or across a stroke
The Gradient panel's Stroke buttons now work: with the Stroke proxy on a
linear or radial gradient they lay it within the stroke (placed on the
page, as before), along the path (start to end of each subpath) or across
it (left edge to right edge; inside/outside strokes span their visible
side). `paint.editGradient {stroke: true, strokeMode}` sets it (new art
too with nothing selected); `document.inspect` reports
strokeOptions.gradientMode. Model: StrokeLayer.gradient_mode (default
Within, not written).

Shared geometry in effects::stroke::gradient: ribs on the bisectors of
the flattened path (fans round sharp corners, widened under arrowheads,
following width profiles) cut slices each painted with a linear piece of
the gradient (Gradient::span). The canvas and raster export clip them to
the stroke's outline and arrowheads, grown a pixel and composited with
copy so there are no seams and translucent stops paint once. SVG and PDF
write the same slices clipped to the outline, with a warning (new
vectorcraft_svg::export_with_report). Outline Stroke and Expand make
gradient meshes clipped to the outline. Mesh quads now grow along their
edges so thin quads show no seams. Dashes, profiles and arrowheads keep
working in both modes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 05:47:54 +07:00
bflatasticandClaude Opus 5.5 be648089be M3.96: Color Themes panel with a local theme library
Window → Color Themes: the Create tab makes a five-colour theme on a
harmony wheel (a harmony rule from the selected colour, colours moving
together; or Custom, each on its own), with a brightness slider, hex
field, current-colour picker and name; the My Themes tab lists saved
themes to edit, add to Swatches as a colour group, or delete. Themes are
kept in the preferences (Prefs::color_themes; no online service).

Commands: colorTheme.list, colorTheme.save {colors | color + rule, name?,
replace?}, colorTheme.delete, colorTheme.addToSwatches. Harmony::theme
and move_on_wheel live in the colour crate; the wheel is now a shared
widget (widgets::harmony_wheel) used by Recolor Artwork's Edit tab too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 05:08:28 +07:00
bflatasticandClaude Opus 5.5 81582c8d4f M3.95: Color Guide Limit to Library and Edit or Apply Colors without art
The Color Guide's library button opens a Limit to Library popup (None,
Document Swatches, the colour libraries): every harmony colour and
variation snaps to the nearest colour there (CIEDE2000), cached per
inputs so the grid isn't re-snapped every frame. `color.harmony` takes
`limitTo`, `ui.colorGuideLimit` sets the panel's limit, and Recolor
Artwork's Limit to Library also offers the document's swatches (one
shared engine helper, swatchlib::limit_palette).

Edit or Apply Colors opens Recolor with the harmony colours limited to
the same library; with no art selected the colours are the rows and OK
saves them as a new colour group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 05:08:15 +07:00
bflatasticandClaude Opus 5.5 d8eff17b9f M3.98: radial annotator extent ellipse, aspect handle and off-centre focal point
Radial gradients show a dashed extent ellipse: its handle across the bar
sets the aspect ratio, the ellipse elsewhere rotates it, and the dot inside
the centre ring drags the focal point (GradientGeom.focal) for an off-centre
radial. paint.setGradientGeom takes aspect (%) and focal ([x,y] | null), and
start/end may be left out; the focal point keeps its place in the ellipse as
the vector, aspect and object transform. It renders as a two-point radial,
exports and imports as SVG fx/fy and PDF two-point radial shadings, expands
to off-centre rings, and travels in gradient params and the native format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:43:43 +07:00
bflatasticandClaude Opus 5.5 9537eca5f7 M3.90: blend modes and opacity composite inks in CMYK documents
A CMYK document that shows transparency is drawn once per ink plane
(complemented C, M, Y; complemented K as a grey) so every blend mode,
opacity, group, knockout and effect composites inks, PDF-style; the
planes' inks are shown through the working CMYK profile (cached 4-D XYZ
table). Cyan multiplied over magenta now shows C100 M100. Opacity masks
keep screen luminance; RGB colours and placed images are separated
first. RGB documents and opaque CMYK documents keep the screen-colour
path.

PDF export of CMYK documents writes its transparency groups with
/CS /DeviceCMYK (luminosity mask groups stay RGB) and puts top-level
transparency in a non-isolated page-wide group so it blends in CMYK too.
Flatten Transparency composites CMYK documents ink by ink and makes CMYK
regions. Non-separable modes no longer give NaN for greys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:30:06 +07:00
bflatasticandClaude Opus 5.5 fca068a0a7 M3.78: Recolor Artwork with colour reduction, methods, presets and Limit to Library
Recolor Artwork keys colours by identity (model and exact values), so CMYK
colours stay CMYK and lookalikes stay apart. recolor.reduce groups the
selection's colours into rows (k-means in Lab, weighted by use; tints of a
global swatch share its row) with Preserve White/Black/Grays and Limit to
Library; recolor.apply maps rows by Exact, Preserve Tints, Scale Tints, Tints
and Shades or Hue Shift; recolor.randomize shuffles or varies new colours.
The dialog has Assign (rows with merge, separate, exclude, randomize, method,
library) and Edit (hue/saturation wheel with linked or unlinked handles,
brightness, harmony rules) tabs, and Edit Colors gains Recolor with Preset
(1/2/3 Color Job, Color Library). Colour parameters accept colour keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:20:27 +07:00
bflatasticandClaude Opus 5.5 ea61d34a93 M3.85: spot colours defined in Lab, with Spot Colors options for Lab values or CMYK equivalents
Colours gain a CIE Lab model (`Color::Lab {l,a,b}`, serde tag `lab`), handled through the
CMS (display, separation, conversions, gamut), tints (toward paper white), keep_model, harmonies,
recolouring, blends, Info and the Color panel. Swatch Options gets a Lab mode
(`swatch.edit {mode: "lab"}` / `{color: {l,a,b}}`), and colour params accept `{l,a,b}`.

Swatches panel → Spot Colors… (`ui.spotColors`, dialog `spotColors`) runs `swatch.spotOptions
{useLab}` (Document.spot_use_lab, default on): Lab spot colours show and separate from their Lab
values, or art linked to them takes their working-CMYK equivalents, as one undo step. PDF export
writes Lab spot inks as Separation spaces with a Lab alternate (tint function from paper white),
or a DeviceCMYK one with CMYK equivalents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:46:57 +07:00
bflatasticandClaude Opus 5.5 c1df6f0621 M3.77: [Registration] swatch on every plate, trim marks on every artboard, Japanese marks and live Crop Marks
[Registration] is a built-in swatch that prints 100% on every plate: Separations
Preview shows it on each process and spot plate and PDF export writes it as
/Separation /All. It can't be edited, moved, duplicated, merged or deleted;
swatch.list shows it after None and the Swatches panel applies it as a swatch.
Object > Create Trim Marks strokes the marks in Registration and marks every
artboard when nothing is selected ({style?, allArtboards?}). The preference
japaneseCropMarks (General) switches to Japanese marks: double lines at the
trim and bleed edges plus centre marks. Effect > Crop Marks (cropMarks) is a
live effect whose marks follow the object; rendering, SVG/PDF export and
Expand Appearance draw them. The mark geometry lives in doc::marks.

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