With the Selection tool on several frames of one threaded story, the
Tabs panel listed the paragraphs to change in the order the frames were
selected, and listed each frame's paragraphs separately. A marker drag
over mixed indents then ran its per-paragraph steps out of story order,
so a step that made text reflow could move a later step's paragraph,
and a paragraph that runs on from one frame into the next got two
steps. After Type tool Select All the split paragraph also got two.
The panel now lists each paragraph of a story once, in story order,
by its last piece with text in a selected frame. With the Selection
tool only the paragraphs with text in a selected frame change, so a
frame selected alone that starts or ends inside a paragraph sets it.
Docs: docs/target-app-parity.md.
object.vectorHalftone read frequency, angle, invert, clip and
keepOriginal with readers that fall back to the default for a value of
the wrong type, so {"frequency": "60"} drew a 20 lpi screen. shape and
mode given as a non-string also fell back to circle and mono. Each of
these is now an error and the document is left as it was. A null
value, color included, still stands for the default.
Seventeen @msg rows in hi.tsv and one in tr.tsv translated or dropped
the command id the English names outside backticks. For example,
"missing `id` (see plugin.list)" read "(information.list)" in Hindi, and
flattener.presets.list became "düzleştirici.presets.list" in Turkish.
The rows now keep each id as written. Several of the Hindi rows also
said the opposite of the English ("a profile is called" for "no profile
is called") and are corrected.
A new test checks every catalog: each command id a status or error
message names (dot-separated lowercase words outside backticks) appears
unchanged in its translation.
Closing a path by dragging on its first anchor made that anchor smooth
with symmetric handles, so the first segment bent along with the closing
one. The drag now sets the first anchor's incoming handle to the mirror
of the pointer. Its outgoing handle stays where it is on a corner, so
the first segment keeps its shape, and on a smooth anchor it turns to
stay in line at its own length. Alt makes the anchor a corner and leaves
the outgoing handle where it was when Alt went down. A click on the
first anchor closes the path with the incoming handle retracted and the
anchor a corner, so the closing segment comes in straight.
The Smart Pen's closing drag follows the same rule.
path.close's `in` now places the handle as path.setHandle does, and a
new `out` param places the outgoing handle and makes the anchor a
corner.
Extrude & Bevel (both kinds) and Inflate drew the faces of the fill's
solid and the stroke's solid in one order by each face's center depth.
At a concave corner a long fill wall could sort after a stroke wall in
front of it, so stripes of the fill showed through the stroke.
- Facets now record whether they are side faces (walls, bevels,
Inflate's sides) and whether they are turned away from the viewer.
- With caps on, each fill side face turned toward the viewer is drawn
before every face of the same shape's stroke whose bounds overlap
it, for strokes whose solid hides the fill's whole edge: a closed
shape, a width, no dashes, width profile or brush, and a solid color
or a gradient without transparent stops. Caps, other shapes in a
group, hollow effects and translucent or blended surfaces keep their
order.
Docs: docs/three-d.md (Fills and strokes), docs/target-app-parity.md.
every_command_survives_empty_params and
read_only_commands_read_the_instance_of_a_picked_object run every command
with {}. That includes plugin.clearCache, which without an id empties the
cache of every plug-in installed in the process. When either sweep ran
during plugin_storage_is_capped_and_survives_a_damaged_settings_file
(called from plugins_keep_settings_and_a_cache_of_their_own_in_the_store),
it deleted that test's cache entries Tile and tile. The test's
st.clear_cache() == MAX_CACHE_BYTES + 3 assertion after the
MAX_CACHE_SCAN folders then failed with 536870912 bytes freed.
Both sweeps now leave plugin.clearCache out.
tests/plugin_clear_cache.rs already runs it with {} in a test binary of
its own. A new test runs the other plug-in commands with {} in the
selection states that every_command_survives_empty_params uses, and checks
that an installed plug-in's cache keeps its 3 bytes.
The gradients category was titled Gradient Swatches and listed every
gradient swatch, used or not. It is now Gradient Objects: one row per
object filled or stroked with a gradient, named as the object is, with
the gradients it uses (a linked swatch by name, else Linear gradient,
Radial gradient or Freeform gradient). document.info adds
gradientObjects and gradientNames, and Selection Only, Save and the
Package report use the same rows.
Gradient objects follow the rules Pattern Objects uses for paint that
draws: hidden fills and strokes, strokes with no width, guides, compound
path members' own paint, image paint without a shape and type that draws
nothing are left out, and type whose characters carry a gradient is
included. objects.gradients now counts the listed objects, so it no
longer counts hidden or zero-width gradient paint and no longer misses
type whose characters use a gradient.
The duplicate Document Info Save line in docs/target-app-parity.md is
removed.
A `File` or `Folder` for a missing path got "does not exist" even when
the path led outside the script's folder, through a folder link or, on a
file system where case matters, through a folder whose name differs from
the script's folder only in case, while an existing file there was
refused. A missing path is now refused unless the nearest folder above
it that exists lies inside a granted folder.
In a frame that is not a rectangle, or with text wrap, layout moves a
line down to the first span wide enough for its first word. The word
was measured at full width before Mojikumi set an opening bracket at
the line's start flush, and with its last character at full width, so
a word that fit as set moved down to a wider line. It also ended at any
break opportunity, so an opening bracket counted without the character
kinsoku keeps with it and could be placed alone on a line too narrow
for that character.
- word_width ends the word only where break_line may end a line: at a
cluster end that kinsoku allows, not inside a No Break run, whose
spaces count. It leaves out the spaces at the word's end and narrows
its last glyph by the Mojikumi line-end credit.
- place_flow_line measures the word after the opening-bracket trim, and
the Every-line Composer's first-line placement takes off the same
start credit, so the composed widths match the placed lines.
With Line-end Punctuation Half Width, a closing bracket, comma or full
stop ending a line is set half width, but both composers measured it at
full width. A closing bracket that fit only at half width went to the
next line with the character before it, and so did a comma or full stop
with Burasagari None.
The single-line and every-line composers now measure each candidate
line with its last character set as narrow as Mojikumi sets it, using
the same rule the line is drawn with. A mark that already lost its half
to the punctuation after it and a line ending in a hyphen take nothing
more off, and the quarter em between Japanese and Latin text counts only
where the line ends. Kinsoku Shori Type measures the pushed-in and
pushed-out lines the same way. Right-to-left lines and Mojikumi None
break as before.
A test font sets ideographs and Japanese punctuation one em wide, so
the layout tests run without system fonts.
When the interface thread stays in one part of a frame for 3 s, a
watching thread writes a hang report to the log and to
logs/hang-report-<process id>.txt. The report gives the part of the
frame (input hook, logic, interface, or the renderer after a frame in
which any window had input), a system clipboard call in progress, the
last 8 different command ids, the last 16 frame times, the platform,
window system, desktop and session type, Flatpak, graphics adapter, the
present mode and frame latency the app asks for, the running time and
the number of open documents. It holds no document contents, file paths
or command parameters. The interface thread marks its stages with a few
atomic stores a frame, and the watching thread reads those marks without
waiting on it.
The process keeps the report file locked until the interface answers
again, then removes it. A file nobody holds at a later launch was left
by a session that was closed while it did not answer: the launch writes
the report to the log, moves the file to logs/hang-report.1.txt and,
unless another start-up message is showing, says so in the status bar.
Help › Copy Troubleshooting Info puts the platform facts, the recent
commands and frame times and both sessions' reports on the clipboard for
a bug report. An in-line file dialog, an open menu of the macOS menu bar,
and the time after a frame that minimizes or hides the window or starts
moving or resizing it are not timed.
The menu label and the two status messages have rows in every catalog.
Since #5839 the Rectangle, Rounded Rectangle, Ellipse, Polygon and Star
tools draw the selection's bounding box with its eight handles, but a
press on a handle drew a new shape from it. A shape just drawn could not
be resized without switching to the Selection tool.
A press on a handle now scales the selection as the Selection tool's
handles do: the same cursors, snapping, Shift and Alt, Use Preview
Bounds, area type frames and Backtick for patterns. The box stays on
show at the changing bounds during the drag. With the bounding box
hidden, a press there still draws.
Docs: docs/target-app-parity.md.
When a second running copy listed the recovery folder, it could lock an
area that this copy had just made but not yet locked, take it for a
crashed copy's empty area and remove it. Locking the area then failed,
and the recovery save reported "No such file or directory". On macOS the
lock could also fail with "Invalid argument" on the removed lock file.
Taking a new area now counts it as busy, and tries the next name, when
its folder or lock file is gone by the time it is locked, when its lock
file is there but cannot be opened (some file systems keep a removed
file that another copy still has open until it is closed), or when the
lock file that was locked is no longer the one in the folder.
Tidying an empty area, and quitting, now remove the area while it is
still locked, and remove its folder once more after letting it go if
the folder is empty.
exact_merge looked at every input curve on each merge attempt, so tidying
the output of a sweep cost the number of input curves for each joint it
tried to remove. It now takes its candidates from Tidy::curves_around,
the bounds index that repair already uses, and tests only the curves
whose bounds hold both ends of the run.
A new test cuts one curve of a 1,000-curve row in three and checks that
the merge rebuilds it while testing at most four curves; before the
change it tested 1,002.
A paste on Linux and the BSDs read the bitmap on the clipboard worker but
waited up to 10 s for it on the UI thread, so a bitmap from an app that
never sent its data froze the window long enough for the compositor to
offer to close it.
The paste now sends the bitmap read to the worker and returns at once;
its background thread waits up to 10 s for the answer and converts it to
PNG. While that read is open, other reads fail at once instead of
queuing behind it. Each read gets its own answer channel, so a late
answer is dropped. The check of whether the clipboard still holds the
app's own bitmap waits 2 s at most, as text does.
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.