With one Objects on Path group selected whose objects have room to move,
the Selection and Objects on Path tools show a diamond below the first
object. Dragging it moves every object along the path together, keeping
the distances between them (object.objectsOnPath.options with start),
in one undo step; Escape cancels it, and the first object's distance
along the path shows next to the pointer.
On an open path the widget shows only when the distance from the first
object to the last is shorter than the path, and the objects stop at its
ends; on a closed path they go on round across the first anchor. Where
Auto or a Space value the open path is too short for sets the distance,
the drag also sets spacing to the distance as drawn.
`inScopeNamespaces()`, `namespaceDeclarations()` and `namespace()` walk
from the node up to its root. The walk cloned each ancestor's whole node,
children and attributes included, to read its namespaces, and charged
fuel only for the namespaces it found, so a deep tree with no declared
namespaces cost no fuel. It now takes one unit of fuel per ancestor,
copies only the namespace list and parent link, and checks prefixes
already seen with a set. `namespaceDeclarations()` compares the node's
declarations with the ancestors' through a map instead of a scan.
The Batch dialog showed File Name for every destination when Play was
Data Sets, though only Destination Folder writes a file named from it.
The row now shows after Destination Folder for Data Sets with a folder
destination, and the dialog sends datasetFileNaming only then. The
chosen value stays in the dialog while another destination is picked.
action.batch and its default for an omitted datasetFileNaming are
unchanged.
links.list, links.check, links.info and document.open's missingLinks,
modifiedLinks and updatedLinks rows no longer give an automation client
with read roots the path of a linked file outside those roots. path and
found are left out of the rows, and links.info also leaves out link,
location and the file size and date the document stores. A relative path
is taken from the document's folder. Each row keeps its id, file name and
status, and the document's links are not changed. Without read roots the
replies are as before.
Find in Folder now matches a search's rows by their ids, comparing paths
only when both rows have one, so it still shows the files found for a
row whose path was left out.
A PDF/A file at PDF 1.4 has no UserUnit, so a page longer than 14,400 pt is written in points with or without longPagesInPoints. The param text, docs/mcp.md and the exporter comment now say that only PDF/A files at PDF 1.5 and later keep the UserUnit.
- `File.encode` and `Folder.encode` leave `/` unescaped, as the File
object reference lists it among the characters kept
(https://extendscript.docsforadobe.dev/file-system-access/file-object/).
- `exists` is true for a `File` only when the path names something
other than a folder, and for a `Folder` only when it names a folder.
- Fields of the records the app returns are read as own properties, so
a property a script puts on `Object.prototype` does not turn a
successful `open` into a failure.
- `File.encode`, `File.decode`, `File.isEncodingAvailable` and the
`Folder` locations count as file calls and charge the host-call limit.
- On the web the locations are `null` and `File.tempName` names a file
at the root, without asking the platform for a temporary folder.
PDF/A-2 limits page boundaries to 14,400 units (ISO 19005-2, 6.1.13), and
PDF 1.4 has no UserUnit. A PDF/A-2b export at PDF 1.4 with a page longer
than 14,400 pt was written in points, with a MediaBox over 14,400 units,
whether or not longPagesInPoints was on.
Such a file is now written as PDF 1.6 with a UserUnit and a warning that
names the version, as PDF 1.5 files already are. PDF/A-2 files may declare
PDF 1.0 to 1.7. Pages past 32,767 pt that aren't whole numbers of points
are written too, since the refusal applies only to PDF 1.3 and 1.4 files.
Files whose pages are all 14,400 pt or shorter stay PDF 1.4.
Advanced Character Formats in Character Style Options and New Character
Style gains Tate-chu-yoko Up/Down and Left/Right fields, in points from
-1296 to 1296, which set the style's tcyUpDown and tcyLeftRight as the
Character panel menu's Tate-chu-yoko Settings sets them on text. An empty
field leaves the attribute unset, an explicit 0 stays distinct from
unset, a value outside the range is an error, and the section's Reset
Panel clears them with its other attributes. Text that uses the style
takes the values and keeps its local overrides.
The floating panels' windows hide while a dialog, a menu, a tool flyout or the
All Tools drawer is open, but the commands went out only when that state
changed. A panel floated meanwhile opened shown at its topmost level, over
the dialog or drawer. A window that floats while the others are hidden now
gets the hide commands in its first frame.
When the room left in the result limit is too small for the lines of a
curved segment to stay within the tolerance, the loop stays as it is
instead of taking fewer lines that can pass it.
New Color Group looked up and removed each named swatch with a scan of
every swatch list, so making a group from artwork linked to many global
swatches took time quadratic in their number. Delete Swatch, Move and
Merge did the same, and checked each paint's link against the name
list.
take_swatches now finds every named swatch in one pass and removes them
together, in the order and with the same choice of swatch as removing
them one at a time. The commands look names up in one name map and
check links against sets.
With 16,000 named swatches in a release build, New Color Group went from
175.9 ms to 16.3 ms; 4,000 went from 11.6 ms to 3.6 ms.
The test now focuses the + button, opens its menu with Enter, and adds a
tool from the menu with Enter, as the docs describe, instead of checking
only that the button takes the focus.
When a flat closed loop becomes the lines between samples of its curved
segments, the line segments it keeps now count toward the 2,097,152-line
limit before the rest is shared among the curved segments. When that
leaves no line for some curved segment, the loop stays as it is.
A light filter that reports its progress kept a failed preview's
interaction open, so OK ran it again on the UI thread without progress
or Cancel. A failed preview is now rolled back and the run goes to the
background.
When a layer effect moves a sublayer's ruler guides onto the layer, a
guide inside a hidden or locked group now stays hidden or locked, as it
does for a hidden or locked sublayer.
Under the read roots, every control reply now shows a hidden library's
id as loaded/<file name> and its path as the file name in every string
and key: status text, dialogs, the command palette query, a script's
report, recorded steps and errors. The id and path are replaced at any
boundary, so a path between spaces, brackets or quotes, followed by a
period or by more path text (<path>.bak), or quoted with escaped
backslashes is hidden too. An occurrence that continues into a longer
path stays only when the request may read that longer path, so a
readable library's id that begins with a hidden one keeps its text.
The libraries hidden after the request ran are hidden, and for a
request that names one, those hidden before it too. A reply with no
path separator in it is not searched. Encoded file data (dataBase64,
pngBase64) is sent unchanged.
Recorded steps in ui.inspect leave out a path outside the read roots
under path, paths, file, files, folder and any param whose name ends in
Path, Paths, File, Files or Folder (diffPath, fontFiles,
destinationFolder), and an absolute path or file: URL outside the roots
under any other param or in any list. The file: scheme is matched in
any case, a URL's %XX escapes are decoded before the check, a file: URL
inside the roots stays, and a file: URL that
names another host is always left out. file_access::is_path_key holds the list of
path params, and testkit's is_path_key uses it.
With Real-time Drawing off, a tool stores its latest preview and applies
it only when the drag is released. A command that ends the drag first
(Undo, Redo, a filter that calls commands, a plug-in command that calls
commands, and the other callers of settle_interaction) committed the
interaction without that preview, so the drag was dropped: Undo then
took back the step before it, and a calling filter was given the
objects where they were before the drag.
settle_interaction now applies such a preview first, as the tool's own
commit does, and cancels the drag when the preview fails. The re-prepare
of a calling filter's input after settling is now reached, and tests
cover it at both plugin.run and plugin.<plug-in>.<command>.
release.yml also runs nightly on main with craft-common-actions' dry-run: every release build
except macOS, with nothing signed, pushed or drafted. Breakage that only the release build hits
(riscv64, Flatpak, FreeBSD, packaging) then shows up the next morning instead of on release
day, as it did on 11 Nov 2026.
When plugin.install can't write or remove a grant and the write that puts
the old module back reports a failure, install now reads the stored module
again and turns the plug-in on only when the store holds the new module. A
put-back that wrote the old bytes and then failed (for example at its last
flush) leaves a disabled plug-in disabled. This applies to modules that
list permissions and to modules that don't.
The saved throw sites of running finally blocks are kept in rising slot
order, so leaving a frame or a block now removes entries from the end of
the list and stops at the first one below the slot. Before, every return
looked at every saved site, so a call made inside many nested finally
blocks took time in proportion to their count.
Opening a .psd or .psb used to give one merged image. document.open now
reads the layer records and builds the document from them. The PSD
import options page
(https://helpx.adobe.com/illustrator/desktop/add-and-import-files/import-from-other-apps/photoshop-import-options.html)
says the option that converts layers to objects keeps as much of the
layer structure as it can, merges the layers of a clipping mask into one
layer and discards hidden layers that require merging, and that Import
Hidden Layers brings hidden layers in. document.open does the same, with
hiddenLayers: true for the hidden layers; layers: false keeps the merged
image.
VectorCraft's own rules:
- Each layer becomes a layer holding its pixels as an image, with its
name, visibility, opacity and blend mode. A group becomes a layer of
sublayers; a Pass Through group is not isolated.
- Clipped layers show where the layer below has pixels. That layer's
fill opacity fades only its own pixels. Layers clipped to an
adjustment layer, or to a fill that stores no pixels and isn't drawn,
show over the whole canvas.
- A layer mask becomes the layer's opacity mask, its default color and
invert flag applied; a disabled mask is kept, switched off.
- Fill opacity multiplies the layer's opacity.
- Layers and groups in Dissolve, Linear Burn, Linear Dodge, Vivid Light,
Linear Light, Pin Light, Hard Mix, Subtract, Divide, Darker Color and
Lighter Color are merged with the layers below, whether or not they
hold transparent pixels; Color Dodge, Color Burn and Difference keep
their mode. A merged Dissolve layer's pixels are opaque or
transparent, in a pattern of each layer's own.
- A solid color fill layer that stores no pixels and has no vector mask
comes in as its color (RGB, CMYK, Grayscale, Lab or HSB).
- A layer name stored only as a Pascal string, without its Unicode
name, is read as Mac Roman.
- Adjustment layers, fill layers that aren't drawn, layer effects that
are switched on, knockouts and vector masks are not applied; the
warnings count them.
- A merge whose canvases need more memory than allowed leaves the layer
alone, in the Normal mode or unclipped, with a warning.
This changes what opening a layered PSD does. Batch Trace Folder passes
layers: false, so a PSD is still traced as its merged image. A file
whose layers can't be read, or whose layers and merges need more memory
than allowed, opens as the merged image with a warning.
Lengths, counts, nesting, decoded pixels and the canvases merges make
are bounded. Zipped 32-bit data with prediction is now read, in layers
and in the merged image. import_fuzz gains a case that mutates the PSD
and PSB samples and opens them with their layers. The PSD reader's new
error messages have rows in every catalog.
Docs: docs/mcp.md, docs/target-app-parity.md, docs/file-format-parity.md,
README.
vectorcraft-js and Check Script took identifier categories from
unicode-general-category 1.1.0, which carries Unicode 16.0, so letters
and marks assigned in Unicode 17.0 (U+088F at the start of a name,
U+1ACF after its first character) were refused. Both lexers now read
general categories from unicode-properties 0.1.4 (Unicode 17.0), which
Cargo.lock already holds. The version is exact because 0.1.5 adds the
Unicode-3.0 license, which is not on the license allowlist. The
Other_ID_Start, Other_ID_Continue and Pattern_Syntax lists are the same
in PropList 17.0.
The exhaustive identifier test now expects the DerivedCoreProperties
17.0 totals (145,916 ID_Start, 149,240 ID_Continue), and new tests read
U+088F and U+1ACF written out and as escapes in both lexers.
The legacy .ai writer resolved cap-height and other font height
references to em sizes before fitting, so Dynamic Text stepped and
limited the em size instead of the size the Character panel shows, and
the type could be written smaller than it is drawn. It now fits first,
and outlines come from the type's own layout.
The editing data now also warns that Dynamic Text is not written when
the type is written as outlines.
Type > Dynamic Text (type.dynamicText) now also works on area type as
Block. Each line as typed (each paragraph, and each forced line break in
one) is set to the frame's width, less the inset, the column gutters and
the paragraph's indents, by changing its font size only. Lines are not
broken again. The first character's size moves in 0.1 pt steps within
0.1 to 1,296 pt, and the line's other characters keep their proportions.
The sizes are written into the characters and fitted again after every
edit to the text or the frame, in the same undo step.
- Word Gap (0 to 1,000 % of a word space, default 100) sets every
paragraph's Desired word spacing; Line Gap (-1,296 to 1,296 pt,
default 0) is added to each character's size for its leading. Both are
VectorCraft's own ranges and defaults, saved in the area options only
when they differ from the defaults.
- Area Type Options gains a Block fit (text.areaOptions fit "block",
with wordGap and lineGap), and the Properties panel's Paragraph section
shows a Dynamic Text checkbox with Word Gap and Line Gap rows.
- Block replaces Auto Size and Shrink Text to Fit. Off keeps the sizes.
Threaded area type is refused, as for type on a path.
- Empty lines keep their size, a frame with no width changes nothing,
vertical type fits each line to the frame's height, and right-to-left
lines and list markers are fitted like other lines. A refit runs at
most four extra layouts.
Docs: docs/mcp.md, docs/target-app-parity.md.
The "Evidence: what ships today, by area" list in
docs/target-app-parity.md carried three lines twice, each copy
byte-identical: Window › Arrange › Tile, the renderer's path cache for
moved anchors, and the plug-in instruction budget at a bulk memory
instruction. One copy of each is removed. The budget line stays next to
the bulk memory fuzz entry; the other two stay at their first place.
Resetting Folder.current for the next run of a named engine now drops the
last run's uncaught exception and $.error first, so memory only they held
no longer makes the reset fail with the heap limit on every later run.
A File an earlier run left open now counts as closed in a later run:
setting its encoding stores it for the next open, and reading the encoding
gives the stored one.
docs/mcp.md: engines stay on the thread that made them, and a
#targetengine line in a file $.evalFile runs makes that call throw a
SyntaxError when it runs.
With the macOS menu bar, File › Scripts was read only at startup and
when the window became active, so a script added while the window
stayed active did not appear. The native menu now reports a menu
opening (Event::Opened, when AppKit begins tracking), and
native_menu::run reads the folder again, as opening the in-window File
menu does. Menu updates wait until tracking ends, so the new listing
shows the next time the menu opens.
Docs: docs/mcp.md (File › Scripts), docs/target-app-parity.md.
longPagesInPoints now applies only to files without a standard. PDF/A-2 limits page
boundaries to 14,400 units (ISO 19005-2, 6.1.13), so a PDF/A file with a page longer than
200 in keeps its UserUnit whether the option is on or off. The Save PDF dialog disables the
checkbox for PDF/A as it does for PDF/X.
Follows #6643.
The harakat test now runs on the in-memory Arabic test font in every
environment, as well as on an installed Arabic face, and checks that
the line is the space longer.
- A layer with a Pathfinder effect, or a Transform effect that copies its
art, keeps the ruler guides of its sublayers on the layer, hidden and
locked as their sublayers were, instead of losing them with the art.
- Opening a file counts the ruler guides of sublayers removed beyond the
canvas.
- Opening a file moves misplaced ruler guides above the art before the
canvas check, so a guide saved before a clipping path beyond the canvas
no longer makes other art the clipping path.
Follows #6632.
paint_only ignores kerning_before, as drawn_alike does: a space before
a line's first letter, set on the letter alone, no longer splits it from
its harakat coloured apart.
malformed_ids_are_refused_by_commands_that_default_to_the_selection put back
the document, the selection and the undo history after a case that changed
them, but not isolation mode or the opacity mask view. If object.isolate
entered isolation mode while refusing a bad id, the test did not report it,
and the later transparency.editOpacityMask and transparency.viewOpacityMask
cases failed with "exit isolation mode first" instead.
Each case now also compares isolation mode and the shown mask before and
after the command, and puts them back when they changed, so a failure is
reported against the command that caused it.
On X11 the last clipboard handle dropped hands what the app copied to
the clipboard manager. When the paste check's thread was still waiting
on a clipboard owner that had not answered, quitting dropped the app's
own handle first and waited 250 ms at most for the check's handle, so
the process could exit before the copy reached the clipboard manager.
- VectorcraftApp declares clipboard_probe before services, so the paste
check stops and lets go of its handle before the app's handle drops.
- Dropping the paste check waits up to 5 s for its thread, longer than
arboard's 4 s X11 read wait.
- New test quitting_drops_the_probes_clipboard_before_the_apps.
Follows #6651. When the refits of a closed subpath enclose no area,
Simplify splits its curves until straight lines between the pieces
enclose one. That split stopped at 65,536 pieces in all, and a loop
with many anchors then came back as the flat refit, with no outline.
- Only curved segments are split; line segments stay one piece each.
- When no split within the limits encloses an area, the loop stays as
it is. With Convert to Straight Lines, each curved segment becomes
the lines between its samples instead.
The docs said every livePaint key left out is reported as missing. livePaint.info reads `id` when `group` is left out or null, and reports a missing group only when both are left out.
A plain Save that the location refuses (read-only filesystem or permission denied) now opens Save As with the name filled in, instead of ending in an error dialog. The refusal travels as a typed engine error (READ_ONLY_LOCATION), so the control protocol and MCP still return an error and never open a panel. Recovery failures, explicit destinations, a busy picker and Close/Quit keep their existing flow.
Closes#6146
* fix(svg): first element of an id, shared linked file reads, bounded blank lines
* address review: cache failed linked-image reads but give each href its own placeholder and link
* address review: charge only blank lines to the line-break budget
* address review: a collapsed line break keeps its measured leading in tiny type
* address review: blank lines use the same scaled leading tolerance
- drawn_alike ignores kerning_before, so a space before the "f" of "fi"
no longer splits the ligature.
- SVG export takes a line's starting space from kerningBefore and the em
shaping sets the character in (vectorcraft_text::line_start_space),
not from where the glyph is drawn, which can include a shaping offset;
the reader layout for justified lines leaves the space out, so those
lines start there too.
- Type on a path writes the space as the first character's dx; a space
after a line break, which <textPath> can't write, is outlined.
Release Type on a Path copied the stored runs into point type with the
Dynamic Text flag still set, so the text dropped back to its stored size
and the file saved the flag on point type. The point type now gets the
drawn sizes and the flag is turned off.
The legacy .ai writer writes Dynamic Text type at its drawn sizes; it
now also warns that the setting itself is not written, as the editing
data writer does, whether the type is written as text or as outlines.
docs/target-app-parity.md lists reshaping or scaling the path among
VectorCraft's own rules, and a task bar comment marks leaving the item
out for threaded type as VectorCraft's own rule.
When plugin.install writes a module that lists permissions, its grant
can't be written, and the old module can't be put back (or, on a first
install, the new module can't be removed), the store holds the new
module. install now takes put_back_plugin's result, so the plug-in's
disabled entries (by id and by stored file) are cleared as for any
install the store holds. The old grant, or none, still doesn't cover the
new module, so the next start skips it until it is approved.
Docs: docs/plugins.md, docs/mcp.md, docs/target-app-parity.md.