Files saved mid-edit by older versions carry the opacity-mask editing
layer at the top level. An id there that names a sublayer is ignored on
load, so that sublayer and its art are kept.
#5139 (in 0.21.0) made a positive Twist angle turn clockwise, but
documents saved by earlier releases stored their angles with the other
sign and reopened twisted the other way.
The document version (FORMAT_VERSION) goes from 1 to 2. Opening a
version 1 document negates its Twist angles (in layers, opacity-mask
art, Shaper edits, symbols, graph designs, pattern swatches, graphic
styles and brush definitions), unless the file is of the current file
version and its generator is VectorCraft 0.21.0 or later. The document
opens and saves as version 2. A save for an older file version, which
only apps before 0.21.0 read, writes a version 1 document with the
angles negated back. Graphic style libraries (.vcstyles) go to version
2: a version 1 library's Twist angles are negated when it is read.
Readers do not check either version, so older builds open the new files
as before.
* Native format: the .vectorcraft envelope comes from craft-vectorcraft-format
vectorcraft-format reads and writes the file's envelope through the
craft-vectorcraft-format crate in craft-libs: Head and Image, the version
and size constants, the format and version check, Tail for the data after
the JSON, split_tail and entry, and base64. The constants, is_compressed,
json_part, base64_encode and base64_decode are re-exported from
vectorcraft_format, so callers do not change. load_file reads the members
it uses with Image entries and skips the preview and PDF data. The
document, its migrations, the version 1 writer, gzip writing, sniff and
the limits on decompression stay here.
A preview or pdf member of the wrong shape now reads as missing on its
own, so pdf_content still returns a file's PDF when its preview is
damaged.
A new test saves a document in every version, compressed and pretty,
with a preview, profiles and a PDF, reads it back, saves it again and
compares the bytes; it also reads each uncompressed file as the crate's
File. Another reads the PDF of a file whose preview is damaged.
* Native format: sniff from craft-vectorcraft-format, pinned at #408's head
- craft-vectorcraft-format moves to craft-libs 0fd7e75fe6, the head of
storytold/craft-libs#408.
- `sniff` is the crate's: a file is native when its top-level `format`
member, within the first 256 bytes after decompression, is
`vectorcraft` or `drawcraft`, read with JSON escapes decoded. The word
elsewhere in the head no longer counts. All 192 corpus files and
their resaved copies get the same answer as before.
- Envelope errors other than the format and the version keep the
crate's message instead of reading as a compressed file that unpacks
to too much.
- A test checks that a repeated `generator`, `preview` or `pdf` opens
and any other repeated member is refused, by `load_file` and by the
crate's `Contents` alike.
* Native format: read through the crate's Contents and Entries
Pins craft-vectorcraft-format at 420a27f18f, the current head of
craft-libs#408, and replaces VectorCraft's own copies with the crate's:
- load_file reads the JSON through Contents::read, which also refuses
another format or a newer version. Opened is removed. Messages are
those of main: serde names the struct File, as main's reader did.
- In-memory data after the JSON (uncompressed files, and compressed
files unpacked whole) is read through the crate's Entries.
entries_within keeps its streaming reader for compressed files, whose
data is unpacked entry by entry and is never one slice in memory.
- The streaming reader finds the marker through the crate's marker_at.
- MAX_BLOB is the crate's MAX_ENTRY (1 GiB).
save_to still writes the envelope member by member: it streams the
document into the output and looks for the images it uses as it
writes, and File would need the images before the document. A new test
checks that its output, compact, pretty and compressed, for versions 1
to 4, equals serde_json::to_vec or to_vec_pretty of the crate's File
holding the same members followed by the crate's Tail, so a change to
either side fails.
* Native format: pin craft-vectorcraft-format at craft-libs main
craft-libs#408 merged as 14d5dbb50c; the crate is unchanged from
420a27f18f. A test checks that the crate's sniff takes every layout
VectorCraft has written: the version 1 and 2 files from before the
rename, compact and pretty, and each version save_with writes.
Without `precision` 500 coordinates of every size reopen bit for bit and a
second save writes the same bytes. Halves round away from zero, a closed
path keeps its closing anchor on its first one, a shape smaller than one
step collapses onto one point, coordinates at the edge of the canvas round
to the asked places, and saving a rounded file again at the same precision
writes the same bytes. docs/mcp.md states these rules.
A save with precision rounded the tier-1 Intertwine area but left the
areas of tier 2 and up exact. The format test now gives the object a
tier-2 area too, and sets the symbol's new dynamic field.
Edits refuse to leave anything beyond MAX_COORD (doc_sane checks the
artboards, the guides, the ruler origin and the objects an edit touches),
but the native loader didn't check them. A file with one object, artboard,
guide or ruler origin out there opened without a warning, and then every
move failed with "result would exceed the canvas", in-range objects
included; saving kept it that way.
Document::drop_beyond_canvas removes objects whose geometric bounds aren't
finite or reach past MAX_COORD (a layer or non-clipping group keeps its
children within it), artboards with their artboard guides, and guides out
there, and puts the ruler origin back at the origin. load_file runs it
after the depth limit and reports what it did with the other load notes; a
file whose artboards all lie beyond the canvas is refused.
NodeKind's reader buffers the fields that come before `type` as
serde_json::Values. VectorCraft writes `type` first, but a crafted file
can put a long array there: a group whose children held 50M numbers before
its type (a 96 KB gzip) peaked at 1.6 GB, and the cost grows with the
array up to MAX_JSON, before any object in it is counted.
The buffer is read through a counting visitor: each JSON value counts
against MAX_BUFFERED_VALUES (32M, 4M on wasm32) as it is read, and values
replayed from the buffer aren't counted twice. load_file reads within it;
the same file is now refused at a 1.1 GB peak.
Opening a native document now reads at most MAX_READ_NODES objects (4M,
512K on wasm32) and refuses the file past that with a message naming the
budget. Paths already read within MAX_READ_ANCHORS and the web build's JSON
cap is 256 MB, but the number of objects had no bound: a gzip'd document of
empty groups costs about 270 bytes per object once read against some 50 in
its JSON, so 10M groups in a 24 MB file took 2.7 GB to open.
NodeWire's id is read through counted_id, so each object counts once as it
is read. load_file reads through a private load_file_within so a test can
exercise the budget without a 4M-object document.
MAX_JSON was 1 GiB on every target, but on wasm32 the 4 GiB address space
also holds the document the JSON deserializes into, several times its size.
The cap is now a quarter of the native one there, mirroring MAX_BLOBS,
and Unpacked::within makes the cap exercisable wherever a test runs.
Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
MAX_JSON was 1 GiB on every target, but on wasm32 the 4 GiB address space
also holds the document the JSON deserializes into, several times its size.
The cap is now a quarter of the native one there, mirroring MAX_BLOBS,
and Unpacked::within makes the cap exercisable wherever a test runs.
Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
A document keeps up to 25 saved views, but the View menu listed only the
first 10, so views 11 to 25 could be saved and never chosen. All 25 are
listed now, by name, and choosing one changes the current window to its
zoom, centre and rotation (within the canvas's zoom range).
Edit Views… used to open the generic parameter dialog on the first view,
where the name of the view to change had to be typed. It now opens a list
of the document's views, like Edit Selection…: pick one to rename it or
delete it; OK applies every change as one undo step and Cancel leaves the
views as they were. view.saved.edit takes {edits: [{name, newName?,
delete?}…]} for that, the one-view form still works, and the command is
off while the document has no saved view.
A file's views are untrusted: on open at most 25 are kept, names are
trimmed, capped at 255 characters and unique, and views without a finite
centre and a positive zoom are dropped. New View… starts from the first
free "View N" and caps names the same way.
Behaviour from the public help page "Use multiple windows and views":
https://helpx.adobe.com/illustrator/using/viewing-artwork.html
docs/gaps.md (G7) and docs/target-app-parity.md take New View/Edit Views
off the missing list; docs/mcp.md documents the view.saved commands; the
catalogs get the Saved View 11-25 rows in the pattern of 1-10.
Found while testing crates/format/src/lib.rs:690-717 (base64_decode)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Native files store every anchor and handle as a full double, such as
3.4876474632797176, so a file of traced art is larger than it needs
to be.
document.save, file.saveAs, file.saveCopy and file.saveAsTemplate take
precision (0 to 9) for native files. The save rounds the anchors and
handles of path objects, Live Corners' uncut outlines, blend spines,
envelope outlines, Intertwine areas and brush art to that many decimal
places, in the layers, symbols, graph designs, patterns and opacity
masks, and in the art that placed documents become in files for older
versions. Colors, transforms, opacity and type are written exactly.
Without precision the file is written as before, and Save does not
remember the option. A .ai file, saved or exported, ignores precision.
The VectorCraft Options dialog shows no row for it.
A save given precision beside the path for a format that does not read
it (SVG, PDF, .ai and .ait) says so in its warnings, as it does for
compress, version and preview. MCP save_file passes precision on to the
save as it passes the other native options, and its schema, description
and docs/mcp.md list it.
A native save was pretty-printed when the document had at most 2,000
layers and objects and its compact JSON was at most 1 MiB. Pretty
printing puts every coordinate on its own indented line, so traced art
with few paths and many anchors could pass that check and be written at
about five times the size of its compact JSON.
The 1 MiB limit now applies to the pretty-printed document, indented as
the file holds it and in the form the save writes: version 1 anchors,
and placed documents expanded for older versions or Include Linked
Files. Documents whose pretty-printed JSON is over 1 MiB are saved as
compact JSON, even when their compact JSON is under it. The 2,000-object
rule and compressed saves are unchanged. docs/mcp.md describes the rule.
CodeRabbit findings #7, #72, #92, #94, #95, #99, #100, #140, #158, #174:
- Link search hashes at most the size it looked at (a growing file no longer matches).
- Resolving links reads, decodes and previews each linked file once, shared by every key.
- Placed documents read inside one another resolve links at most 8 deep, never one already
being read.
- A data set's image path and the placed-document rereader follow stored paths through
`document_path` (no other computer's share).
- The Links panel looks at a linked file within the roots of the automation caller that
opened the document.
- A fonts folder set by a confined caller is scanned without following links out of it.
- Recovery refuses an area folder that is a link or another account's.
- Atomic saves keep the replaced file's group and Linux access ACL, or make the replacement
owner-only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit findings #58, #75, #82, #113, #159, #175, #187:
- Live evaluation (expansion, envelope mapping and everything inside) makes at most 4M
objects (`MAX_GENERATED`); deep expansion counts what each live object makes, and envelope
image pieces count before they are cut.
- Clipping runs copy at most 8M clip path elements together; past it runs are plain groups.
- Exports read at most 256 placed documents; the rest show their previews.
- Graph design and marker budgets count opacity-mask art (`Node::count_with_masks`).
- Native files read at most 32M anchors (4M on wasm) of paths; the web build allows 1 GiB of
entries after the JSON.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: line tables only for workspace crates, no debug info for dependencies
* CI: one integration-test binary per crate
Each crate's tests/*.rs files become modules of tests/it/main.rs. Files that change
process-wide state (Color Settings, the font database, installed plug-ins, the working
directory), the fuzz sweeps and the test targets the Affinity corpus workflow names stay
their own binaries.
* Docs: name the integration tests' new paths
* CI: xtask test with nextest and doctests, one wasm check, clippy --keep-going
cargo xtask ci runs the tests with cargo nextest (cargo test when it is not installed),
builds them with --lib --bins --tests, runs the doctests as their own step, and checks
the wasm crates in one cargo check --keep-going call.
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
A .vectorcraft file's type opened with the character and paragraph
values it held. With a size of 0, the characters were not drawn. With
a size, leading, horizontal scale, left indent or space before of
1.797e308, point type's bounds were infinite or over 1e308 wide or
high, and moving or scaling it failed with "result would exceed the
canvas".
These values are now read into range when a file opens, in the type of
the layers, symbols, graph designs, patterns and the pattern definition
that Cancel puts back in pattern editing mode, and in character and
paragraph styles. A size, horizontal scale or vertical scale of 0 or
less becomes 12 pt or 100%, and a negative leading becomes Auto. A
size, leading or scale past 4,000,000 (MAX_COORD) becomes 4,000,000,
and a baseline shift, indent or space before or after past 4,000,000
either way becomes 4,000,000 or -4,000,000. A tab stop is kept to the
0 to 100,000 pt that text.tabs.set takes. A leading of 0, which legacy
.ai import stores, and the sizes, scales and indents within these
limits that SVG, PDF and .ai import keep outside the panels' ranges
open as saved.
Docs: docs/mcp.md (Paragraph attributes per paragraph).
document.setup accepts a superscript and subscript size of 1 to 500%
and a position of -500 to 500%, and a small caps size of 1 to 200%.
text.setFormat copies these proportions into the characters it
formats. A .vectorcraft file's characters, character styles and graph
label styles opened with the proportions they held. With a superscript
size of 0, the superscript characters were not drawn, and with a
superscript or small caps size of 1.797e308, moving the type failed
with "result would exceed the canvas".
These proportions are now read as the nearer end of each range when a
file opens: in the type of the layers, symbols, graph designs, patterns
and the pattern definition that Cancel puts back in pattern editing
mode, in graph label styles and in character styles. Proportions in
range and the other character attributes open as saved.
Docs: docs/mcp.md (Small Caps, Superscript and Subscript).
file.new takes a width and height up to 163,830 pt, 1 to 1,000
artboards, a spacing of 0 to 100,000 pt and a bleed of 0 to 72 pt a
side, but a saved or Recent New Document preset in the preferences
was read as it was stored. With a width of -5, file.new failed with
"width/height must be positive and within the canvas"; a spacing of
-50 overlapped the artboards, a preset with 5,000 artboards made 5,000
artboards, and a bleed of 100 reached the document. A .vectorcraft
file's Document Setup bleed, superscript and subscript proportions and
small caps size also opened as stored, and document.setup then failed
when its own report was passed back to it.
These values are now read as the nearer end of the range their
setter takes, and an artboard width or height of 0 or less is read as
1 pt. Presets and files with values in range read as before.
Docs: docs/mcp.md (Document Setup, New Document).
document.rasterEffectsSettings accepts a resolution from 1 to 2,400
ppi, but a file's raster_effects_ppi opened as it was saved. With 0,
-5 or 5000 in the file, object.rasterize without a ppi param failed
with "ppi must be between 1 and 2400", and document.rasterEffectsSettings
and document.info reported the stored value. With 0 or -5, EPS and
PDF export wrote a freeform gradient as a 1 x 1 px image, and EMF
export wrote every gradient as one. A saved New Document preset in the
preferences kept such a value too.
A file's or a saved preset's resolution outside 1 to 2,400 ppi is now
read as the nearer end of that range. Documents with a resolution in
range open as before.
Docs: docs/mcp.md (resolution, rasterEffectsPpi).
document.rasterEffectsSettings accepts addAround from 0 to 1,000 pt,
but a file's raster_effects.addAround opened as it was saved. With
addAround 1e300, the area of a raster effect image overflowed f64,
the image scale became 0 and the image got the transform
[inf, NaN, NaN, inf, x, y]. PDF export left that image out, so the
drop shadow was missing.
A file's addAround now opens clamped to 0 to 1,000 pt. An effect
image whose area or scale is not finite is not made. Export then
keeps the object with its effects, and Expand Appearance expands it
without an image, as when its PNG can't be encoded.
Docs: docs/mcp.md (addAround).
In a dev build, opening a native file whose area type has 2^60 rows
or 2^60 columns failed with an internal error ("capacity overflow"),
and opening one with 2^32 rows and 2^32 columns failed with "attempt
to multiply with overflow". Text layout reserved room for rows times
columns frame cells. `text.areaOptions` keeps both counts within 1 to
100, but reading a document kept the counts the file gave.
Reading a document now clamps each count to 1 to 100
(`AreaOptions::MAX_ROWS_COLUMNS`): a larger count reads as 100, and 0
reads as 1, as layout already treated it. Text layout clamps both
counts the same way. Area type with 1 to 100 rows and columns lays out
as before.
The cells of a frame now share one copy of the frame's flattened
polygons and one copy of the flattened text wrap shapes. Each cell held
its own copy: in a dev build, a test process that laid out text in 100
rows and 100 columns of a circle frame of radius 1,000,000 pt peaked at
1.33 GB of memory, and now peaks at 100 MB.
hayro-syntax 0.8.0 reads arrays and dictionaries 64 deep and no deeper wherever they are (the
file's text, object streams, the content of pages, forms, patterns, glyphs and masks), walks the
page tree without recursion and follows a chain of object streams once: what 0.7.2 overflowed
the stack on. hayro-interpret 0.8.0 comes with it (a new device interface: the importer's device
takes it as before). Our raw pre-scan of the file's text and page content goes: the reader's own
cap covers more, and the scan miscounted the bytes of unfiltered binary streams.
What 0.8.0 still builds by recursion with no cap is refused when the file opens: functions and
colour spaces built on themselves or on one another more than 16 deep, and calculator functions
whose procedures nest more than 64 deep. The import runs on a 16 MB thread, as the SVG importer
does.
hayro-syntax 0.8 turns on flate2's zlib-rs backend, which the whole app then deflates with: a
compressed native file is the same gzip stream as before (header and data), but its deflate
blocks now depend on how the data is fed, so the streaming test checks the header and the
unpacked data instead of the bytes.
Where a folder refuses the temporary file or the rename, the new content
(whole and flushed elsewhere) is copied into the file itself: the file was
truncated first, so a copy cut short by a lost connection or a crash left
neither version in place, and only the new one on disk under a hidden name.
The file as it was is now copied and flushed beside the temporary file before
it is truncated; if it can't be, the file is left alone and the save fails,
naming the temporary file. A failed copy names both.
The limits on a compressed file's entries were on their lengths. An entry of
one byte at a far offset unpacked everything before it, and each entry behind
the previous one started the stream over and unpacked the JSON and the gap
again: a small hand-made file could ask for hours of decompression. All of it
is now counted against one budget (the entries a writer can make, and the
JSON once more), past which the file is refused as too big.
A save writes a temporary file beside the document and renames it over
it. Some network shares refuse one of those steps although the document
itself can be written. The save now falls back, without giving up what
the rename is for:
- the temporary file can't be created beside the document: it is made
in the system's temporary folder (for its owner only);
- the rename is refused: the content, whole and flushed in the
temporary file, is copied into the document itself;
- when that copy fails, the temporary file stays and the error names
it; when the document can't be opened for writing, it is untouched
and the first error is returned;
- permissions that can't be copied to the temporary file (SMB, FAT) no
longer fail the save.
A folder that isn't there and a full volume fail as before.