An envelope is written on `%_` lines as a group holding the mesh that shapes
it, with the distortion (`AIDeformStyle`, `DeformValue`) in the group's
dictionary; the distorted art the PDF part draws follows on plain lines. The
reader kept that group, so the mesh drew as a grey gradient mesh behind the
art. A group on `%_` lines that holds only meshes and carries a distortion is
now dropped; a mesh with no distortion is still art.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6znVFRqGqvB9f7njwZ46r
A .ai or EPS opened from its editing data lost the opacity and blend
mode of its text objects. text_slot made the slot without the graphics
state's opacity and blend mode, which paths, meshes and images take, so
an Xy before a text object was lost. An Xy after it reached the slot,
but fill_slots then put type made from the text document in the slot's
place with only its visibility and name, and style_marker dropped the
transparency that a text object with an appearance carries on its look.
text_slot now takes the state's opacity and blend mode. fill_slots
gives made type the slot's opacity, blend mode, isolation and knockout.
The page's type gets them too, in place of its own transparency when
that is none, the slot's, or the slot's combined with that of the
objects around it; otherwise the page's type and the strokes around it
keep their own (a fill's, for example). When the type has transparency
and comes with other art (the strokes the page draws around it, or page
type that goes with it), a group of them takes the transparency. A text
object with an appearance takes the transparency from its look; where
the look draws more than the type and has transparency, the look and
the type go into a group that has it, and that group is not taken as
the look of a later object. The import fuzz's .ai tokens now include a
commented text object, 1 /FreeUndo , and a group's state marker
(6 () XW).
Docs: docs/mcp.md, docs/target-app-parity.md.
The PostScript interpreter that reads EPS and PostScript files had no
CCITTFaxDecode filter. Reading a file whose image data went through it
stopped at an error naming the filter, and the file opened from a
fallback, such as its preview or the art drawn before the image, or
did not open.
CCITTFaxDecode now decodes CCITT Group 4 (ITU-T T.6) and Group 3
one-dimensional and mixed (ITU-T T.4) data to rows of 1-bit samples.
It reads K, Columns, Rows, EndOfBlock, EndOfLine, EncodedByteAlign and
BlackIs1 (PLRM 3rd ed., §3.13; ISO 32000-2, §7.4.6). With EndOfBlock
the data ends at its EOFB or RTC regardless of Rows, and the RTC of
mixed Group 3 data, six EOLs each with a tag bit, is found by the
filter. Damaged data keeps the rows decoded before it, and damage in
the first row is an ioerror. Columns outside 1 to 65,535 and Rows
below 0 are a rangecheck, and data that would decode to more than
256 MiB is refused. hayro-ccitt 0.3.0, already in Cargo.lock through
hayro-syntax, becomes a dependency of vectorcraft-eps.
The tests encode their images in the test, with the fax crate's
Group 4 encoder and T.4 run codes (a dev-dependency, already in
Cargo.lock through tiff), and fail without the change. The import fuzz
tests read a Group 4 and a Group 3 image coded by hand from the T.4
and T.6 tables, read the same file with random damage, and add the
filter and its parameters to the hostile programs.
Docs: docs/file-format-parity.md, docs/target-app-parity.md.
An object whose art style had a Drop Shadow with Darkness (a csrc
other than 0 in the shadow filter's dictionary) came in as its drawn
look, with a warning that its appearance isn't editable yet. With a
Color shadow instead, the same object comes in as itself with its
fill, stroke and shadow.
The reader now reads such a shadow as Drop Shadow with shade
"darkness" and darkness set to the filter's dark value, a percentage
kept within 0 to 100, or 100 when the value is missing or not a
finite number. The shadow's other keys are read as for a shadow with Color,
and the object comes in as itself wherever it would with a shadow with
Color. A shadow with Color comes in with the same params as before.
The import fuzz's .ai tokens now include csrc and dark.
Docs: docs/target-app-parity.md.
The raster matrix [a b c d tx ty] is an art-space matrix (y up) applied to
the pixels with their rows running downwards, (u, -v). The reader flipped
the matrix's result instead, which only agrees when b = c = 0, so a rotated
image came in turned 180 degrees around its corner. The legacy version 7
writer and its prolog used the same convention; they now write and draw
the matrix the reader takes.
A `.ai` opened from its editing data lost a text object on a layer that
shows and has Print off when its page drew no type for it. prune_unseen
weighs each text object made from the text document that the page's
type doesn't match against the page, and the layers are drawn for that
comparison as they print (as_printed), with such layers hidden. Taking
one of these objects out never changed the comparison, so each was
removed with the note "text objects that the file keeps but its page
doesn't draw ... are left out".
unmark now carries a print flag next to the shown flag, and a layer
with printable off clears it for everything it holds. Only made text
objects that show and print are weighed against the page; the others
stay on their layer.
Docs: docs/mcp.md, docs/target-app-parity.md.
The PostScript interpreter that reads EPS and PostScript files turned
the samples of a CMYK image into display RGB and stored an RGBA PNG,
so the image lost its ink amounts. The DCTDecode filter decoded a CMYK
JPEG to RGB, so it lost its ink amounts too. The PDF importer keeps
the ink amounts of 8- and 16-bit CMYK images (in a CMYK TIFF, or the
CMYK JPEG itself).
A CMYK image (image or colorimage in DeviceCMYK, CIEBasedDEFG or a
four-component ICCBased space) now keeps its samples in a CMYK TIFF
(ImageBlob::cmyk_tiff), read through its Decode array, at every bit
depth the interpreter reads. A CMYK JPEG gives its samples as stored,
and the image's Decode array reads them: [1 0 1 0 1 0 1 0] undoes the
inverted samples of Adobe's CMYK JPEGs. The pixels that a MaskColor
or an ImageType 3 mask hides become a grayscale opacity mask over the
image, as in PDF import. A mask that hides nothing adds no mask. Gray,
RGB, indexed and spot color images and image masks stay RGBA PNGs.
Docs: docs/file-format-parity.md, docs/target-app-parity.md.
The editing data reader sized each row of a 1-bit image as width ×
channels bits, padded to a whole byte, but read one bit per pixel and
made the pixel gray from it. A 1-bit RGB or CMYK image came out as a
black and white image of the wrong bits. When that made the layers
differ from the page by more than 5%, the file opened from its PDF
part instead, with a warning.
The reader now takes channel c of pixel x from bit x × channels + c of
the row, a set bit being the channel at full strength, and converts
the samples as it converts 8-bit ones. 1-bit gray images read as
before.
An Intertwine group keeps its objects as they are, in their own stacking
order; each member can be drawn over the others in an area of its own.
The group draws, hits, saves and exports through its live expansion, so the
members stay editable and Release gives them back unchanged.
object.intertwine.arrange picks an overlap area by a click point or by a
closed path drawn around overlaps and toggles which member is on top
there, or brings a member to the front or sends it to the back where three
or more objects overlap.
A PDF page's UserUnit (PDF 1.6) sets the size of its user space unit
in multiples of 1/72 inch. The PDF reader has applied it since #1894, so
a page with /UserUnit 10 and a 36 × 50.4 media box opens at 360 × 504
pt. A .ai whose editing data has its artboards in the page's own units
then no longer matched its PDF part. It opened from the PDF part,
without its layers, its hidden objects and the art outside its
artboards, and with its artboards in a row instead of where the
editing data puts them. document.pdfInfo gave such a page's size in
points and its boxes in the file's numbers.
Editing data whose first artboard has the page's size in the page's
units, and not in points, is now compared with the PDF part scaled
down by the UserUnit. The document read from it is then scaled up by
the UserUnit, with its stroke weights, dashes, effects and pattern
tiles. When the layers can't be read, the artboard layout taken from
the editing data is scaled the same way. document.pdfInfo gives the
boxes in points, as it gives the size.
Docs: docs/mcp.md, docs/target-app-parity.md.
When a .ai file, or an EPS with editing data, opened from its editing
data, the reader converted each embedded 8-bit CMYK image to RGB and
stored it as a PNG, so the document lost the image's ink amounts. When
the same .ai opens from its PDF part, the PDF importer keeps them in a
CMYK TIFF.
The reader now stores those samples with ImageBlob::cmyk_tiff, as the
PDF importer does (crates/pdf/src/import_image.rs). An alpha channel
that hides part of the image becomes the image's opacity mask, a
grayscale image over it, as in PDF import. Grayscale, RGB and 1-bit
images still open as RGBA PNGs.
Docs: docs/file-format-parity.md, docs/target-app-parity.md.
The reference app's Drop Shadow has a Color and a Darkness, the
percentage of black added to the object's own colors. Drop Shadow had
only a Color, so every shadow was one color. Choosing one or the other,
and the defaults, are VectorCraft's own.
Drop Shadow now takes `shade` ("color" or "darkness", default "color")
and `darkness` (%, default 100). Its dialog and the Appearance panel's
inline editor show Color and Darkness as radio buttons. With Darkness
d, a CMYK document keeps the object's cyan, magenta and yellow and
raises its black from K to K + d × (1 − K), and an RGB document
multiplies each channel by 1 − d, so in RGB 0% gives the object's
color and 100% gives black. A shadow saved without these keys draws as
before. SVG export writes a Darkness shadow with feComponentTransfer
and SVG import reads it back; CSS export leaves it out.
Docs: docs/target-app-parity.md.
Documents are now at most vectorcraft_doc::MAX_DEPTH (32) deep, so the writer's MAX_DEPTH of 256
and its warning for art nested deeper can go; symbols and live objects nested in one another keep
their limit.
The file had an empty prolog, so a PostScript interpreter stopped at its first operator: it
couldn't be printed, placed or previewed by anything that runs PostScript. The prolog's
procedure set (VectorCraft_Legacy, written from the PostScript Language Reference and the
version 7 specification) now gives every operator the writer emits its meaning: paths and their
painting with separate fill and stroke colours and overprints, the fill rule, compound paths
painted as one path, clipping groups and masks, guides that don't print, layers that are hidden
or don't print marking nothing, and images drawn from the hex samples on the comment lines after
XI. Gradients fill with the colour set before them (the first stop's), and the type and encoding
operators take their operands and mark nothing.
VectorCraft opens a file with that prolog from its program, as before: its page has flat
gradients and no type.
Every file the writer's tests write is now run through import_with(bytes, false) and must draw
what the document it was written from draws: its paths, fills, strokes and images in the same
places and colours, with type left out (type the writer outlines is compared as its glyphs'
outlines), gradients as their first stop, layers that don't print left out and hidden objects
shown when the options write them visible. A new test prints the sample with every layer shown.
The engine's flattened-image test also opens the file as PostScript.
The page trailer the version 7 specification gives (section 3, Script Setup) is
`gsave annotatepage grestore showpage`, and annotatepage is a procedure of the app's own
prolog, which the file didn't have. The file now carries a procedure set of the writer's own
(VectorCraft_Legacy 1 0, listed in %%DocumentSuppliedResources) in a dictionary the setup begins
and the trailer ends, which defines annotatepage as marking nothing.
Type is written as version 7 text objects (§15): point type, area type and type on a path, with
fonts by PostScript name re-encoded to Windows-1252, and each run's paint, size, scaling,
tracking, leading, alignment and indents; type 8-bit text can't hold (or all, with outlineText)
is outlines. The format hides only layers, so hidden objects go to a hidden layer above their
own (hiddenObjects "layer", the default), are left out ("omit") or show ("visible"). The .ai
reader reads legacy type (To … TO, TE, %AI3_BeginEncoding) and ignores Ar; the import fuzzer
gets legacy type tokens.
export_legacy_ai writes a document as a PostScript-form .ai of version 7 (one artboard per
file), keeping every layer (name, order, Hidden, Locked) and its groups, compound paths and
clip groups. Closed paths end on their start point, so they keep their corner. The .ai reader
learns what the writer emits, and the tests read every file back.
A scatter brush whose art is an instance of a symbol stroked with that brush, or any brush whose
art is painted with a pattern whose tile is stroked with it, drew itself without end: a stack
overflow in a hand-written native file. Brush pieces already strip the brushes of their own
strokes; now what they draw by name that would draw a brushed stroke (a symbol instance, or a
pattern paint, also through the symbols and patterns those use) is left out too, in one place
(`stroke_pieces`, which every renderer and exporter calls with the document).
Probed the rest of item 2 of the depth follow-up with hand-written hostile inputs, kept as tests:
graph designs of deep art and of a symbol holding itself, Affinity groups nested to and past
both of its reader's caps, DXF blocks inserting themselves, EMF/WMF saving states without end,
and plug-in manifests and output nested 30,000 to 100,000 deep. None of them overflowed.
Composite objects are reference-counted, so `/a 1 array def a 0 a put` (or two
dictionaries naming each other) outlived the import: emptying the standard
dictionaries and the stacks doesn't break a cycle, least of all one the
program no longer reaches. Up to the interpreter's whole memory budget could
stay allocated per imported file. The interpreter now lists the arrays and
dictionaries it makes (weakly, swept as they are dropped) and empties the
ones still alive when it is done.
A `z` is four bytes for one character, so the text's length didn't bound
the result, and the filter's and the interpreter's budgets only saw it once
it was whole. The decoder now stops at 256 MB, the most one filter or data
section may hold.
Every symbol instance was measured as its transform applied to the
20 × 20 square around the origin, which symbols made in the app keep
their art in. The SVG and DXF importers keep symbol art as drawn, so
the box of an instance of an imported symbol did not match its art.
The SVG Star instance at x 20, y 30 draws from (20, 30) to (40, 50)
and was measured from (10, 20) to (30, 40). Align moved such an
instance by the wrong amount, clicks and marquees beside its art
selected it, and the canvas, exports and prints left it out when its
art reached the view or the page and the square did not.
An instance is now measured by its symbol's art, which
Document::symbol_boxes looks up in the document each time
(Node::geometric_bounds_with, visual_bounds_with and
reach_bounds_with). With Use Preview Bounds on, the strokes in the art
count at their own weight, as the canvas draws them, and
object.setBounds counts them at that weight when it sizes an instance
or an object that holds one. When strokes do not scale and a width or
height of 0 or more is smaller than what a path's stroke adds,
object.setBounds now gives the path no width or height, as it does
for an instance whose art has strokes; before, it mirrored the path.
With Use Preview Bounds on, object.setBounds now measures the margins
that strokes add object by object, the members of groups and the
objects in symbol instances' art included, and solves for the width,
height and reference point asked for when the width and height are 0
or more. Before, it measured the margins of the whole selection and
could miss the size or the reference point asked for when strokes did
not scale and the objects' strokes added different margins, and when
strokes scaled and the width and height were out of proportion, even
for one path. A clip group, a Shaper Group, a compound path, type, and
a group or instance with effects or an opacity mask of its own are each
measured as one object, and so is an instance in perspective or one
turned or slanted relative to the bounding box, other than by quarter
turns. When the selection holds more than 4,096 objects to measure this
way, each selected object is measured as one. Instances inside symbol
art are measured by their own symbol's art. An instance is measured as
the square when its symbol is missing, when its symbol or a symbol
nested in its art holds an instance of itself, and when symbols nest 32
deep in its art.
Align and Distribute, the Transform panel, the bounding box and the
selection outline, clicks, the marquee, document.inspect and
document.node, Fit to Artwork Bounds and Fit to Selected Art, canvas
culling, thumbnails, the SVG filter region of an instance with a
raster effect, raster effect images, Export Selection, the SVG of an
artboard, printing, and the PDF, EPS, EMF/WMF and DXF writers use the
new box. The renderer also culls an instance by the effects and
brushed strokes in its art, and it culls a blend, envelope or repeat
by the brush art of what it holds and the effects in the art of the
instances in it. It culls an instance with geometry effects of its own
by the effects in its art after those geometry effects reshape the art.
It measures the effects in a symbol's art once for each symbol and
linear part of the transform that places it. For symbols whose art
holds instances, it measures at most 4,096 placements a frame, and at
most 4,096 more for each pattern tile it rasterizes and each placed
document it draws. Past that, it estimates the reach from the symbol's
reach with no transform, which it measures once. When the drawn
document's symbols change, the renderer drops the cached bounds of the
instances and of the objects that hold them.
The PDF and EPS writers, the SVG of an artboard and Skip Blank
Artboards grow the box of an instance whose art holds brushed strokes
by 16 times the widest stroke (at least 1 pt) of the objects in that
art that have a brushed stroke, plus 8 pt, as the renderer grows the
cull bounds of a brushed path. They keep an instance whose brush art
reaches the page. The SVG of an artboard also keeps an object that
holds an instance when what the canvas paints for it, the effects in
the symbol's art and the instance's own included, reaches the artboard.
Not yet: other commands that measure objects, such as Select › All on
Active Artboard, Transform Each and Convert to Artboards, still measure
an instance by the square.
Docs: docs/mcp.md, docs/target-app-parity.md.
A stitching function's bound became one stop, evaluated at the bound:
the right piece's colour. Where the pieces don't meet, the left piece's
end colour was lost. The colour just before the bound is now a stop at
the same offset too, ahead of the one after it, when the two differ;
continuous stitches keep one stop per bound as before.
Fixes#1653
Both readers skipped a clip of at most six straight segments whose
bounding box contained the page. A diamond touching the page edges has
the page as its bounding box, so its clip was dropped and everything it
cuts away showed. geom::hit::polygon_covers_rect answers it properly
(one convex polygon containing the page's corners); any other path is
kept as a clip.
Fixes#1542
Interp::font took a font's FontMatrix times 1000 as the matrix of one
em, the Type 1 convention (glyphs in 1000-unit space). A Type 42 font's
glyph space is one unit an em, its FontMatrix usually the identity, so
type in Type 42 fonts (cairo, Inkscape, LibreOffice) came in 1000 times
too large and off the page. FontType 42 now takes the matrix as it is.
Fixes#1552