An image whose data is empty failed to decode before the size check, so a
version 7 .ai said it couldn't be decoded. The writer now checks for empty
data first and gives "an image that is empty or too large for legacy .ai
was left out". docs/mcp.md now says the DXF decode note applies to R13 and
later; R12 leaves out every image with its own note.
EPS, EMF, WMF and DXF exports gave each image they left out because it
couldn't be decoded its own warning, "image '<key>' could not be
decoded and was left out", which names the internal key the document
stores the image under. A version 7 .ai said "an image that can't be
decoded or is too large was left out".
The four writers now give the note raster exports, PDF and the legacy
.ai flattening give, "an image couldn't be decoded and is missing from
the picture", once per export. In a version 7 .ai it is one line with
the note for flattened art. The other image notes in the same
functions no longer name the key: EPS's note for an image too large to
write, and DXF's notes for an image with no pixels (its rows in 23
catalogs are reworded) and one that could not be written. Legacy .ai
gives a separate note for an image that is empty or too large.
Docs: docs/mcp.md (EPS, DXF export, EMF and WMF, aiVersion "7").
**What:** Illustrator type styled through the Appearance panel came in as its drawn look (outlined compound paths). Comic and "text effect" templates are the common case, and the type couldn't be retyped. This keeps it as editable type with the style as a live VectorCraft appearance.
**Changes**
- **Art styles parsed as a tree.** Compound filters close at their `;`, basic ones at `/Part`. Each fill and stroke in a stack keeps its own effects, opacity and blend mode, and object-level effects stay on the object.
- **Type keeps any plain stack.** That covers several fills and strokes, pattern and gradient fills, Warp, Offset Path, Transform with copies, Drop Shadow, Outer Glow, Feather, Gaussian Blur and Bas Relief. Bas Relief settings are left at their defaults, because Illustrator stores them in a Photoshop plug-in's binary block. Gradients and patterns are placed as the drawn look places them. Other objects keep the existing plain-style rule, since their drawn look is the more faithful option.
- **Point type without a frame matrix.** Illustrator 24 omits the matrix when point type is neither rotated nor scaled. Such stories were left out entirely; they're now read with an identity matrix.
- **Page comparison.** The page's rendering of a style isn't held to match VectorCraft's live rendering where the replaced drawn look lay, and styled made type isn't pruned as unseen.
- **EPS override.** `editingData: "always"` / `convert --editing-data` now applies to Illustrator EPS as well as `.ai`.
**Tests:** 6 new tests, written from scratch; `vectorcraft-eps` passes all 298 on current main. clippy `-D warnings` and rustfmt are clean for eps, engine and cli. `docs/file-format-parity.md` has been updated, including its revision history.
**Clean-room note:** I found the bugs with a user's licensed stock EPS, used locally only; nothing from it is committed. Filter and key meanings come from the public Illustrator Effect docs and public LiveEffect scripting examples (`jntp`, the Warp style order).
**Not included:** VectorCraft's pixel effects treat transparent content as empty instead of using Document Raster Effects Settings › Background (white by default). Because of that, Bas Relief on a transparent pattern gives flat grey. That needs a separate renderer change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
A colour named by a gradient stop (`Bs`) was collected with the colours the
colour operators define, in file order, and the first definition of a name
made its swatch. A stop can't say whether its colour is an ink, so it reads
as a spot colour; and since the file defines its gradients before the art,
a global process colour set with `Xk` that a gradient also names came in as
a spot swatch, which PDF export then writes as a Separation. The stops'
named colours are now kept apart and make swatches only after the colour
operators' colours, so `Xk`'s definition wins and a name only a stop uses
still gets its swatch. The stop's values were already read right since the
lexer reads a `%_BS` line's own operands.
Shrink Text to Fit scales area type down at layout time, and the canvas,
PDF export and the exports that write type as outlines draw it at that
size. SVG export wrote the live text at its stored font size, letter
spacing and baseline shift, so a viewer drew it larger than the canvas
does. Legacy .ai saves wrote the stored size, leading and baseline shift
into the text objects, DXF export took the TEXT heights from the stored
size, and the CSS Properties panel gave the stored font size, letter
spacing and line height.
The text crate's scaled, which the layout scales shrunk text with, is
now public. SVG export, legacy .ai saves and the CSS Properties panel
take the type's styles from it at the layout's fit scale, and DXF
export multiplies the TEXT heights by that scale. Positions and
textLength in SVG still come from the layout.
Docs: docs/target-app-parity.md.
An EPS or PostScript file that stops on a PostScript error named the
error, such as typecheck or rangecheck, with the operator and the
procedure, but did not say what the name means in any language.
The warning or error now gives the name as PostScript spells it,
followed by VectorCraft's own wording of what it means in the reader:
"a PostScript error (typecheck in `add`; a value has the wrong type for
the operator)". vectorcraft_eps::POSTSCRIPT_ERRORS lists the 14 names
the reader raises other than undefined, with their meanings, and a
test checks that each of those names has one. Each meaning and the
new message shapes have a row in every catalog. i18n::message now
translates placeholder values five levels deep instead of three, which
the status bar's line for an EPS file that can't be opened needs.
Docs: docs/mcp.md, docs/target-app-parity.md.
The version 7 .ai writer wrote the %_Bs color stops of every gradient
from ramp point 100 down to 0. The Adobe Illustrator File Format
Specification version 7.0 lists a linear gradient's stops in that order
(section 1.4.3). Its radial definition, "White & Purple Radial", lists
them from 10 to 100 (section 3.5.2). As reported on #2807, the
reference app reads both stops of #2687's radial gradient, written from
100 down to 0, at ramp point 100 and draws the circle in #1b2a6b. It
reads a radial gradient with three stops written that way in reverse
order.
The writer now lists a radial gradient's stops from the first to the
last. Each stop is written with its own midpoint to the next stop, and
section 3.5.2 says the last stop's midpoint is ignored. Linear
gradients are unchanged.
Illustrator stores General ruler units in %AI5_RulerUnits; apply them on EPS/.ai import so rulers and length fields match the saved file.
Co-authored-by: Cursor <cursoragent@cursor.com>
Illustrator 6/8 EPS files whose PostScript can't be run (the Adobe_level2_AI5
procset is provided by the RIP, not embedded) are read through their private
layer data instead. That reader aborted the whole file when a text object
carried the hyphenation/tab-ruler operators `Th` (hyphen control), `XL`
(hyphenation language) and `Xb`/`XB` (a tab definition, with `Xe` descriptions
between them), reporting them as operators it doesn't know.
These carry no geometry, so drop them like the sibling type attributes the
reader already discards (§22 of the Illustrator File Format Specification)
rather than failing the import.
Saving a version 7 .ai wrote the title, spot color names and gradient
names as the UTF-8 bytes of their characters: a spot color named Café
was written (Caf\303\251) in the header's custom colors and in each x.
Type and, since #2722, layer names are written in Windows-1252, the
encoding the file installs with TE.
These names now go through #2722's Windows-1252 path, so Café is
written (Caf\351). A character outside Windows-1252 is written as ?,
with one warning for all of these names. A spot color keeps one name
in the header, in each x and X and in gradient stops, and a gradient
keeps one name in its definition and in each Bg. Two spot colors or
two gradients whose names become the same that way are numbered (? 2).
Docs: docs/file-format-parity.md.
An image in a .ai's editing data whose operands had no matrix, fewer
than 11 numbers, or a matrix of other than six numbers was left out
with no warning. So was one whose matrix maps it onto a line or a
point. The reader's warnings did not mention such an image, while the
reader warned "an image VectorCraft can't read was left out" for an
image whose size or bit depth it can't use.
Each of those exits now adds the same warning, as does the one where
the decoded pixels don't make an image.
alongtheblue-sketch took 0.12.0's version 7 output of the SVGs in #2692
and #2830, edited it by hand and opened it in the reference app
(https://github.com/storytold/vectorcraft/pull/2801#issuecomment-6244853548).
Writing the image lines as #2801 does, with the bracketed matrix and
the 60-digit data lines, did not change the result there. With one
line, F, added after %AI5_EndRaster, the reference app opened the image
in its clipping group and the art after it in both files.
The writer now writes F after every %AI5_EndRaster: clipped, grouped
and loose images, flattened images and images in pattern tiles. No path
is open at that point, so the line paints nothing. VectorCraft's reader
opens the files with the same objects as before, and the file's
PostScript prolog draws the same page.
document.pdfInfo refused PostScript (an EPS, a .ps file, or a .ai in
PostScript form), mostly with "this is a PostScript file ..., not a
PDF", although document.open and file.place read the same file
through the EPS reader as one page.
It now answers {pages: 1, needsPassword: false, pageInfo: [{width,
height, rotation: 0, boxes: {}}]}. The page is the file's bounding box
(%%HiResBoundingBox when it has one, a US Letter page without one),
read from its comments without running its program. thumbnail: 1 is a
PNG of what that page prints, read by the EPS reader without any
editing data the file carries. A program whose header is only %! is
read as PostScript, as clipboard.importEps reads it.
Docs: docs/mcp.md, docs/target-app-parity.md.
The PostScript interpreter that reads EPS and PostScript .ai files
accepts a FlateDecode or LZWDecode filter with Predictor 2 at
BitsPerComponent 1, 2, 4, 8 and 16, but undid the horizontal
differencing only at 8 bits. At the other depths the filter gave back
the stored differences, and the image drew them instead of its
samples.
The predictor is now undone at every depth it accepts, as ISO 32000-1
§7.4.4.4 describes: each component adds the same component of the
pixel to its left, within its bit width, and each row starts again.
16-bit components add as big-endian 16-bit numbers. The padding bits
at the end of a row stay as they are, and 8-bit data takes the same
path as before. The PNG predictors are unchanged.
Docs: docs/target-app-parity.md.
A right-to-left paragraph in an Illustrator editing copy (key 33 of its
paragraph attributes) stores its alignment relative to its direction:
"left" lays the line out ending at the anchor and "right" starting there.
The importer read the alignment alone, so RTL point type opened shifted by
its own width (Arabic headings past the artboard edge, subtitles starting
where their title ends, a label over a QR code).
Checked locally against ~1,440 single-line point-type stories in a user's
own files (never committed): RTL + left ended at the anchor in 332 of 336,
RTL + right started there in 51 of 51; LTR and centred type unchanged.
Justified alignments keep their current mapping (too few samples).
A .ai can keep its variables and data sets in a variable library in
its editing data. VectorCraft doesn't read that library, and such a
file opened with an empty Variables panel and no warning about it.
The reader now counts the library's variables and data sets in the
editing data's /Document dictionary, and opening the file adds a
warning: "the file's variables and data sets weren't read (2
variables, 3 data sets): the Variables panel doesn't list them". The
same warning appears when the layers can't be read and the file opens
from its PDF part. A PDF that opens from the editing data it carries
gets the warning too. A library with no variables and no data sets
gives no warning.
Docs: docs/target-app-parity.md.
A version 7 .ai wrote each image's matrix as [a b c d tx ty] and its
samples on comment lines of 64 hex digits. The Adobe Illustrator File
Format Specification version 7.0, which the writer follows, writes the
matrix with a space inside each bracket in the XI syntax (section 8.1)
and in all five examples (section 8.4), and every example data line
holds 60 hex digits. #2692 reports that the reference app leaves out an
image inside a clipping group, and all the art after it, in the files
written from the reporter's two SVGs.
The writer now writes [ a b c d tx ty ] and data lines of 60 hex digits
(the last holds the rest), for every image it writes. VectorCraft's
reader and the file's PostScript prolog read both forms. The result was
not checked in the reference app.
The version 7 .ai writer wrote a radial gradient's highlight (Bh) only
when the gradient had a focal point off its center. A centered radial
gradient, such as an SVG radialGradient without fx and fy, was written
Bb, Bg, f, BB with no Bh. Every radial instance in the Adobe Illustrator File
Format Specification version 7.0, which the writer follows, has one,
0 0 0 0 Bh for a centered highlight (sections 3.5.5, 3.5.6 and 9.3).
#2687 reports that the reference app draws such a circle in its outer
color.
The writer now writes Bh in every radial instance, 0 0 0 0 Bh at the
center. A focal point past the radius, which an elliptical gradient
written circular can have, is written on the circle, so its x and y
offsets agree with the fraction 1 written after them (section 3.5.5).
Linear instances are unchanged. The result was not checked in the
reference app.
When a .ai or EPS file opens from its editing data, the import left
out the page's type that no text object owns whenever the regions of
the text objects made from the text document covered at least half of
it, without comparing letters. The region of point type reaches its
longest line's length in characters times its size, plus its size, to
the left and right, and one and a half times its size above and below.
A 21-character line at 10 pt reaches 220 pt to each side. The page's
other type inside that box was lost.
Now the region of a made text object counts toward that half only
when its story has the Latin letters and digits of one of the type's
pieces, compared without case, and only when such stories between
them have the letters of every piece. Only those text objects count
as drawn by the page. The check that decides whether the text
document is trusted still counts every region.
Docs: docs/target-app-parity.md.
When the editing data of an EPS or .ai file was for a page of another
size than the file's own page, the file opened as its page or its PDF
part with the reason "its editing data is for a different page". The
reason did not say which page the editing data was for, or that it was
the same page at another scale.
The reason now gives both sizes in points: "its editing data is for a
600 × 400 pt page, not the file's 612 × 792 pt page". When the two
pages are in proportion (within the 0.5 pt per side of the check), it
gives the scale of the editing data's page to the file's instead:
"its editing data is for a 100 × 100 pt page, the file's
1000 × 1000 pt page at 1:10". Every catalog that had the old reason
has rows for both new ones.
Docs: docs/mcp.md, docs/target-app-parity.md.
A PDF-compatible .ai, or a PDF saved with the editing data, whose
editing data can't be decompressed (damaged zlib or Zstandard data, or
data that decompresses to more than 256 MB) opened from its PDF part
with no note about its layers. layered_ai returned on the
decompression error without a note. Data that decompresses to layers
that can't be read gets one, and so does an EPS.
layered_ai now gives the same note with the decompression error as
its reason: "the file's layers weren't read from its editing data (its
zlib compression is damaged): they come from its PDF part". The note
counts as a loss, so document.export or Save As over a PDF saved with
the editing data asks first (acknowledgeLoss), as it already did for
a .ai. The three reasons have rows in the 17 catalogs that translate
every message.
Docs: docs/mcp.md, docs/target-app-parity.md.
When a .ai or EPS opens from its editing data and its layers draw a
little differently from the file's own page, the warning gave the share
of all the pixels that differ, type included, and blamed brush strokes
and live effects. Type that the import lays out again from the file's
text, or draws in another font, raised it on its own: a line that the
page sets in pieces gave "about 1.5% of it (brush strokes, live effects
and the like ...)" with no art differing.
The comparison now also counts the pixels that differ away from type.
The warning gives that share, and when the art away from type does not
differ by more than the warning's limit, the warning names the type
instead. The warning appears in the same cases as before, and the
limits that make the file come in as its page are unchanged.
Docs: docs/target-app-parity.md.
VectorCraft listed a variable font's named instances as styles, but
type could not be set between or past them. The reference app's
Character panel has a Variable Font control that adjusts a variable
font's axes, such as Weight, Width and Slant.
Type now keeps Variable Font settings by axis tag
(CharStyle::font_axes, saved as fontAxes only when set). text.setFormat
and text.setRangeStyle take fontAxes: each value is clamped to its
axis's range, a tag the font has no axis for is left out, and null
goes back to the style's own setting. Another font or style clears
them, including one from a character style that does not set them. A
character style captured from type sets them, with {} for none.
text.fontList {family, style} lists a font's axes.
Layout, outlines, the substituted-glyph highlight and PDF export use
the font instanced at the settings, and neighboring runs at different
settings are shaped apart, as runs in different fonts are. SVG export
writes them as font-variation-settings, the wght setting as the weight
and the wdth setting as font-stretch, and embeds the font instanced
there; SVG import reads font-variation-settings back. Legacy .ai export
warns that the settings are left out.
The Character panel shows a Variable Font button for a variable font.
It opens a slider and a field for each axis the font does not hide,
labeled with the font's name for the axis, and screen readers get each
slider under that name. A drag is one undo step. Reset Panel leaves
the settings, as it leaves the font and style.
Missing Glyph Protection now keeps the settings and the font version
on the characters that keep their font; before, it kept only the
family and style. A character style applied or redefined with another
font now clears the old font's version, as text.setStyle does, unless
the style sets one. SVG export with embedded fonts warns when faces of
one weight, style and width share characters, which browsers then draw
in one of the faces.
Docs: docs/mcp.md, docs/target-app-parity.md, docs/type-parity.md.
A PDF-compatible .ai read through its editing data kept the PDF part's
warnings that text was kept as outlines ("text was converted to
outlines", the U+FFFD warning and the warning on glyphs that differ
from the installed font). The layers take from the page only text
nodes and the stroked paths around them, so filled outlined glyphs
don't reach the document, and type made from the text document holds
every character.
- The warnings are dropped when the document holds no glyphs kept as
outlines. Stroked glyphs that come in with stroked type keep them.
- A text object that takes the page's type, next to glyphs the page has
only as outlines, lacks those characters: a new warning counts such
objects.
- vectorcraft_pdf exports the three warnings, is_outlines_note,
is_glyph_outlines and holds_outlines.
Docs: docs/target-app-parity.md.
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.