Commit Graph
28 Commits
Author SHA1 Message Date
dotCooCoo 3d7dda200e .ai import: opacity mask art of one object opens as that object
The reader put an opacity mask's art in a group every time, so a mask
made from one path came back from a saved .ai as a group holding the
path, while Make Opacity Mask keeps one object as the art. This also
changes how existing files with single-object masks open, because
the editing data of a .ai, an EPS and a PDF goes through this reader:
the art is the object.

A hidden object or a guide stays in a group, because the canvas draws
the art's root whether it is shown or not and whether guides show. A
clipping path alone stays a clipping group.

The editing copy writes a plain group of one object as a group (u, the
object, U), so it reads back as a group. Its other plain groups are
still written as their members. Hidden mask art is written shown,
because the canvas draws it and a group read back would not.
2026-11-07 08:19:20 +07:00
bflatastic ccdccb1299 Tests: an image next to type keeps its knockout shape flag 2026-11-07 07:36:26 +07:00
dotCooCoo a9b49f716b .ai import: images, meshes and blends keep the knockout shape flag
An `Xy` whose shape operand is set now reaches an image (embedded or
linked) and a gradient mesh when it is written before the object, and a
mesh or blend when it is written after the object's end. A live blend
takes the flag from its drawn steps. Before, the reader dropped the flag
on these objects, so an image saved with Opacity & Mask Define Knockout
Shape lost it on reopening, with no warning.

A styled object takes the flag written on its drawn look. For a text
object, the flag goes to the object when the look draws only its type,
and otherwise to the group that holds the look and the type. An opaque
look that draws more than the type and sets the flag now goes into that
group as well.
2026-11-07 05:15:19 +07:00
dotCooCoo 7be6f8731c .ai save: check a frame's segment count before building its path
A frame or path past the segments the reader reads is rejected from its count, before it is copied and converted.
2026-11-07 05:15:17 +07:00
dotCooCoo 9006177033 .ai save: test a thread with a frame in a clipping group
A thread with a frame in a clipping group whose clipping object is a path stays type and comes back as one thread.
2026-11-07 05:15:16 +07:00
dotCooCoo 63dbb0f0dd .ai save: an open frame whose ends meet is written as outlines
Area type whose open frame ends where it starts was written as text and
read back in a closed frame. It is now written as its outlines, alone or
as a frame of a thread.
2026-11-07 05:15:16 +07:00
dotCooCoo 0c5ad8e242 .ai save: objects next to type keep their own transparency
The reader gives an Xy right after a compound path or a text object to
that object. The writer now writes XR before the Xy of type, images and
guides, so type next to type, a compound path before type, image or
guide, and type before an image or guide keep their own opacity, blend
mode and knockout shape flag.

- Type keeps its Isolate Blending and Knockout Group state in a state
  block after its text object, as its outlines did on their group.
- The reader no longer gives a layer the Xy before its last text object.
- The ASCII85 end marker of the text document stays on one line.
- The warning and docs/file-format-parity.md also list area type in an
  open frame, type on an open path whose ends meet, type of only white
  space and threads of type on a path as written as outlines.
- README.md, ROADMAP.md, docs/gaps.md, docs/roadmap.md and
  docs/target-app-parity.md describe the editing copy a saved .ai holds.
- clip_path's doc comment is back on clip_path.
- import_fuzz reaches threaded area type and vertical type.
2026-11-07 05:15:16 +07:00
dotCooCoo aed9898b8d .ai save: the editing copy keeps type as type
The editing copy of a PDF-compatible .ai wrote every text object as its
outlines. Point type, area type and type on a path are now written as
text: a text object in the layers that names its story, and the text
document after the document data (%AI11_BeginTextDocument) holding the
stories.

- Each story holds its characters (a paragraph ends in \r, a forced line
  break is \x03), its character runs (font, size, leading, tracking,
  baseline shift, horizontal and vertical scale, solid fill and stroke
  with its weight, capitals, Tate-chu-yoko) and its paragraph runs
  (alignment, indents, space before and after, hyphenation, right-to-left
  direction).
- Point type keeps its point and, when it is turned, its matrix. Type
  scaled by one factor, such as all type on a large canvas, has its scale
  in its sizes, frame and path. Area type keeps its frame, rows,
  columns, gutters and first baseline, type on a path its path and where
  it starts and ends, and vertical type its orientation. A threaded story
  is one story through its frames in order. Area type and type on a path
  carry their frame's path in the layers as well.
- Type the text document can't hold is written as its outlines, with a
  warning: type in opacity masks, pattern tiles and art cut to a clipping
  object, mirrored type, turned or unevenly scaled area type and type on
  a path, frames of several parts, area type that flows around a text wrap,
  threads whose frames differ in orientation or include a frame written
  as outlines, type and stories past the sizes the reader reads, and type
  with inline graphics, ruby, gradient or pattern paint or an appearance
  or opacity mask of its own. Other character and paragraph attributes are left out, with
  a warning.

Tracking is written as a whole number of thousandths of an em, and
solid paints as simple paints; tracking that isn't a whole number is
rounded, with the warning.

Each construct is written in the form VectorCraft's .ai reader reads
(crates/eps/src/import/ate.rs at 36ef34dfdd: Texts::read :407, story
:501, frame :456, para :562, style :588, paint :622, AreaFrame::read
:284; the text object in import/ai/read/art.rs:15 and its path in
import/ai/frames.rs:26), and the tests read the written data back
through that reader. The scripting
reference documents the same attributes on the reading side (text frame
kinds, columnCount, columnGutter, rowCount, startTValue, endTValue,
orientation, and the character attributes):
https://web.archive.org/web/20160410101607/https://wwwimages.adobe.com/content/dam/Adobe/en/devnet/illustrator/sdk/CC2014/Illustrator%20Scripting%20Reference%20-%20JavaScript.pdf

The reader now closes a text object's frame path painted with a closing
operator (n, f, s, b, h), so area type's frame comes back closed; places
point type whose story keeps no composed lines at its frame's point;
keeps a story's last empty paragraph (only the \r that ends the story is
dropped); gives type made from the text document its text object's lock,
as it gives it its visibility; and gives type its text object's knockout
shape flag, as it gives it its opacity and blend mode.

Tests: the round trip of every kind of type and its state, a second
write of what is read back giving the same bytes, the text document and
stubs, scaled point type, a story split between threaded frames, the
fallbacks and the warnings, and the engine's editing copy with every save
option; a fuzz case for damaged text documents and stubs.

Docs: docs/file-format-parity.md.
2026-11-07 05:15:15 +07:00
dotCooCoo d45ebe465b .ai save: the editing copy names the active artboard
The editing copy of a .ai or .ait file that VectorCraft saves or exports
now has `%_<n> /Int (CropAreaActive) ,` after the ArtboardArray in its
recorded part, and marks that artboard as the selected one. The app
records its view's active artboard before each command
(DocState::active_artboard; an index past the last artboard is the last,
as in the Artboards panel); without a view, the artboard the document
opened on (Document::opened_artboard) is written, else the first. A .ai
file read from its editing copy opens on that artboard. Before, the copy
named no active artboard and always marked the first as selected.

Docs: docs/mcp.md, docs/file-format-parity.md, the editingData option text.
2026-11-07 00:17:17 +07:00
dotCooCoo 6fef93de8f .ai save: new gradient names continue from the last one given
Each gradient definition that no swatch names took its name by trying
`Gradient`, `Gradient 2`, `Gradient 3`, ... from the start, so a document
with many such gradients tried every earlier name again for each new
one. The writer now keeps the number the next search starts from, and
still skips names that a gradient, swatch or pattern already has.

- A test names 1,000 gradients around two taken names and checks that
  each name is tried once (1,002 tries; 502,496 when each search starts
  over).
- An export test writes 300 gradients next to a `Gradient 3` swatch and
  a `Gradient 7` pattern, and checks the names in order and the same
  output when written again. It passes with the old search too.
2026-11-05 17:59:03 +07:00
dotCooCoo 653e6ff7fb .ai save: the editing copy keeps images and linked files
The editing copy of a PDF-compatible .ai wrote every image as its outline
filled with its average color. Embedded images are now written with their
pixels, and linked images as placed files with a preview:

- Embedded images are rasters in the form of the version 7 specification
  (section 8, https://13thmonkey.org/documentation/fonts/AI7FileFormat.pdf):
  %AI5_BeginRaster, the color space, the image matrix and the XI operands
  of current files, then the samples in binary in a %%BeginData section.
  Gray, RGB and CMYK images keep their samples (CMYK images their ink
  amounts), and see-through images their alpha channel after the color
  samples. The matrix is written to ten decimals.
- Linked images are placed files (section 16): the matrix, the box of the
  file's pixels and its path, %%IncludeFile, the folder relative to the
  document's (when the relative path ends in the path's file name), and a
  preview of at most 256 pixels a side.
- Images that can't be decoded or have more than 33,554,432 pixels (the
  most the reader reads) are written as their outlines filled mid gray,
  images of 16 bits a channel (CMYK TIFFs included) at 8 bits, and linked
  images in opacity masks and pattern tiles as embedded images, each with
  a warning.

Each of these is written in the form VectorCraft's .ai reader reads, and
the tests read the written data back through that reader.

The reader now gives an image its name and opacity mask when the
operators that end its raster (XH, Xr and the N after %AI5_EndRaster) come
before its dictionary, reads an empty relative folder as the document's
own folder, and looks for the %%EndData that ends an image's samples
after the alpha channel its XI operands give it, preferring one whose next
line is XH, so samples that hold the marker's bytes are read whole.

Tests: the round trip of every kind of image and its state, a second write
of what is read back giving the same bytes, the raster and placed-file
forms, previews of large linked images, and the engine's editing copy with
every save option; a fuzz case for damaged images and placed files.

Docs: docs/file-format-parity.md.
2026-11-05 17:38:55 +07:00
dotCooCoo 1f68248bc5 .ai save: the editing copy keeps gradients and patterns
The editing copy for other apps now writes linear and radial gradients and
grid patterns as themselves instead of as average colors and cut tiles.

- Gradients the art uses are defined in the setup (`n Bn`, then
  `%AI5_BeginGradient` … `Bd`, the stops as `Bs` with their opacity and
  midpoints, `BD`), and gradient swatches nothing uses in a non-printing
  block. Stops keep gray, CMYK, RGB and spot colors at a tint. A
  gradient that isn't a swatch gets a name that no swatch or pattern of
  the document has.
- Each fill or stroke with a gradient is an instance around its painting
  operator (`Bb`, `Bh`, `Bg`, `Xm`, `Bm`, `Bc`, `BB`), on a compound
  path's last subpath and on clipping paths too. `Bg` places the gradient
  relative to the path: a linear one by its angle and its start and length
  as fractions of the path's extent along it, a radial one by the matrix
  that takes the unit circle onto its extent ellipse, its center as
  fractions of the path's bounds in that circle's space and its radius as
  a multiple of the one the center's distances to the right side and to
  the bottom give. In a pattern's tile the placement is mirrored top to
  bottom.
- Patterns whose tiles make a grid and hold their art are defined with
  `%AI3_BeginPattern` … `E`, the tile's box first, and fills and strokes
  paint with `p` and `P`; gradient and pattern swatches are in the palette.
- Freeform gradients, gradients along or across a stroke and gradients
  without stops, with more than 256 stops or without extent are written
  as their average color, and other patterns as their tiles cut to the
  shape, each with a warning. Gradients that dither or mix perceptually
  are written as classic ones, stops linked to global process colors as
  their colors, and pattern swatches without the placement that moves,
  turns or scales their pattern, with warnings.
- Reading: an unpainted box that a pattern's definition marks as its
  tile's box (`AIPattern_Editor_Backing_Tile_Rect`) is no longer read as
  art, and a linear gradient whose stops all lie at one place keeps each
  stop on its side.

The definitions, instances and patterns follow sections 3.4, 3.5 and 14
of the version 7 specification
(http://mercator.elte.hu/~saman/hu/okt/AI7FileFormat.pdf). The stop
opacity, `Bg`'s placement and the marked tile box follow the newer form
that current .ai files carry, which that specification predates.

Each of these is written in the form VectorCraft's .ai reader reads, and
the tests read the written data back through that reader.

Docs: docs/file-format-parity.md.
2026-11-05 15:38:38 +07:00
dotCooCoo 31250ad315 .ai save: guides in the editing copy end with the guide operator only
The editing copy of the art now writes a guide as its path followed by
`(N) *`, or `(n) *` when the path is closed, without the `(N) *` before
the path. The legacy version 7 .ai keeps both, as section 6 of the
version 7 specification shows them
(http://mercator.elte.hu/~saman/hu/okt/AI7FileFormat.pdf).
2026-11-05 15:26:36 +07:00
dotCooCoo 0990bfb244 .ai save: the editing copy keeps transparency, clipping and opacity masks
The editing copy of the art that a PDF-compatible .ai carries now holds:

- Transparency: `mode opacity isolate knockout shape Xy` (the blending
  mode's number, Knockout Group 0 off, 1 on, 2 neutral), written when it
  changes: in a path's paint after its colors, before a guide, and after
  a group's end followed by `0 0 Xd` and `6 () XW`. A group without
  transparency of its own ends `9 () XW`, and a group's name comes
  before its state block. A layer's transparency follows its art: `Xy`,
  `0 0 Xd`, `7 () XW`, `LB`.
- Clipping groups as `q`, the art, the clipping path or compound path last
  with `h` (or `H`) and `W` before its painting operator, `Q`, as section
  5.6.2 of the version 7 specification shows them
  (http://mercator.elte.hu/~saman/hu/okt/AI7FileFormat.pdf). A clipping
  layer whose clipping object is a path or compound path sets the last
  `Lb` flag and writes its clipping path last.
- Opacity masks as an `AI9_Mask` entry in the object's dictionary: the
  mask's art between `X=` and `X+`, and Clipping, Inverted, Disabled and
  Linked.

Before, clipping groups were written as their art cut to the clipping
path, and transparency and opacity masks were left out. Still left out,
with a warning: the opacity and blending mode of single fills and
strokes, opacity masks on layers (disabled ones too), and opacity masks
nested more than 8 deep. A clipping group or layer whose clipping object
isn't a path or compound path is written as a group of the art cut to
that object, in a layer that doesn't clip, and so are the clipping
groups inside that art. A clipping path's paint that a plain fill and
line can't draw is written as paths inside the clipping, fills under the
art and strokes over it, each side in a group of its own when it has
several, with the clipping path's transparency, lock and hidden state;
the clipping path's opacity mask stays on the clipping path, with a
warning. A clipping group without members is an empty group, and an
empty clipping layer is an empty layer.

Each of these is written in the form VectorCraft's .ai reader reads, and
the tests read the written data back through that reader.

Reading .ai editing data: an `Xy` after a layer's last group or compound
path, followed by the layer's `7 () XW`, is the layer's (a following
`6 () XW` keeps it the group's); groups, compound paths and layers keep
Opacity & Mask Define Knockout Shape.

The `.ai` option `editingData` and docs/file-format-parity.md describe
what the copy now holds.
2026-11-04 22:29:43 +07:00
dotCooCoo 41477041ea .ai save: the editing copy keeps artboard display marks and dense patterns
Each artboard's DisplayMark was written as 0. It now holds the center
mark (1), cross hairs (2) and video safe areas (4), as the reader reads
them.

A pattern fill that would place more tiles than a fill places came back
with no tiles and was written as an empty group, so the shape was
missing from the editing copy. Such a shape is now filled gray, as a
pattern that isn't found is, with a warning.
2026-11-03 16:19:15 -08:00
dotCooCoo f87ba9224c .ai save: artboards larger than the canvas are written on a large canvas
Artboards that together did not fit on the 16,383 pt canvas were
written with %AI24_LargeCanvasScale: 1, so part of them lay outside
the canvas. The editing data now declares a scale of 10 for them and
writes the art, boxes, artboards, strokes and effects at 1/10 of their
size, symbol and pattern art included.

Reading such a .ai, the editing data comes in at its declared scale
when it matches the page that way rather than in the units of the
page's UserUnit, also when the PDF part can't be read, and a file saved
without its PDF part applies the declared scale too.
2026-11-03 16:02:28 -08:00
dotCooCoo 5df9b2d1e0 .ai save: nine-slice symbol placement counts toward the symbol art limit 2026-11-02 18:59:11 -08:00
dotCooCoo 029661cf78 .ai save: the editing copy places symbol instances through their 9-slice guides
A scaled instance of a symbol with 9-slice guides was written with its art stretched as a whole. The editing copy now places it as the canvas draws it, and leaves the instance out when placing it would pass the symbol art limit.
2026-11-02 18:55:11 -08:00
dotCooCoo bb80e741bf .ai save: the editing data tests build static symbols
Symbol has a dynamic field on main; the tests set it to false.
2026-10-31 20:39:46 -07:00
dotCooCoo 5dc36cac8d .ai save: the editing data comments say what VectorCraft writes
The comments on the %%Creator line and on the artboards' Recorded and
NotRecorded parts of the document dictionary now describe what the
writer writes.
2026-10-31 20:19:57 -07:00
dotCooCoo b4e28e504a .ai save: write the artboards in the recorded part of the document data
The editing data put ArtboardArray and AIDocumentCanvasSize in the part
of the /Document dictionary marked /NotRecorded. Apps that read the editing
data do not read artboards from there: a file opened with one default
artboard the size of the first, the other artboards and the names were
lost, and the art landed one artboard's height above it. The two entries
now close a /Recorded part, followed by an empty /NotRecorded one, as in
the files users sent.
2026-10-31 08:53:28 -07:00
dotCooCoo 25a193e5a9 Save As .ai: bound the symbol art the editing data writes
Instances of symbols that hold instances of other symbols multiply, and the
editing data wrote every level for each instance above it, limited only by
nesting depth: eight levels of 100 instances each could hang a save or run
out of memory. The writer now counts the objects of symbol art it writes,
symbol instances and inline graphics alike, and leaves out instances past
16,000,000 objects in all, as the PDF part does, with a warning.
2026-10-28 22:03:38 -07:00
dotCooCoo 00fe031879 Save As .ai: write inline graphics in type as their art in the editing data
Type is written as its outlines, and the outlines leave out inline
graphics, so the editing data lost them while the PDF page drew them. Each
inline graphic is now written as its symbol's art where the layout places
it, over the characters and under the object's own fills and strokes above
the Characters row, in the order the PDF page draws them.
2026-10-28 21:58:55 -07:00
dotCooCoo 80c43c8133 Save As .ai: keep the group fills and strokes of pattern tiles in the editing data
A pattern fill is written as its tiles cut to the shape. A group in the
tile art with fills or strokes of its own was written as its members alone,
without that paint, because only the layers and the symbols' art were baked
before writing. The patterns' tile art is now baked the same way as the
symbols' art, so its group paint and geometry effects are written as plain
paths.
2026-10-28 21:52:04 -07:00
dotCooCoo 9cd5e3df8f Save As .ai: place the editing data's art on its artboard and carry it on every page
The editing data wrote art space with its origin at the first artboard's
bottom-left corner, its artboard records lacked keys a reader requires, and
its %%Creator line made the reader measure %AI3_TemplateBox on an
8640-point canvas. Other apps opened the art 3,871.5 points away from its
artboard.

Art space is now ruler space, as in the .ai files users sent: y up from the
first artboard's top-left corner. The artboards together are centered on the
16383-point canvas. Each artboard's record gives its top-left corner there
(RulerOrigin), its pixel aspect ratio (PAR), DisplayMark and whether it is
selected, and %AI3_TemplateBox holds the canvas's center. %%Creator names
the format's app and holds no version number. %AI3_Cropmarks and
%AI3_TileBox give the first artboard, %AI5_ArtSize is 14400 14400, and the
/Document dictionary gives the canvas size.

A file with several artboards carried the data on its first page only, and
other apps opened it as a PDF, page by page. Every page now refers to the
one data dictionary, as the files users sent do.

The format details come from users' own .ai files, static analysis of
Illustrator's program and the user's manual testing in Illustrator.
2026-10-28 21:47:14 -07:00
dotCooCoo a5be18b693 Save As .ai: name dots and corner-fitted dashes in the editing data's stroke warning
Dotted strokes (zero-length dashes with round or projecting caps) and dashes
fitted to corners are written as filled outlines in the editing data for
other apps. The warning that lists the strokes written that way now names
them.
2026-10-28 17:57:36 -07:00
dotCooCoo b8279befba Save As .ai: write the editing data's header, palette and layers in the full layout
The editing data now has the layout of the .ai files it was learned from:

- Lines end in CR LF. Line breaks inside strings stay escaped.
- The header adds %%For (the document's author), %%Canvassize 16383,
  %AI7_ImageSettings, %AI5_ArtFlags, %AI5_TargetResolution,
  %AI5_OpenViewLayers (a digit for each layer, 7 shown and 6 hidden) and
  %AI7_GridSettings (the document's grid spacing and subdivisions), in the
  order those files have them.
- The setup starts with 0 Bn (no gradient definitions).
- The palette starts with the Registration swatch: Xs in a CMYK document,
  Xz with black's CMYK equivalent in an RGB one. Reading the file back does
  not add a swatch for it.
- Each layer and sublayer carries its name as an XML id in an /ArtDictionary
  after Ln. An object named like a layer gets a numbered id.
- The LastModified dates of the PieceInfo dictionaries end in +00'00'.
2026-10-28 17:45:42 -07:00
dotCooCoo f33617142c Save As .ai: carry editing data that other apps open as layers and paths
A PDF-compatible .ai now also carries, on its first page's PieceInfo, an
editing copy of the art in the form VectorCraft's own .ai reader reads:
AIPrivateData streams of 64 KiB holding the program compressed with
Zstandard after %AI24_ZStandard_Data, an AIMetaData stream of its header
comments, and the ContainerVersion, CreatorVersion, NumBlock,
RoundtripStreamType and RoundtripVersion entries.

The program holds the header comments, the color swatches and color
groups, every artboard, layers and sublayers (name, order, visibility,
lock, color, printing, dimming), groups, paths, fills and strokes (width,
caps, joins, miter limit, dashes, overprint, fill rule), process, spot and
global colors, guides, hidden and locked objects and object names. Type,
gradients, patterns, images, symbols, live objects, brushes, clipping
groups and compound paths are written as plain filled and stroked paths,
each with a warning; transparency and effects other than geometry effects
are left out.

The .ai option editingData (on by default, Include Editing Data for Other
Apps in Save As) turns it off. The legacy version 7 writer is unchanged;
the path, guide and line style operators both writers write move to
eps/src/ai_ops.rs. LoadOptions::native off opens a file from its editing
copy or pages instead of the VectorCraft document it carries.
2026-10-28 16:55:41 -07:00