Paste read PDF only from the Windows clipboard and EPS nowhere, so on macOS a copied
equation or drawing pasted nothing new (the last thing copied in the app came back).
- macOS: the pasteboard's PDF, EPS and SVG are read, ahead of text and bitmaps.
- Windows: EPS is read beside PDF.
- Everywhere: an .eps file copied in a file manager pastes.
- clipboard.importEps loads an EPS into the clipboard as document.open reads one.
The renderer kept the rasters of an object's drop shadows, outer glows
and filtered content (Gaussian Blur, Feather and the Photoshop-style
filters), and drew them again while the object, the zoom and rotation
and the ink plane stayed the same. The content drawn into a raster also
reads things outside the object: patterns, symbols, brushes, images,
document, layer and color settings, the knockout a group inherits, the
fonts, and the options the frame is drawn with. When one of them
changed, a renderer that had drawn the object went on drawing the
raster it had kept, made from the old values.
Each kept raster now records what its content was drawn from: which of
those things the object reads, found by walking the object when the
raster is made, and a hash of their values then. The renderer draws
the raster only while that hash is the same in the current frame. Once
a frame, the renderer hashes the document's patterns, symbols and
images. It hashes their art and pixels by the addresses of their
shared values, and it keeps weak references to those values while a
raster made with them is kept, so no other value can take one of those
addresses. A filtered raster also records the color channels its
filters split, and under De-Interlace the raster resolution.
A temporary release test timed the inputs of 400 kept rasters at about
10 microseconds a frame.
Opening an EPS whose threaded area type frame has a control point at
x = 8e115 failed with an internal error ("capacity overflow").
`eps_text_frames_never_panic` in `import_fuzz` found it with
PROPTEST_CASES=20000. Text layout flattens a frame to within 0.1 pt,
and for such a curve kurbo 0.13.1's `flatten` calls `reserve` for
`usize::MAX` quadratic pieces. Laying out text around a wrap shape with
such a curve failed the same way. Laying out type on a path along such
a curve panicked in kurbo's `solve_itp` ("attempt to shift left with
overflow") in builds with overflow checks, and in a release build it
did not finish within 20 s. Laying out area type in a triangle frame
that comes to a point at its top, at y = 1e20, did not finish either:
adding a lead to a baseline that large leaves the baseline where it is.
Text layout now leaves out a frame, a text wrap shape or the path of
type on a path that has a coordinate beyond 1,000,000,000 pt either
way, control points included, or a NaN coordinate in an anchor point.
Such a frame or path holds no text, and such a wrap shape is ignored.
PDF export uses the same bound (`MAX_COORD` in
`crates/pdf/src/export.rs`).
Text layout also steps a line down a frame cell at most 100,000 times
to find room for it. In a cell taller than 100,000 leads, a step is the
cell's height divided by 100,000; in a shorter cell, a step is one
lead, as before. Before this change, in a dev build, with triangle
frames that come to a point at their top, laying out text in one
2,000,000,000 pt tall took 9.8 s, and laying out a paragraph whose
Space Before is -1e300 pt in one 1,000 pt tall did not finish within
20 s.
- The type in a text object's drawn look (1 /FreeUndo) is a story of the
undo-free text document, which counts its stories on its own. It was read
as the story of that number in the text document, so a label could show
another object's text in another object's frame. The object itself, which
follows the look, is the type now.
- The page's type of a text object on a hidden layer stays with that object
(hidden) instead of going to the nearest text object that shows.
- The type on the page of a later artboard is matched where the layers have
that artboard, not by the first page's place.
The editing data places a gradient with matrices after Bg: Xm for a linear
gradient's whole ramp and one Bm for each pair of stops next to each other.
The reader kept the last Bm, so a gradient of three stops drew only its last
part. It now uses Xm (or, without one, the span of the Bm's from the first
stop to the last), and a radial gradient's one Bm as before. The angle and
length are in the matrices, so Bg's own numbers are used only without them.
An object's own note of its gradient after it (Bb, Bg, BB with no path) no
longer becomes the fill of the next path.
A .ai file with placed files opened from its PDF part (no guides, no sublayers,
no hidden objects), because the editing-data reader stopped at them.
- Lexer: a placed file's preview section is one token (it was skipped).
- Reader: %AI5_BeginPlace ... ~ becomes a linked image with the file's path,
showing the preview the data keeps of it (premultiplied ARGB, capped at
MAX_PIXELS, its rows checked against the bytes there), in its place: the
matrix takes the file's pixels from the canvas, where they start at the point
of art space's origin. A placed file without a preview, a path or a matrix
still makes the file open from its PDF part.
- The open result lists one warning saying linked pictures show the
low-resolution preview until their files are found (rows in the catalogs).
- Engine: a link nothing is known of whose image shows a preview is read where
it is found as the document opens, into the image's bounds, and isn't
reported as modified.
- Tests: lexer, reader (hand-written data), engine open with the link missing
and found, fuzz tokens.
- Docs: file-format-parity, target-app-parity, gaps, ROADMAP.
Unlocked rectangle corners now each use their own radius and type (top left,
top right, bottom right, bottom left) instead of all following the first one,
and straight corners are cut across instead of rounded. patchy-shapes.af now
matches its embedded thumbnail (mean difference 1.91 -> 0.24) with no warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In builds with overflow checks (`cargo run`, `cargo test` and
`cargo xtask ci`), PDF export aborted the process when a number it
wrote rounded to -2,147,483,648 as an f32. pdf-writer 0.15 writes
-2,147,483,648.0 as an integer, and `Limits::register_int` overflows
when it takes the absolute value. krilla 0.8.2's `Surface` then fails
an assertion in its `Drop` while that panic unwinds.
`svg_hostile_markup_never_panics` in `import_fuzz` hit it with type
shifted 2,147,483,649 pt down. Paths, gradients, dash offsets, images
and artboards that far out hit it too, and so does a group around a
stroke wide enough to reach that far. Release builds have no overflow
checks.
PDF export now leaves out art whose coordinates or transform entries
lie beyond 1,000,000,000 in its own space or in the content stream it
is drawn into, and glyph lines whose spacing lies beyond 1,000,000,000
thousandths of an em. That covers paths and clipping paths, glyph
lines, images, gradients, strokes whose width, caps or miter spikes
reach that far, and the art and marks of a page whose artboard lies
that far, which is written blank. A dash offset beyond the bound is
written as its remainder after whole periods of the pattern.
The type's transform is now pushed inside the form that marks a fill
or stroke of type to overprint. The exporter checks what that form
holds through the type's transform, as it checks the same fill or
stroke without overprint.
A PDF saved with the reference app's editing data keeps that data in
streams named AIPDFPrivateData1, AIPDFPrivateData2, ..., where a .ai
file uses AIPrivateData1, AIPrivateData2, .... illustrator_data read
only the .ai names, so such a PDF opened like any other PDF: plain
groups opened ungrouped, and hidden objects and guides were missing. The
import gave no warning that the PDF's editing data went unread.
illustrator_data now reads the AIPDFPrivateData streams when the file
has no AIPrivateData1, and the PDF opens from its editing data as a .ai
file does. editingData: false still opens it like any other PDF.
has_private_data still looks for AIPrivateData keys only.
layered_ai also returns whether it read the layers. When it did, art
reaching past a PDF's page opens whole, and through_editing_data drops
the PDF importer's note that such art was clipped (PAST_PAGE_NOTE, now
public) as it drops the off-artboard note. When it didn't, both notes
stay. On main, a .ai whose editing data can't be decoded or has no
layers opened from its PDF part without the off-artboard note.
testkit::ai::pdf writes the .ai file of testkit::ai::ai with the PDF
key names. The editing-data tests open it as a PDF, and the import fuzz
test opens the damaged sample as a PDF too.
Docs: the document.open params, docs/mcp.md,
docs/file-format-parity.md, docs/gaps.md (G9: the export direction of
#868 remains), one evidence line in docs/target-app-parity.md, and #868
moved from layer structure to editing data in the File formats row of
docs/target-app-parity.md and in docs/roadmap.md.
History was limited by step count only; a whole-selection move of 50k paths keeps ~40 MB per
step. Steps now carry a size estimate (only unshared subtrees are visited) and the oldest are
dropped beyond a byte budget, keeping the last 8. Documents entering history drop their id
index, selections are shared between steps, the command journal is bounded (held while an
Action records), and document resources are copy-on-write. Saved files are byte-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
The engine's guard turns a panic inside a command into
EngineError::Internal and rolls the document back. The panic checks in
command_sweep.rs counted that error like any other error. They include
probe, which the empty-params, junk-params and structured-junk sweeps
call. The tests in import_fuzz.rs that open, place or paste a file
through a command counted it the same way. None of them reported the
panic.
Each of these checks now passes the command's result through no_panic,
which the library file tests in import_fuzz.rs already use, and the
change adds a copy of it to command_sweep.rs. A case whose command
panics now fails. The panic from no_panic reads "<command> panicked:
<message>", and the checks that print a panic's message show it. Each
check handles any other error as before.
Agents could set and clear the key object but not read it:
document.inspect, the control channel's ui.pointer reply and the
headless pointer_gesture reply left it out, and select.key returned
null. select.key with an id that was not selected, or with a value
that is not an id, such as "x", cleared the key without an error. A
compound-path member or an object inside a selected group could become
the key.
select.key now checks its id as select.set, select.add and
select.toggle do (#784). It returns an error for an id that is not a
selected top-level object and leaves the key as it was. Layers,
compound-path members and objects inside another selected object are
not top-level objects. select.key {id: null} used to clear the key and
is now an error; select.key {} still clears it. select.key returns
{key}, and document.inspect and both pointer replies include the key
object as "key".
In isolation mode a Selection-tool click can pick an object that
select.key does not accept as the key: a sublayer inside an isolated
layer, a member inside an isolated compound path, or an object in an
isolated group that is also selected. The tool no longer sends
select.key {id} for such an object. When the object is already the
key, the tool still sends select.key {}, which clears the key.
A recorded action whose select.key step names an object that is not a
selected top-level object when the action plays now fails and is
rolled back, as an action with a select.set step naming a deleted
object has failed since #784.
Docs: docs/mcp.md, docs/control-protocol.md, docs/target-app-parity.md.
The Anchor Point tool drags any segment of a selected blend's spine into
a curve, straight ones too, and Direct Selection reshapes a curved spine
segment as it does a path's (a straight one it leaves, as on paths). Both
run the new `object.blend.spine.reshapeSegment`, which bends the segment
with the same math as `path.reshapeSegment`, now shared as
`SubPath::bend_segment`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
object.align with neither horizontal nor vertical returned success
and moved nothing. The command palette, and a key bound to it in
Keyboard Shortcuts, run it with no params. It now returns
"give horizontal or vertical", as object.distribute already did.
An action recorded while Align ran from the palette or a bound key
contains an object.align step with no params. Playing that action now
fails at that step, and command.batch rolls back the whole action.
Values outside a parameter's choices were read as another choice or
ignored. Align ignored an unknown horizontal or vertical and aligned
to the selection for an unknown to value. Distribute read an unknown
direction as center. Distribute Spacing and Shear read any axis other
than "vertical" as horizontal, and Reflect read any axis other than
"horizontal" or a number as vertical. A bounds or to value that was
not a string fell back to the default, and a spacing that was not a
number gave automatic spacing. The commands now return an error that
names such a value, before they move anything or record an undo
step. When a to, bounds, axis or spacing value is missing or null,
the command uses the parameter's default: Distribute Spacing without
an axis spaces the objects horizontally.
The run_command description and docs/mcp.md showed object.align with
an align param it does not have; they now use horizontal. The new
status-bar messages have rows in the de, es, fr, it, ja, ru and uk
catalogs.
Docs: docs/mcp.md, docs/target-app-parity.md.
The sweeps `every_command_with_junk_params` and
`fuzzed_calls_keep_documents_serializable` skipped every case that set
`object.scale`'s `sx`, `object.transformEach`'s `scaleH` or `scaleV`,
`object.distributeSpacing`'s `spacing`, or `artboard.setProps`'s `x`,
`y`, `width` or `height`. The comment above `KNOWN_BUGS`, the list of
those (command, param) pairs, referred to `#[ignore]`d repro tests, but
`bug_huge_transform_makes_document_unloadable` and
`bug_artboard_huge_props_break_svg` have no `#[ignore]` and run with the
other tests. Since 42df20ca, the commit that added the list,
`Session::edit` returns an error when an edit leaves an artboard or a
selected object with coordinates that are not finite or exceed
`MAX_COORD`, and `object.transformEach` clamps `scaleH` and `scaleV`.
The list and its two checks are removed, so both sweeps run those
cases.
A .ai in PostScript form went only to the PostScript interpreter, which
stopped on the first operator its missing prolog would define
(`Adobe_packedarray`, `Lb`, `Np`): files from Rhino and other CAD apps name
the procsets without including them.
- Such a program is its own editing data: when it has layers
(`%AI5_BeginLayer`) and no private data, the editing-data reader reads it,
compared with the page when the program runs, from the layers alone when
it can't be run. If the layers can't be read either, the error says why.
- The reader takes the legacy ten-operand `Lb`, ignores flatness `i`, and
uses the bounding box as the page when nothing else says where it is.
- Tests on synthetic files; fuzz fixture and hostile-program layers added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
# ROADMAP.md
# docs/file-format-parity.md
# docs/gaps.md
The editing-data reader stopped on `Xk`/`XK` (global process colours) and
on `p`/`P` (pattern fills), so the file fell back to its PDF part, which has
only the top-level layers.
- `Xk`/`XK` read like `Xx`: a global swatch (not a spot ink), linked at its tint.
- `%AI3_BeginPattern` sections are read by a reader of their own into
pattern swatches (tile from `(name) llx lly urx ury`, art in pattern space);
`p`/`P` fill or stroke with them, placed by the operator's matrix. A pattern
it can't read whole still makes a fill with it unreadable, as before.
- Tests on synthetic editing data; fuzz tokens and fixture extended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Eight raster effects on the pixel-filter pipeline
(vectorcraft_effects::pixel::brushstrokes), under a new Effect › Brush
Strokes submenu between Blur and Distort in the Raster Effects section:
- Accented Edges (edgeWidth pt 1..14, default 2; edgeBrightness 0..50,
38; smoothness 1..15, 5): the edges of the softened object painted
towards white chalk at high brightness or black ink at low brightness;
Smoothness also leaves the faint edges out. The shape stays as it was.
- Angled Strokes (directionBalance 0..100, 50; strokeLength pt 3..50, 15;
sharpness 0..10, 3): light areas in right diagonal strokes, dark areas
in left diagonal ones; 0 makes every stroke a left diagonal, 100 every
stroke a right diagonal. Sharpness brings detail and bristles back.
- Crosshatch (strokeLength pt 3..50, 9; sharpness 0..20, 6; strength
1..3, 1): the detail kept under pencil hatching along both diagonals,
Strength passes of it, the edges roughened.
- Dark Strokes (balance 0..10, 5; blackIntensity 0..10, 6;
whiteIntensity 0..10, 2): short strokes towards black in the dark
areas, long strokes towards white in the light areas.
- Ink Outlines (strokeLength pt 1..50, 4; darkIntensity 0..50, 20;
lightIntensity 0..50, 10): black outlines over the edges, ink hatching
in the shadows, shadows darkened and highlights lightened.
- Spatter (sprayRadius pt 0..25, 10; smoothness 1..15, 5): each pixel
shifted by noise up to 0.4 x the radius; Smoothness makes the grains
larger and smoother.
- Sprayed Strokes (strokeLength pt 0..20, 12; sprayRadius pt 0..25, 7;
strokeDirection Right Diagonal, Horizontal, Left Diagonal or Vertical):
strokes along the direction, scattered by up to 0.3 x the radius.
- Sumi-e (strokeWidth pt 3..15, 10; strokePressure 0..15, 2; contrast
0..40, 16): wet strokes, ink bleeding from the darks along paper
fibres, more ink with pressure and an S-curve contrast for rich blacks.
It works in value only: hues and saturation stay, and so does the
object's shape (no fringe in the transparency).
A stroke averages the colour along a segment through the pixel (16
samples at most, the art softened first when they lie more than a pixel
apart); strokes lie in rows around the object's centre, each row
starting at random and each stroke shifted along itself, with streaked
noise for the bristles. Everything is in document space, so the effects
look the same at any zoom or resolution, move with the object and draw
the same every time; outsets and reaches are exact. On a 2000 x 2000
raster in release the slowest settings take 0.3-2.0 s (Crosshatch at
50 pt with 3 passes the most).
Shared helpers instead of copies: edges::sobel (Glowing Edges now uses
it, unchanged), distort::displace and texture::Tones are pub(super).
Catalogue rows with parameter docs, menu entries, Sprayed Strokes'
direction dropdown, translations (de, es, fr, it, ja, ru, uk, zh-hans,
zh-hant; menu labels in cs and pt-br), tests (parameters and clamping,
outsets and reaches, each filter and option showing, chalk and ink on
the edges only, shadows and rich blacks, Sumi-e keeping the shape and
hues, hatching keeping the tone, strokes smearing along their direction,
Spatter's radius, resolution and move invariance, canvas, multithreaded,
PDF and SVG, the command sweep with junk parameters, the menu) and
ROADMAP.
Behaviour from public documentation only:
https://helpx.adobe.com/photoshop-elements/using/brush-stroke-filters.htmlhttps://www.computerhope.com/jargon/p/photoshop-sprayed-strokes.htm
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three raster effects on the pixel-filter pipeline
(vectorcraft_effects::pixel::distort), under a new Effect › Distort
submenu between Blur and Pixelate in the Raster Effects section (the
vector Distort & Transform stays its own submenu):
- Diffuse Glow (graininess 0..10, default 6; glowAmount 0..20, 10;
clearAmount 0..20, 15): the highlights, softened over 2 points, glow
white from the brightness Clear Amount leaves clear; white grain as
dense as Graininess says is sprinkled over the object, thicker in the
glow. Transparency stays as it was.
- Glass (distortion 0..20, 5; smoothness 1..15, 3; texture Blocks,
Canvas, Frosted or Tiny Lens, default Frosted; scaling % 50..200, 100;
invert): the object seen through a glass surface; each pixel takes the
colour the surface's slope bends it to, sampled between pixels. Slopes
are taken over a span Smoothness widens. The surfaces are made in code
(Canvas is Texturizer's weave); Load Texture is left out.
- Ocean Ripple (rippleSize 1..15, 9; rippleMagnitude 0..20, 9): smooth
noise ripples about Ripple Size + 2 points across shift the object by
up to 0.03 x magnitude x their size.
The shifts are capped, so the effects' outsets and reaches are exact,
and every surface lies in document space around the object's centre:
the same at any zoom or resolution, moving with the object, identical
on every redraw.
Catalogue rows with parameter docs, menu entries, the Texture dropdown,
translations (and German's missing Canvas), tests (parameters and
clamping, outsets, each texture and Invert, the ripples' reach, the glow
and its grain, resolution and move invariance, canvas, multithreaded,
PDF and SVG, the command sweep with junk parameters, the menu) and
ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Six raster effects on the pixel-filter pipeline
(vectorcraft_effects::pixel::texture), under a new Effect › Texture
submenu between Sharpen and Video in the Raster Effects section:
- Craquelure (crackSpacing pt 2..100, default 15; crackDepth 0..10, 6;
crackBrightness 0..10, 9): the object on relief plaster that cracks into
plates about Crack Spacing apart (a Voronoi network over jittered points)
and along the quarter, half and three-quarter contours of its softened
tones. Depth sets how dark and wide the cracks are and how high the
plates' bevels and the tones stand (lit from the top left); Brightness
how brightly the surface is lit.
- Grain (intensity 0..100, 40; contrast 0..100, 50 = unchanged; grainType
Regular, Soft, Sprinkles, Clumped, Contrasty, Enlarged, Stippled,
Horizontal, Vertical, Speckle; default Regular): coloured 1-point grains,
smooth ones, clumps, 2-point grains, streaks along rows or columns, dark
specks; Sprinkles and Stippled use the background colour, white.
- Mosaic Tiles (tileSize pt 2..100, 12; groutWidth 1..15, 3; lightenGrout
0..10, 9): irregular tiles bevelled at their edges, with sunken grout half
a point wide per step, lightened as Lighten Grout says.
- Patchwork (squareSize pt 0..10, 4; relief 0..25, 8): squares filled with
the colour around their centres, raised to heights that follow the
highlights and shadows, a little more or less at random.
- Stained Glass (cellSize pt 2..50, 10; borderThickness 1..20, 4;
lightIntensity 0..10, 3): single-coloured panes leaded in the foreground
colour (black, half a point per step), lit from behind at the object's
centre and fading out towards its corners (PixelSpace gains `extent`).
- Texturizer (texture Brick, Burlap, Canvas, Sandstone, default Canvas;
scaling % 50..200, 100; relief 0..50, 4; lightDirection Bottom, Bottom
Left, Left, Top Left, Top, Top Right, Right, Bottom Right, default Top;
invert): the object on a surface in relief. The four surfaces are height
fields generated in code (bricks in stretcher bond, plain weaves coarse
and fine, octaves of smooth noise); no image files. Load Texture is left
out.
Every pattern lies in document space around the object's centre, so the
result is the same at any zoom or raster resolution, moves with the object
and is identical on every redraw (hash noise of cell indices); slopes are
taken in document units. Work per pixel is bounded (a 5 x 5 cell search,
four or five height samples, box blurs); parameters are clamped to their
ranges and finite-checked. The Pixelate helpers (Place, the cell search,
the softened colours) are shared, Mezzotint uses the shared smooth noise,
and the effect dialogs' dropdowns translate their labels in an "effect"
context where the plain word means something else (Soft, Horizontal and
Vertical are type settings in Japanese and Chinese).
Catalogue rows with parameter docs, outsets and reaches, menu entries,
dropdowns (Grain Type, Texture, Light Direction), translations, tests
(parameters and clamping, cracks and contours, ten distinct grains, tiles
and grout, squares in relief, leaded panes and the centre light, the
textures lit and inverted, the same at twice the resolution and when
moved, canvas, multithreaded, PDF and SVG, the command sweep with junk
parameters, the dialogs and the menu) and ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Double-clicking a Scatter, Art or Pattern brush in the Brushes panel (or the
panel menu's Brush Options...) now opens its options dialog, as Calligraphic
and Bristle brushes already did. OK runs brush.options and every stroke
painted with the brush redraws.
- Scatter Brush Options: Name; Size (10-1000 %), Spacing (10-10000 %),
Scatter (-1000-1000 %) and Rotation (-180-180 deg), each Fixed (the first
value), Random (between the two values) or Pressure (the first value at
the lightest pen pressure, the second at the heaviest); Rotation relative
to Page or Path. Scatter brushes now keep these modes (`modes`); brushes
saved before keep their look (Random where the two values differ).
- Art Brush Options: Name; Width (1-1000 %); Scale Proportionately, Stretch
to Fit Stroke Length or Stretch Between Guides with the guides' Start and
End (% of the art length); the four Directions; Flip Along and Across.
- Pattern Brush Options: Name; Scale (1-10000 %), Spacing (0-10000 %); the
five tiles (Outer Corner, Side, Inner Corner, Start, End) drawn with their
art or None; Flip Along and Across; Stretch to fit, Add space to fit or
Approximate path.
- All three: Colorization (None, Tints, Tints and Shades, Hue Shift with its
Key Color picked from the art's colours, the most used one by default) and
a live preview of a stroke painted with the edited brush.
Brush definitions from files and commands are now kept within these ranges
(Brush::sanitize, also for Calligraphic and Bristle values); unknown modes,
directions, fits or colorization methods are refused. The brush.options and
ui.brushOptions docs list the new params and fields. Tests: the brush model
(modes, pressure, sanitizing, art colours), engine params reaching the
brush and strokes redrawing, junk params in command_sweep, and headless
dialog tests for each dialog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Search: a search field at the top of the Libraries panel filters the
current library's colours, character and paragraph styles and graphics
by name, ignoring case; groups without a match are hidden, groups with
one open, and a search nothing matches says so ("No results").
Groups: items can be put in user-named groups, kept in the library file
(`groups: [{name, items: [{kind, item}]}]`, left out when there are
none, so files written before groups load as they are). New commands:
library.createGroup {library?, name?, items?}, library.renameGroup
{library?, group, name}, library.deleteGroup {library?, group} (its
items stay, ungrouped) and library.moveItem {library?, kind, item,
group?}. An item is in one group at most; deleting an item takes it out
of its group. The panel shows each group as a collapsible section with
its item count after the ungrouped items (rename and delete on its
context menu), each item's context menu has Move to Group (No Group,
the groups, New Group...), and a New Group button sits in the footer.
library.get lists the groups, library.list counts them. The generic
command dialog takes fixed params in `__params` for this.
Export / import: library.export {library?, path?} writes a library
(colours, styles, graphics with art and thumbnails, groups) as a
.vclibrary file, the JSON the library folder keeps (without a path:
{data}); library.import {path | data | dataBase64, name?} adds one as a
new library, made current, numbered on a name clash ("Brand (2)").
The panel menu has Import Library... and Export Library..., through the
app's file dialogs (the web downloads, and picks through File > Open,
which now lists Libraries files). Imported and folder files are made
safe: at most 10,000 items of a kind and 1,000 groups, names clipped and
made unique, sizes finite, thumbnails kept only when their PNG header
says at most 1024 px a side, group entries naming missing items or
unknown kinds dropped; a file over 512 MB or that isn't a library is an
error. Paths go through the engine's file helpers (automation roots).
The importer is fuzzed (import_fuzz: garbage and mutated libraries).
Also fixed: the third graphic added to a library got the second's id
(`graphic-2` twice).
Docs: the commands' params docs, docs/mcp.md, ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the Video group to the raster effects, after Sharpen:
- De-Interlace…: Eliminate Odd or Even Fields, Create New Fields by
Duplication or Interpolation. The fields are the rows of the document's
raster grid (Document Raster Effects Settings › Resolution, carried to
the filters as PixelSpace::line) counted from the top of the page, so
the result is the same at any zoom and moves with the page. A line
taken out copies the line above (the one below at the top), or takes
the average of the two.
- NTSC Colors (no options, applied at once): colours a composite
television signal can't carry, with luma plus chroma past 110 % of white
or luma minus chroma below -20 % in YIQ, have their chroma cut to fit,
keeping their luma. Saturated yellow, cyan and pure red lose some
saturation; legal colours stay as they are.
Catalogue rows with parameter docs, dialog dropdowns, menu, translations,
tests (effects, canvas, command sweep junk, menu) and ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
File › Place and File › Open read PSD and PSB files: the merged image
every such document carries after its layers, which is what a linked PSD
shows. A placed PSD links like any image, so the Links panel updates and
relinks it, and it is sized by its ResolutionInfo resource.
The reader is written from the public file format specification. It
reads Bitmap, Grayscale, Duotone, Indexed, RGB, CMYK (through the colour
settings' CMYK) and Lab, at 1, 8, 16 and 32 bits per channel (32-bit
values are linear), raw, PackBits or zipped with or without prediction.
A document with transparency stores its merged image on white with the
alpha in the first extra channel. It comes back with straight colours,
also for 16- and 32-bit files, which say so in their Lr16/Lr32 block.
Multichannel files are refused with a message. The file is untrusted:
every read is bounds-checked and the image's size is checked against
the decode budget before anything is allocated.
PSB is a read-only format of its own, so exports never write a PSD under
a .psb name. The desktop packaging still doesn't associate either
extension with VectorCraft (UNASSOCIATED_EXTS): they belong to raster
editors. Layers (import options) are left for later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four Photoshop-style raster effects on the pixel-filter pipeline
(vectorcraft_effects::pixel::pixelate), under a new Effect › Pixelate
submenu between Blur and Sharpen in the Raster Effects section:
- Color Halftone (maxRadius 4..127, default 8; channel1..channel4 screen
angles -360..360, defaults 108/162/90/45): each colour channel is
screened at its angle in square cells sized so a dot of Max. Radius
fills one; each cell's dot covers the share of the cell that is the
channel's coverage-weighted mean there (white stays white, black
black). RGB documents screen R, G, B (channels 1 to 3); CMYK documents
screen C, M, Y, K inks (channels 1 to 4), also when they are drawn ink
plane by ink plane (the K plane uses channel 4).
- Crystallize (cellSize 3..300, default 10): every pixel takes the
colour at the nearest of one jittered point per cell (exact Voronoi
over a 5 x 5 neighbourhood, pruned by distance).
- Mezzotint (type: Fine/Medium/Grainy/Coarse Dots, Short/Medium/Long
Lines, Short/Medium/Long Strokes; default Fine Dots; a dropdown in the
dialog): each channel is turned fully on or off against its own random
threshold pattern, so the object becomes pure colours.
- Pointillize (cellSize 3..300, default 5): one dot of random size per
cell at a jittered point, coloured by the softened object there and as
opaque as the object at its centre, stacked in random order over a
white canvas where the object is.
Lengths are in points (Scale Strokes & Effects scales them) and every
pattern lies in document space around the object's centre, so the
result is the same at any zoom or raster resolution, moves with the
object, and is identical on every redraw (hash noise of the pattern's
cell indices, no state). Work per pixel is bounded whatever the
parameters, which are clamped to their ranges and finite-checked; the
halftone's cell grid is capped by the raster's size and a screen finer
than 3 pixels leaves the image as it is (its average). PixelSpace gains
`channels` so the renderer tells the filters what the channels hold.
Catalogue rows with parameter docs, outsets and reaches for cropping,
menu entries, dialog labels (Max. Radius, Channel 1-4, Cell Size), the
Mezzotint Type dropdown, translations, and tests: parameters and
clamping, tone and coverage, CMYK inks, polygon crystals, pure-colour
mezzotints with ten distinct patterns, dots on a white canvas, the same
result at twice the resolution and when moved, the canvas, PDF and SVG
through the engine, the command sweep with junk parameters, the dialogs
and the menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A selected live polygon now shows Illustrator's Polygon Properties in the
Transform panel and the Properties panel's Transform section: side count,
angle, corner type and radius, radius, side length and Make Sides Equal
(enabled once an uneven scale has made its sides differ). Before, only the
side count and corner radius were there.
object.setLiveShape takes polygonAngle (degrees counterclockwise),
polygonRadius and sideLength (points, centre to vertex / side) and
makeSidesEqual, which drops the uneven scale and shear and keeps the
polygon's centre, angle and mean radius. Each call is one undo step; a
radius or length that isn't a number above 0 is an error. A star stays a
plain path, as in Illustrator.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Double-clicking a brush in the Brushes panel (or the panel menu's Brush
Options…) opens its options. Calligraphic Brush Options: Angle,
Roundness and Size, each Fixed, Random or Pressure with its Variation,
over live previews of the nib and of a stroke. Bristle Brush Options:
Shape and the bristle sliders. OK runs brush.options; strokes painted
with the brush update.
The Paintbrush records the pen pressure along a stroke (path.freehand
takes [x, y, pressure] points), and a Calligraphic brush with Pressure
variation changes its nib along the stroke, from the value less the
variation at the lightest pressure to the value plus it at the heaviest.
Brushes saved before keep their Random variation. Pen pressure doesn't
reach the app on macOS yet (winit 0.30 reports none there).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Curvature tool only edited the path it was drawing. It now edits any
selected path, whatever drew it, keeping its shape except where edited:
a click on a point makes it the current (direct-selected) point and a
drag moves it, re-curving only its two segments (the neighbours keep
their handle directions); Alt-click or double-click toggles it smooth or
corner and Delete removes it; a click on a segment adds a smooth point
without changing the shape, and a drag moves it in the same undo step.
With an end of an open path current, clicks go on from it (the old
segments keep their shape) and a click on the other end closes it.
The edits go through a new path.curvatureEdit command (move, insert,
extend, close), path.convertAnchor and path.removeAnchor, one undo step
each. The tool now claims Delete/Backspace while it has a point to
remove, so the key no longer deletes the whole path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Illustrator EPS and .ai type is made whole from the file's text
document, shown or hidden: point type, area type (a story in several
frames as threaded type that flows when the file opens) and type on a
path, with its fonts, size, leading, tracking, scaling, baseline shift,
fill and stroke colours (grey, RGB or CMYK), alignment, indents and
paragraph spacing. The page's drawing of that type, often in pieces, is
left out; where the text document can't say, type that shows still
comes from the page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a document opens with fonts that aren't available, fonts used
only in its symbols included, VectorCraft lists them in a Missing
Fonts dialog after the missing linked file questions, one document at
a time, with Find Fonts and Close. Find Fonts opens Type › Find Font
on the first missing font of the layers' type, to replace it. After
Find Fonts or Close the dialog doesn't open again for that open; when
another dialog replaces it, it opens again once that dialog closes.
Search Fonts Folder (the Fonts folder File › Package writes next to
the document) and Find in Folder… search a folder and its subfolders
for the fonts' files, and Add Fonts copies the files the user chooses
into a Fonts folder next to the preferences. The desktop app and
vectorcraft-cli read that folder with the system's font folders. Type
› Find Font has a Find in Folder… button too when fonts the document
uses are missing. The web app shows no dialog; a status message says
how many fonts aren't available.
- text.missingFonts lists each missing family and style once,
whatever version the document's type names, and leaves out CSS
generic families such as sans-serif. text.findFontFiles searches a
folder on up to 8 threads and returns at once; text.findFontFiles {}
reports the search, and {stop: true} stops it. A search reads only
what the font scan reads of font files (their table directories and
name, fvar and OS/2 tables) and, on macOS, of suitcase fonts, and
lists a file for a font only when the file makes the font resolve
exactly. It does not enter app packages, the Program Files and
ProgramData folders, the system's folders at the root of a volume,
the Shared and Public folders in Users, a home folder's Library,
AppData, Applications, snap and dot folders, a Library folder's app
data folders or the folders the environment names for apps, and it
skips other hidden folders and does not follow links below the
picked folder. It ends after 60 s by default, after 10,000,000
entries listed, with more than 1,000,000 folders waiting, after
10,000 font files read or 1,000 kept, or once every font has a file;
Stop and Close end it too.
- text.addFontFiles copies only files the last search found. Of
those, it copies only regular .ttf, .otf, .ttc, .otc, .woff and
.woff2 files and suitcase fonts with their resource fork, at most
256 MB a file and 1 GB in all. It never replaces a file: a file with
the same contents is kept, and when another file has that name, the
copy gets a number (Name 2.otf). The fonts are then scanned again,
and type set in them redraws.
- ui.missingFontsDialog and ui.findFontsInFolder open the dialog for
agents; docs/control-protocol.md lists its fields.
- File › Package gives a font file whose name has no font extension
the extension that matches its data (.ttf, .otf or .ttc).
- vectorcraft-text adds set_app_font_dir, which system_font_dirs
reads, WantedFonts, which reads a font file's names as the font scan
does, is_font_file and is_suitcase. #762 split its face reader, which
now takes read budgets: file_face_names_within tries the suitcase
reader first, and reader_face_names_within applies the budgets to
each font. vectorcraft-format adds write_new_with.
Docs: docs/mcp.md, docs/control-protocol.md, ROADMAP.