Commit Graph
561 Commits
Author SHA1 Message Date
Louise FoxandClaude Opus 5.5 24936cd006 Plug-ins: exporters write file formats of their own (G17)
A plug-in could filter objects or reshape them as a live effect, but
nothing it made could leave the app: pen plotters, cutting machines and
laser cutters read formats VectorCraft doesn't write. An exporter is a
third kind of plug-in ("kind": "exporter", with the file "extensions"
it writes), in the same sandbox and with the same ABI v1 exports.

- It adds a write-only format, plugin.<id>. document.formats lists it
  with its plug-in id, and document.export writes it by that id, or by
  a path ending in one of its extensions that no built-in format has;
  "params" carries its parameters, checked against its manifest as a
  filter's are.
- File › Export › Export As lists it after the built-in formats, with
  Use Artboards greyed out, since the plug-in writes the whole
  document. One with parameters asks for them in a dialog generated
  from its schema (pluginExport) once the file is picked.
- The plug-in is given the native file's JSON (format::json_part:
  images of 8 KiB or more without their data) and _context {mode:
  "export", extension, name}. It returns {"dataBase64", "warnings"?}
  or {"error"}. The host writes the file, and nothing when the plug-in
  declines or returns bad data.
- An export gets 10 G instructions plus 4,000 per byte and 300 s.
  Background Export's Cancel stops it between two slices of that
  budget (Error::Cancelled), and a cancelled live-effect run is not
  cached as a failure.
- plugin.run and Object › Plug-ins say where an exporter is instead of
  calling it a live effect.

engine/tests/plugin_export.rs reads back what an exporter is handed
with wat::echo_exporter, an exporter whose file is its input or its
parameters (its base64 encoder is written in WAT). The document it is
given is document.save's JSON, without the large image's data, and its
parameters arrive defaulted, clamped and with the file's name and
extension. It also covers listing and removal, built-in extensions
winning, selectedOnly, declines, bad base64, missing data, warnings,
and Cancel stopping a plug-in that never returns. It is a test binary
of its own: installed plug-ins are process-wide, and the format-list
unit tests check exact lists. dialogs/tests_plugin_export.rs drives
Export As headlessly; plugins/tests/abi.rs covers the budget, the
parameters and the echo exporter; format/tests/native_v4.rs covers
json_part.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
2026-10-20 08:17:48 +07:00
dotCooCoo 7617fcd536 Artboards: Width, Height, Preset and Orientation apply to every selected artboard (#2565) 2026-10-20 07:23:49 +07:00
dotCooCoo 89986d01bf Clipboard: a copied .ai or .ait file pastes the objects of its editing data (#2408) 2026-10-20 07:23:40 +07:00
KnottyDyes 371d0de798 Docs: install the CLI and register it with MCP clients 2026-10-20 07:10:21 +07:00
00xJ0 548ace24d2 Clipboard: a copy with a large image no longer puts its SVG on the clipboard as text (fixes #2213)
With Include SVG Code on, Copy offered the SVG markup as plain text with every placed image
embedded as base64: a 13 MB photo made 17 MB of text. A paste reads the text first (eframe on
every paste key; on Linux and macOS it is the one format written, and holds_ours reads it again),
so each paste moved it through the system clipboard on the UI thread, and on GNOME the app
stopped responding.

The SVG markup now goes out as text only while the images it embeds total at most 1 MiB; above
that it is offered as image/svg+xml and PNG. Type keeps its text, and vector art keeps its SVG
text at any size (only embedded image bytes count).

Windows 11, Ryzen 5 2600X, 13 MB PNG, real Ctrl+C: the longest UI stall fell from 823-1021 ms
to 133 ms. Not run on Linux.
2026-10-20 07:10:21 +07:00
Dan Green 7852f363a1 Fix #2143: selected ruler guides align to a key object
A lone object selected with ruler guides can be the key (the Selection
tool's click on it), and Align to the key moves the other selected
guides to its edge or centre: vertical guides to left, centre or right,
horizontal ones to top, centre or bottom. Parallel guides line up on a
key guide. Aligning to the selection or the artboard leaves guides alone.
2026-10-20 05:56:59 +07:00
Dan Green 263c36567f Fix #2143: a ruler guide can be the key object Align lines objects up on
A click with the Selection tool on a guide selected with objects makes
it the key (select.key {guide}), drawn thicker; a click on it again
cancels it. Align to Key Object is on with one object and a selected
guide and makes the guide the key. object.align lines the objects (or
selected anchors) up on the guide's line and leaves the guide where it
is: a vertical guide sets x only, a horizontal one y. The selection
keeps key_guide beside key, never both; it goes with the guide or the
objects, and follows the guide when earlier guides are deleted.
Distribute Spacing doesn't space from a key guide.
2026-10-20 05:56:58 +07:00
Dan Green 6bed87b5fb Libraries panel: the list scrolls; items and groups reorder by dragging, items drag into groups
The list fills the panel down to its bottom bar and scrolls, also while a drag nears its top or
bottom edge (in the dock and in a flyout, as the Layers panel does).

Dragging in the list:
- an item (colour, text style or graphic) dropped on another of its kind lands just before or
  after it, by the pointer's half, an accent line showing where, and joins that item's group;
- dropped elsewhere on a group (its header too, even collapsed) it goes into that group, and on
  the ungrouped items out of its group, the section outlined; with every item grouped, a
  "Drop here to take it out of its group" strip shows while one is dragged;
- a group dragged by its header goes before or after another group.
Colours and graphics still drag onto art and the canvas as before.

Engine: library.moveItem takes `before` / `after` (an item of the same kind) to place the item
in its kind's order, which the panel shows; the new library.moveGroup {group, to} moves a group.
Text styles drag as PanelDrag::LibraryItem, groups as PanelDrag::LibraryGroup, and a library
colour's Paint drag names its library item, so the panel tells it from a swatch dragged in.

Tests: reordering and moving in the engine, the commands in the library fuzz test, and panel
tests that drop styles and group headers and check the list scrolls and scrolls while dragging.
Strings translated in the 23 catalogs; docs/mcp.md and the parity row updated.
2026-10-20 05:28:17 +07:00
MarekSpodBiedry 7c47692b32 Ask for the .ai options on Save a Copy like Save As (#3141) 2026-10-20 03:41:16 +07:00
bflatastic a52198cefa Appearance: image fills and strokes draw the shape Convert to Shape or Outline Object gives them
Squashed from #2500 (its merges of main dropped).
2026-10-20 02:06:54 +07:00
dotCooCoo 926ca62952 Simplify: Object › Path › Simplify opens a slider with Auto-Simplify and More Options
Squashed from #2555 (its merges of main dropped).
2026-10-20 02:06:54 +07:00
sungjea kimandClaude Opus 5.5 515569470c Anchor Point tool: Shift-drag on an anchor moves it, handles and all
With the Anchor Point tool, a drag from an anchor pulls out smooth handles.
Shift held at the press now moves the anchor instead, its handles keeping
their offsets; Shift kept down during the drag keeps the move at 45° steps
(the constrain angle), released it frees it. A Shift-click that doesn't
move leaves the anchor as it was. A selected blend's spine points follow
the same rule (object.blend.spine.moveAnchor).

The move goes through a new engine command, path.moveAnchor
{id, subpath?, anchor, x, y}, since path.moveAnchors only acts on the
direct-selected anchors and the Anchor Point tool keeps no anchor selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:19:53 +07:00
Hamza 7bc7025a62 Exports: say that an encrypted PDF still differs, and test the fallback
Two review points on #2880.

The reproducibility claim needed qualifying. PDF encryption draws its AES
initialisation vectors and the file `/ID` from a random seed, which
`SOURCE_DATE_EPOCH` deliberately does not reach, so a PDF exported with a
password never repeats however the variable is set. The docs and the ROADMAP
line say so now.

The byte-identity test passes an explicit `created`, so it never exercised the
clock-or-variable fallback. `pinned_or` takes the environment and the clock as
arguments, and the new test shows the variable is preferred, that a blank,
negative or non-numeric value leaves it to the clock, and that a host with
neither has no date to record. The environment is injected rather than set: a
test that changes this process's environment is `unsafe` under edition 2024 and
would reach every other test running beside it, which is why `cursor_theme`
takes a lookup closure too.
2026-10-19 22:19:51 +07:00
dotCooCoo 219253f163 Objects on Path: attach objects to a path and spread them along it
VectorCraft had no Objects on Path. A click with the Objects on Path
tool attaches the selected objects to the path clicked. The tool is in
Edit Toolbar's Modify section and the Blend tool's flyout, and Object ›
Objects on Path › Attach and the context menu with two or more objects
selected choose it. The objects and the path go into an Objects on
Path group with the path at its bottom.

- object.objectsOnPath.attach orders the objects column by column from
  left to right and top to bottom in each column. The first one sits
  on the path's first anchor, the last one sits on the end of an open
  path, and the rest are spread evenly by length; a closed path takes
  them all the way round. Each object's attachment point (Pivot, one
  of nine, default the center of its bounds without strokes) sits on
  the path, and the object is turned to the path there. An object that
  was rotated by an angle other than a multiple of 90° is first turned
  to the nearest multiple of 90°. Rotate tilts every object by that
  many degrees counter-clockwise. Symbol instances are measured by
  their art. Guides, compound paths, type, paths without length and
  graphs are refused with a reason.
- After every edit the objects go back on the path: editing the path
  moves them, an object moved on its own goes back, and objects moved
  into or out of the group in the Layers panel join the end of the
  path or leave it. Scaling, shearing or reflecting the whole group
  keeps each object's scale, shear or reflection, and the objects are
  spread evenly along the transformed path again. Undo takes the path
  and the objects back in one step, and a copied group stays live on
  its own path.
- object.objectsOnPath.options changes Pivot and Rotate, and
  object.objectsOnPath.expand leaves a plain group. Both act on the
  groups that `ids` names, or on the groups of the selection when
  `ids` is not given.
- The native file keeps the group live; SVG, PDF and the other exports
  write plain art. The tool highlights the path under the pointer, has
  a cursor entry, and Escape during a press cancels the click.

Part of #2545

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-19 21:51:32 +07:00
dotCooCoo e6c8ce1759 Live Paint: Live Paint Selection Tool Options set what a click selects and the highlight
Double-clicking the Live Paint Selection tool did nothing, and the tool
always selected faces and edges and highlighted them in red at 4 px.
It now opens Live Paint Selection Tool Options with Select Fills,
Select Strokes, Color and Width, the options the reference app's help
page on selecting objects lists.

- With Select Fills off, a click selects the edge nearest the pointer,
  as Alt-click does. With Select Strokes off, Alt-click selects the
  face under the pointer, as a click does. With both off, a click
  selects no face or edge.
- Color (a layer color or a custom color) and Width (1 to 20 px) set
  the highlight on the face or edge under the pointer.
- The defaults are the earlier behavior: both selections on, red
  (#ff2a2a) and 4 px. The options are tool options (tool.setOption
  {tool: "livePaintSelection"}): they last across tool switches and
  are saved with the preferences.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-19 21:51:31 +07:00
dotCooCoo 729582dc78 Layers: Locate Selected Objects opens nothing for an object the panel does not list
With Locate Selected Objects on, an object selected on the canvas had the
layers and groups around it opened even when Show Layers Only, a search or
the filter left it without a row. The panel did not scroll to it, but the
rows stayed open, so they showed once the panel listed every row again.

The panel now opens the rows around the object only when the object has a
row once they are open. Otherwise it opens nothing and scrolls as with the
option off. The Layers panel entries in docs/mcp.md and
docs/target-app-parity.md describe this.
2026-10-19 21:51:29 +07:00
dotCooCoo 7c64ecd998 Layers: with Locate Selected Objects on, the panel shows the row of art selected on the canvas
By default the Layers panel opens the layers around a row and scrolls
the row into view when its highlighted rows change, for example after
a click in the panel, a new layer or Locate Object. When no row is
highlighted and a selection makes another layer or sublayer the
active one, the panel does the same for that layer's row. Selecting
art on the canvas or by a command highlights no row, so by default
the panel does not open the layer or sublayer that holds the art or
the groups around it, and does not scroll to the art's row.

Panel Options gets Locate Selected Objects, off by default and kept
with the UI state. With it on, when the last selected object changes
on the canvas or by a command, the panel opens the layers and groups
around it and scrolls to its row, as after Locate Object, but does not
highlight the row. When the selection also changes the active layer,
the panel scrolls to the object's row instead of the layer's row.
When Show Layers Only, a search or the filter leaves the object
without a row, the panel scrolls as it does with the option off. A change made with the panel's own
rows, buttons or menu is not located, and a change made during a drag
is located when the drag ends. Locate Object is unchanged.

On a layer of 5,000 groups of 10 paths with every row open, the first
panel frame after a selection change took a median of 1.6 ms with the
option on and took a median of 1.4 ms with it off, in a release build.

Docs: docs/mcp.md (locateSelected), docs/target-app-parity.md.
2026-10-19 21:51:29 +07:00
dotCooCoo c3a6aa0a29 Variables: a data set with no value for any bound variable shows a status message
Choosing a data set in the Variables panel, stepping through data sets
with the panel's arrows, and running `dataset.select`, `dataset.next`
and `dataset.prev` applied nothing and showed no message when no object
was bound to a variable that the data set has a value for. This
happens, for example, after Import Data Sets… defines new variables
without binding them.

The app now shows "nothing in data set A was applied: no object is
bound to a variable it has a value for" in the status bar, and the three
commands add it to the reply as `warning`. A data set with a value for
a bound variable gives no warning even when every object bound to it is
locked; `skipped` counts those objects. What applying a data set does
is unchanged.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-19 21:51:26 +07:00
Hamza 1c80b23745 Exports: SOURCE_DATE_EPOCH stands in for the clock, so output repeats
PNG, PDF, EPS, legacy `.ai`, packaged zips and saved `.vectorcraft` files
record the wall-clock time they were made in, so the same commands run twice
gave different bytes although the pixels matched (#2880):

  PDF   /CreationDate and /ModDate   D:20261018155816 vs D:20261018155817
  PNG   the `Creation Time` tEXt entry
  .vectorcraft  one byte of the stored date
  SVG   carries no time, and already repeated

`metadata::output_time` is the time a file records: `SOURCE_DATE_EPOCH` when it
holds a non-negative whole number of seconds, else the clock, as the
reproducible-builds specification defines it. A date a command is given
(`file.new {created}`, a save's `modified`) still wins over the variable, and a
blank, negative or non-numeric value is ignored so that an export is never
refused over the environment.

`metadata::now_unix` keeps the real clock and now has exactly two callers, both
of which must not be pinnable from the environment: the recovery heartbeat, and
the seed PDF encryption draws on.
2026-10-19 00:37:55 +07:00
dotCooCoo cec31c5f4a PDF/.ai import: a .ai saved without PDF compatibility opens when pages names every page
A .ai saved without PDF compatibility has only a placeholder page in
its PDF part. document.open reads such a file from its editing data,
but with `pages` given it failed with "the file holds only a
placeholder page", even for "1" on a file of one page.

When `pages` names every page of the file, document.open now reads
the editing data as it does without `pages`.

Docs: docs/mcp.md.
2026-10-19 00:37:38 +07:00
dotCooCoo c0ec14b86d Paths: anchor commands refuse a subpath that is not a whole number
path.curvatureEdit, path.removeAnchor, path.convertAnchor,
path.reshapeSegment, path.split, path.setHandle and path.insertAnchor
read subpath as an unsigned integer and fell back to 0 for any other
value. A subpath of 1.5, -1 or "1" edited subpath 0 and the command
reported success. path.split read anchor and segment with as_u64 too
and treated any other value as absent, so a bad anchor beside a good
segment split at the segment.

These commands now refuse a subpath that is not a whole number of 0 or
more with "missing integer `subpath`", the error path.curvatureEdit,
path.removeAnchor, path.convertAnchor and path.reshapeSegment already
give for a bad anchor or segment, and path.split refuses such an anchor
or segment too. Without a subpath, or with null, they edit subpath 0 as
before. The document and the undo history stay as they were after each
error. The params text of the six commands that listed subpath as
required now marks it optional.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-19 00:03:47 +07:00
dotCooCoo 89a73abc38 Links: a document opened from a mapped drive finds links on its share
craft-docpath follows a network path only on the share the document is
on, and its caller contract asks for a document opened from a mapped
drive to be passed by its UNC path. fileio::document_path passed the
drive path as given, so a document opened as Z:\jobs\doc.vectorcraft
treated a link stored as \\server\share\img\logo.png, on the share
behind Z:, as on another computer: the image was missing, and
document.open, links.check and links.info gave refused: "the path is on
another computer".

On Windows, when a stored path is refused as on another computer and
the document is on a drive, document_path now reads the final path of
the document's folder (std::fs::canonicalize). When that names a share
(\\?\UNC\server\share\...), the path is checked again against the
document's path on that share. Links on other computers stay refused,
and paths that were followed resolve as before. Opening a document,
Link Info, links.check and the linked-file watch all use it. Other
systems are unchanged.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-19 00:03:47 +07:00
dotCooCoo cc8944ad2a Transform Each, Rasterize, Mosaic, Trim Marks and Rearrange: a number given as text is refused
Transform Each, Rasterize, Create Object Mosaic, Create Trim Marks and
Rearrange All Artboards read their optional numbers with f64_or, which
falls back to a default for a value that is not a JSON number.
object.transformEach {"scaleH": "50"} left the selected objects at
their size and reported success. Rasterize did the same with ppi,
Create Object Mosaic did the same with width, height, spacingX and
spacingY, Create Trim Marks did the same with offset, length, bleed and
weight, and Rearrange All Artboards did the same with spacing. Crop
Image cropped to the artboard when rect was a string or another value
that is not an array.

These commands now fail with "`<key>` must be a number, not <value>"
for a value that is not a number or null, as the transform commands
do, and change nothing. Transform Each checks scaleH, scaleV, moveH,
moveV and rotate. Crop Image refuses a rect that is not an array of
four numbers with "rect must be [x, y, width, height]", the message it
already gave for an array of the wrong length; a null rect crops to the
artboard, as a missing one does.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-19 00:03:46 +07:00
dotCooCoo 05264b2801 MCP: document.pdfInfo answers one page for an EPS, a .ps file or a PostScript .ai
document.pdfInfo refused PostScript (an EPS, a .ps file, or a .ai in
PostScript form), mostly with "this is a PostScript file ..., not a
PDF", although document.open and file.place read the same file
through the EPS reader as one page.

It now answers {pages: 1, needsPassword: false, pageInfo: [{width,
height, rotation: 0, boxes: {}}]}. The page is the file's bounding box
(%%HiResBoundingBox when it has one, a US Letter page without one),
read from its comments without running its program. thumbnail: 1 is a
PNG of what that page prints, read by the EPS reader without any
editing data the file carries. A program whose header is only %! is
read as PostScript, as clipboard.importEps reads it.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-19 00:03:45 +07:00
dotCooCoo d7e7066251 File formats: Targa (.tga) files open and place
Squashed from #2765 (its merge of main dropped).
2026-10-18 23:50:58 +07:00
bflatastic eec304dde1 SVG export: a Japanese face's numbered weight (W0–W9) is its font-weight, W6 600 (#2873)
style_weight reads W0–W9 style names (Hiragino, Morisawa…) as their weight classes before the English words, so SVG and CSS export write font-weight 600 for W6, and nearest-style matching weighs them too.
2026-10-18 19:02:52 +07:00
VictorandClaude Opus 5.5 a275496ef4 G4: Paragraph panel menu › Justification… (word and letter spacing, glyph scaling, auto leading, single word justification)
The Paragraph panel menu's Justification… was a disabled stub, and justified
lines stretched word spaces between fixed limits (80% to 150%).

Each paragraph now has Justification options: Word Spacing, Letter Spacing
and Glyph Scaling (Minimum, Desired, Maximum, in percent), Auto Leading and
Single Word Justification. Desired applies to every line; a justified line
fills its measure with word spacing first, then letter spacing, then glyph
scaling, each within its range, and past them its word spaces take the
rest. The Every-line Composer scores lines by that flexibility. Auto
Leading sets the leading of characters whose leading is Auto. A justified
line holding one word is spread between its letters (Full Justify) or
aligned left, centre or right.

The defaults are the documented ones (80/100/133, 0/0/0, 100/100/100, 120,
Full Justify), saved only when they differ. Because the default maximum
word spacing is 133%, justified paragraphs break differently than before;
the Every-line Composer's second pass is now unbounded for justified text
(it used to give up past four times the stretch and fall back to the
Single-line Composer, which the narrower default range would make common),
and lines that can't stretch at all are still refused.

text.setFormat takes wordSpacing, letterSpacing, glyphScaling ([min,
desired, max] or {min?, desired?, max?}), autoLeading and
singleWordJustification; Minimum ≤ Desired ≤ Maximum must hold or nothing
changes. Paragraph styles carry the options (New and Redefine capture
them), and Reset Panel resets them. Justification… opens a dialog
(ui.justification) with Preview; OK is disabled while a row is out of
order. Live SVG text places characters the options move, and a glyph that
Glyph Scaling scales is stretched to its width; PDF uses the glyphs'
transforms. Vertical type isn't glyph scaled; type on a path doesn't use
the options.

Docs: docs/mcp.md, docs/type-parity.md, docs/gaps.md.

Closes #2755.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:40:14 +07:00
dotCooCoo d8b0ce1013 Blends, meshes and envelopes: a number given as text is refused
The blend, envelope and gradient mesh commands read their optional
numbers with f64_or or as_f64, which fall back to a default for a value
that is not a JSON number. object.blend.make {"steps": "8"} made a
Smooth Color blend and reported success. Blend Options did the same
with steps, distance and value. Make with Warp, Reset with Warp and
Envelope Options did the same with bend, h and v (and their long
names). Make with Mesh, Reset with Mesh and Create Gradient Mesh did
the same with rows, cols and columns. Envelope Options did the same
with fidelity, Create Gradient Mesh did the same with highlight, and
Set Mesh Point Color did the same with opacity.

These commands now fail with "`<key>` must be a number, not <value>"
for a value that is not a number or null, as the transform commands
do, and change nothing. Envelope Options refuses the warp keys whether
or not a warp envelope is selected. Create Gradient Mesh refuses an
`at` that is not exactly two numbers with "`at` must be [x, y]".
Before, it took the first two entries of a longer array, and for any
other value it made a mesh with no lines through the point.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-18 12:40:12 +07:00
dotCooCoo 138e9ca466 Paths: a curve with a handle near f64::MAX is measured by its control points
PathData::bounds measured each segment by its start, end and extrema,
as kurbo's bounding_box does. For a curve with a handle at about 1e308
in size, or an infinite one, kurbo's extrema computation overflows and
drops the extreme. The box then held only the anchors. The canvas check
after an edit reads that box for the selected objects, so path.create
kept a curve with handles at 1e308.

The new segment_bounds measures a segment by its control points when
one of them has an x or y that is infinite or larger in size than
f64::MAX / 16, and by its extrema otherwise, bit for bit as before.
curve_bounds joins those boxes as kurbo does, and PathData::bounds uses
it, so such a curve's box holds the whole curve and the canvas check
refuses an edit that leaves one selected.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-18 12:39:09 +07:00
dotCooCoo 1caa957531 History: the panel menu clears the history
The History panel menu had no Clear History, and VectorCraft had no
command that removes the undo and redo steps. In the reference app,
Clear History in the History panel menu removes the states after the
selected state, and it removes the states before it while their
changes remain in the document.

edit.clearHistory (Clear History) removes every step to undo and to
redo in the active document and returns how many it removed. The
document and the selection stay as they are, and clearing is not an
undo step. Typing or a drag still in progress becomes a step first, as
Undo makes it one, so its change stays in the document. The command is
disabled when there is no step to clear, and it waits while an input
method composes, as Undo does.

The History panel menu has Clear History after Step Forward. It runs
the command and is disabled when there is no step to clear.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-18 12:39:07 +07:00
dotCooCoo aa9cf83fd2 PDF export: PDF and .ai files keep the document's creation date
The engine built the PDF options without a creation date, so the
writer dated every PDF and PDF-compatible .ai file with the time it
was written. A document created on 2020-01-01 (File › File Info
shows the date) exported as a PDF with today's date in /CreationDate
and xmp:CreateDate. SVG export with the metadata option already
writes the document's own date.

The options now carry the document's File Info created date and the
time of writing as the modification date. A document without a
creation date is dated with the time of writing, as before, so PDF/X
files still get their required date. The PDF writer writes one date
for both, so the export then sets /ModDate, xmp:ModifyDate and the
PDF/A history dates to the time of writing; /CreationDate and
xmp:CreateDate keep the document's date. The page information mark
and the embedded editing data's modification date keep the time of
writing.

Docs: docs/mcp.md (File Info), docs/target-app-parity.md.
2026-10-18 12:36:27 +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
dotCooCoo db3092ae2e Patterns: Pattern Options refuses a name another swatch has
pattern.options refused a new name that another pattern had, but not
one that a color swatch, a gradient swatch, a second swatch of the
pattern or a color group had. The rename then gave the pattern's swatch
that name, so two swatches, or a swatch and a color group, had one
name. The rename also gave the new name to every swatch that had the
old one, a color swatch included, and art linked to that color swatch
kept a link to a name no swatch had. Make and Save a Copy numbered a
pattern's or a swatch's name, but not a color group's.

pattern.options now refuses a name that a swatch or a color group has,
with "a swatch or color group named `<name>` exists". It takes a name
that only the pattern's own swatches have, such as a name Swatch
Options gave the pattern's swatch, when none of them still has the old
name. A rename gives the new name only to the pattern's swatches that
had the old one. Make and Save a Copy number a color group's name as
they number a swatch's. The new message has a row in the 17 catalogs
that translate every message (COMPLETE_MESSAGES) and in bg, da, id and
nl.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-18 12:36:22 +07:00
dotCooCoo 0cf12a1611 Save as Template: aiVersion 7 is an error for a .ait, and the dialog shows no Version
file.saveAsTemplate with format ait and aiVersion: "7" wrote the
legacy PostScript .ai of version 7 under the .ait name: for a
one-artboard test document, a 7,556-byte file starting %!PS-Adobe-3.0.
The web's Save as Template dialog showed the .ai Version menu for
.ait, whose Legacy (version 7) choice set aiVersion 7.

aiVersion 7 for a .ait is now an error, "aiVersion 7 writes legacy .ai
files; a .ait template is always PDF-compatible", and nothing is
written. The 17 catalogs that translate every message have a row for
it. The dialog shows no Version row for .ait, and aiVersion "pdf"
still writes the PDF-compatible template.

Docs: docs/mcp.md, docs/target-app-parity.md, docs/file-format-parity.md
(two stale rows for .ai and .ait removed).
2026-10-18 12:35:18 +07:00
dotCooCoo d23030ce2b Export: a .ait export names file.saveAsTemplate, and the CLI's convert writes the template
document.export, document.serialize, document.exportSelection and
Export for Screens rows answered "PDF-compatible .ait template files
can be opened but not written" for a .ait, although
file.saveAsTemplate writes them. The CLI's convert and run --export
call document.export, so vectorcraft-cli convert in.pdf out.ait
failed with that error.

The error now reads "PDF-compatible .ait template files are written
by file.saveAsTemplate", with a row in the 17 catalogs that translate
status messages and in bg, da, id and nl. convert and run --export
write a .ait with file.saveAsTemplate, which writes every artboard
with the .ai options. convert passes --replace-lossy and its .ai flags
on, and refuses --artboard and --range for a .ait.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-18 12:34:41 +07:00
dotCooCoo 2643f57b1f Color: type, gradient, mesh and swatch commands store RGB input as color-managed CMYK in a CMYK document
In a CMYK document, paint.setFill stores an RGB color as CMYK through
the color settings, but the other commands that take a color stored it
as RGB: text.create, text.createInPath, text.setStyle and
text.setRangeStyle colors, a shapeBuilder.merge hex fill,
paint.editGradient stops, freeform gradient points, gradient mesh
points, the Symbol Stainer's color, and swatch.new {colors},
swatch.newGroup, swatch.editGroup and swatch.edit. The Mesh tool sent
the fill as a hex value, so a CMYK fill came in as RGB.

These commands now store such a color through paint::new_color, as
paint.setFill does, and take keepModel to keep RGB (shapeBuilder.merge
inside its fill object). A color the swatch
or gradient already has keeps its model, so stops sent back with a
moved offset keep their colors (the Gradient panel's ramp and the
Gradient tool send keepModel), and a stop that names a process
swatch takes the swatch's color as it is, in paint.setFill gradients
too. Where the app copies an existing color, it keeps the color's
model: the Color panel's sliders for gradient stops and freeform
points, a swatch put on a stop or a point (the stop popover, Alt-click
in the Swatches panel, a drop on the Gradient panel's ramp), the fill
the Mesh tool gives a new point or the Symbol Stainer applies, and the
colors the Color Guide, Color Themes and Recolor Artwork save as
swatches.

swatch.new {color, mode} converted RGB into CMYK before converting it
into mode, so the New Swatch dialog in RGB mode stored #00ff00 as
R101 G176 B62 in a CMYK document. With mode, the color is now
converted once.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-18 12:34:40 +07:00
dotCooCoo 22ce516e2d Appearance: the inline effect editor shows 3D, Warp, Rasterize and channel choices as dropdowns
The Appearance panel's inline effect editor still showed eight choice
parameters as text fields holding the stored value: Revolve's edge,
Extrude & Bevel's bevel, every Warp style's orientation, Rasterize's
Color Model, Background and Anti-aliasing, and the channel of Levels
and Curves. Their dialogs built these lists in their own bodies, so
effect::choices did not hold them. Nothing checked a channel's value:
a typo was stored, and Levels or Curves drew it as RGB.

effect::own_choices now holds these lists, each label as its dialog
passes it to its dropdown or radio buttons. The dialogs read the lists
from it (Rasterize's Anti-aliasing from the AntiAlias list it is built
from), and the inline editor shows each one as a dropdown with the
dialog's translated labels; Rasterize's Color Model reads CMYK or RGB
by the document's mode. effect.apply and effect.setParams refuse a
Levels or Curves channel other than rgb, red, green or blue (r, g, b
and any letter case are read too) with "`channel` must be one of rgb,
red, green, blue", and the document and its undo history stay as they
were.

Docs: docs/mcp.md (effect params), docs/target-app-parity.md.
2026-10-18 12:34:36 +07:00
dotCooCoo b27b5b80ea Place: Bounding Box measures only the art that shows
With crop "bounding", file.place measured the placed art by each
object's visual bounds, which include the hidden members of a group.
A group holding a 40 pt square and a hidden 170 x 42 pt rectangle was
measured as 251 x 123 pt: at, rect, the place cursor and
file.place.info all used that box. A group whose only part on the page
or artboard was a hidden member was also placed, and symbol instances
were measured as a 20 pt square.

The art's box now follows the rule exports use
(vectorcraft_render::encode::drawn_bounds, made public): hidden
objects, guides and template layers do not count; the reach of effects
such as a drop shadow now counts; symbol instances are measured by
their art. The same rule picks the objects on the page or artboard.
Hidden objects on the page or artboard are still placed, and stay
hidden. Replace and the width and height that file.place answers also
measure symbol instances by their art.

Docs: docs/mcp.md (Place), docs/target-app-parity.md.
2026-10-18 12:34:29 +07:00
dotCooCoo df7daff3e4 PDF export: an image that can't be decoded is left out with a note instead of failing the export
When the PDF writer keeps a PNG, GIF or WebP image as it is, it accepts
the image once its header reads, and it reports pixel data that it
can't decode only when it finishes the file. An image whose pixel data
was cut short or damaged therefore failed the whole export with "PDF
writer error: Image(...)", and nothing was written. This happened in
Save as PDF, .ai saves, Save a Copy, Print, Export for Screens and
native files with PDF compatibility, and with On Copy: PDF turned on,
Copy left the PDF off the clipboard.

When the writer fails on an image's pixel data, the export now runs
again with that image left out. Its warnings hold the note that raster
exports give, once: "an image couldn't be decoded and is missing from
the picture". This note replaces the writer's "image '<key>' could not
be decoded and was skipped", which named the image by an internal key.
An export without such an image runs once, as before. A .ai file keeps
the image in its editing data and reopens with it.

Docs: docs/mcp.md (PDF options).
2026-10-18 12:34:27 +07:00
dotCooCoo d7e79448d7 Legacy .ai export: warns when an image in art it flattens or expands can't be decoded
Saving a version 7 .ai expands raster effects into images and flattens
images with see-through pixels, opacity or a blending mode, and both
draw the art with the renderer. An image the renderer couldn't decode
drew nothing, so it was missing from the result, and the reply carried
no note. A semi-transparent image cut short was removed without one.

The save now compares decode_failures() before and after it writes the
artboards, as Expand Appearance and the raster exports do, and adds
"an image couldn't be decoded and is missing from the picture" to its
warnings once.

Docs: docs/mcp.md (aiVersion "7").
2026-10-18 12:16:40 +07:00
dotCooCoo 4eef9b7e8c EPS and .ai import: the note for editing data of another page size gives both sizes and the scale
When the editing data of an EPS or .ai file was for a page of another
size than the file's own page, the file opened as its page or its PDF
part with the reason "its editing data is for a different page". The
reason did not say which page the editing data was for, or that it was
the same page at another scale.

The reason now gives both sizes in points: "its editing data is for a
600 × 400 pt page, not the file's 612 × 792 pt page". When the two
pages are in proportion (within the 0.5 pt per side of the check), it
gives the scale of the editing data's page to the file's instead:
"its editing data is for a 100 × 100 pt page, the file's
1000 × 1000 pt page at 1:10". Every catalog that had the old reason
has rows for both new ones.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-18 12:15:39 +07:00
dotCooCoo aafe102649 .ai import: a file whose editing data can't be decompressed says why its layers weren't read
A PDF-compatible .ai, or a PDF saved with the editing data, whose
editing data can't be decompressed (damaged zlib or Zstandard data, or
data that decompresses to more than 256 MB) opened from its PDF part
with no note about its layers. layered_ai returned on the
decompression error without a note. Data that decompresses to layers
that can't be read gets one, and so does an EPS.

layered_ai now gives the same note with the decompression error as
its reason: "the file's layers weren't read from its editing data (its
zlib compression is damaged): they come from its PDF part". The note
counts as a loss, so document.export or Save As over a PDF saved with
the editing data asks first (acknowledgeLoss), as it already did for
a .ai. The three reasons have rows in the 17 catalogs that translate
every message.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-18 12:15:32 +07:00
skylar taylor-barrick 299a4b36ad Merge pull request #2777 from skylartaylor/open-original
Open originals and linked files only on this computer or the document's share
2026-10-17 15:45:22 -06:00
dotCooCoo a03583fc73 Save for Web: saving and Preview in Browser warn when an image couldn't be decoded
document.exportForWeb renders the artboard or the art and writes GIF,
JPEG, PNG-8 or PNG-24 files. When the art held an image that the
renderer could not decode, the renderer drew nothing for it, and the
files lacked the image with no message: the reply had no warnings at
all. A raster export of the same art warns that "an image couldn't be
decoded and is missing from the picture".

document.exportForWeb and document.exportForWeb.preview now compare
decode_failures before and after they render, as raster exports do,
and their replies carry that text in `warnings`. The files are still
written. Save, Download and Preview in Browser in the Save for Web
dialog show the first warning in the status bar as Save As shows a
save's notes.

Docs: docs/mcp.md (Save for Web), docs/control-protocol.md.
2026-10-18 01:32:01 +07:00
dotCooCoo 598f1de3cc Character and Paragraph Styles: a new style's name is trimmed as a rename trims it
charStyle.new and paraStyle.new kept the name they were given as it
was, while charStyle.rename and paraStyle.rename trim the white space
around the new name. So a script could make a style " Body" beside
"Body", and a blank name was refused with the message for a name that
is taken.

New Character Style and New Paragraph Style now trim the name as rename
does, compare the trimmed name with the styles there are, and refuse a
name that is then empty with rename's message. Duplicate names the copy
from the trimmed name. symbol.new and symbol.duplicate trim the name as
symbol.options trims newName, and a blank newName gives "<name> copy"
instead of a symbol named with the blank. library.use adds a library's
text style under its trimmed name, so a library file whose style is
named with spaces around it still applies.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-18 01:32:01 +07:00
dotCooCoo 3fe8aa7db9 Brushes, patterns and artboards: new and renamed items have their names trimmed
brush.new, brush.duplicate, object.pattern.make,
object.pattern.saveCopy, artboard.new and artboard.setProps kept the
name they were given as it was, and brush.options and pattern.options
kept the white space around a new name. So a script could make a brush
" Nib" beside "Nib", and a blank name made a brush, a pattern or an
artboard named with the blank. Artboard Options sends its Name field
as typed, so clearing it named the artboard "". The Brush Options
dialog, the Pattern Options panel and the Artboards panel trim the
names they send.

The eight commands now trim the name, and the brush and pattern
commands compare the trimmed name with the names there are. In
brush.new, brush.duplicate, object.pattern.make,
object.pattern.saveCopy and artboard.new, a blank name gives the name
the command gives when there is none: "New Calligraphic Brush" and the
other default brush names, "<name> copy", "New Pattern N",
"<pattern> copy" and "Artboard N". brush.options and pattern.options
keep the name when the new one is blank, and artboard.setProps gives a
blank one the artboard's default name, Artboard N for the Nth
artboard. Duplicate Brush and Save a Copy name the copy of a brush or
pattern whose saved name has white space around it from the trimmed
name (" Old " gives "Old copy").

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-18 01:27:14 +07:00
dotCooCoo 2ccb147403 Variables: Import Data Sets… defines the variables a file's header names
dataset.import refused every header cell that wasn't the name of a
variable the document defines. A data source file could only fill
variables made beforehand, and `#` and `@` in a header were read as part
of a variable's name.

A header cell that names no variable now defines a new, unbound
variable in the import's undo step. `#Shown` defines a visibility
variable `Shown`, `@Photo` defines an image variable `Photo`, and a
cell without a prefix defines a text variable. A prefixed cell whose
name is already a variable of the prefix's kind names it. A cell that
is a variable's exact name keeps naming it, even when the name starts
with `#`, `@` or `%`, so files that dataset.export writes import back
as before. The import fails, naming the column, on a prefixed cell
whose name is a variable of another kind and on a `%` cell that names
no variable (graph data files). While the variables are locked, a
header that would define a variable fails the import with "variables
are locked", as New Variable does. The reply's `defined` lists the new
variables.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-18 01:27:13 +07:00
dotCooCoo a12f5b0f20 Export: Save for Office, CSS and Copy warn when an image couldn't be decoded or a font is missing
Save for Office Documents, the pictures that css.export rasterizes and
the PNG and PDF that Copy offers other apps were encoded with
fileio::encode, which drops the encoder's warnings. When the art held
an image that the renderer could not decode, the PNGs lacked it; type
in a font that is not installed came out in the fallback font in the
Office PNG and Copy's PNG and PDF. No reply or status message said so.
The app also dropped the warnings that document.exportSelection
returns.

document.exportForOffice, clipboard.exportPng and clipboard.exportPdf
now return `warnings`, and css.export adds the notes of its pictures to
its `warnings`, each once. Save for Office Documents and
document.exportSelection show the first note in the status bar in Save
As's message, and the CSS Properties panel's Export Selected CSS… and
Export All… show it in Export As's message. After Copy or Cut, once
the PNG and PDF are on the system clipboard, the status bar shows their
first note. The files are written as before.

Docs: docs/mcp.md (Clipboard, CSS Properties), docs/control-protocol.md.
2026-10-18 01:27:13 +07:00
dotCooCoo 919970a467 Export for Screens: exports warn when an image couldn't be decoded or a font is missing
Export for Screens wrote each file with fileio::encode, which drops the
encoder's warnings, and its replies had no `warnings` key. When the art
held an image that the renderer could not decode, or type in a font
that is not installed, the files lacked the image or used the fallback
font, and neither the reply nor the status bar said so. document.export
returns the same notes in `warnings`, and Export As shows the first in
the status bar.

document.exportForScreens and assets.export now collect the warnings of
every file they export, each once, and every reply carries them in
`warnings`. The export still writes or returns every file. When there
are warnings, the Export for Screens dialog and the Asset Export panel
show the first in the status bar as Save As shows a save's notes.

Docs: docs/mcp.md (Export for Screens), docs/control-protocol.md.
2026-10-18 01:27:13 +07:00
dotCooCoo 6b13dd776b Export: names typed into file names follow Windows' naming rules
Export for Screens, Batch over data sets, Save for Web and the
Libraries panel name files after text the person typed: a prefix, a
suffix, a name pattern, the name of an artboard, asset, layer, slice,
data set or library, or the document's title. The prefix, the suffix,
name patterns and Batch's data set names had only /, \ and : replaced,
so on Windows a name holding <, >, ", |, ?, * or a control character
failed to save, and Save for Web without a path named its files after
the document's title as it was. A row folder name could end in a
period, which Windows does not allow, and names such as CON, NUL, COM1
or LPT1, which Windows reserves for devices, were written as they
were.

file_part, which Export for Screens' prefix and suffix and Batch's
data set names use, now writes all of those characters as -, and name
patterns use it in place of their own copy. A new safe_file_name runs
a whole file or folder name through file_part, removes trailing
periods and spaces, and adds - after a device name that is the whole
name or comes before its first period (CON.png becomes CON-.png).
Export for Screens uses it for each file, folder and zip it names, and
keeps item names distinct after it. Save for Web uses it for slice
images and, without a path, for files named after the title. Library
files use it too. This happens on every platform. Other names are
written as before, except where a changed name now equals another
item's name, which then gets a number added.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-18 01:27:13 +07:00