Commit Graph
277 Commits
Author SHA1 Message Date
dotCooCoo 405372555d .ai import: the document's graphic styles open in the panel with their objects linked
Squashed from #5155 onto main.
2026-11-02 14:32:53 +07:00
dotCooCoo 0e38796573 .ai import: placed files are found by the folder they keep relative to the document
Squashed from #5176 onto main.
2026-11-02 14:32:53 +07:00
dotCooCoo 56f6128f94 .ai import: a line width of 0 draws as a hairline
Squashed from #5174 onto main.
2026-11-02 14:32:53 +07:00
bflatastic dc363ec356 Tests: Convert to Shape beside Offset Path is a style any object takes 2026-11-02 14:32:53 +07:00
dotCooCoo dbb04905e0 .ai import: Offset Path, Outline Object and Outline Stroke open live
Squashed from #5148 onto main.
2026-11-02 14:32:52 +07:00
bflatastic 645757448f .ai import: the layer-end operator gets its own method 2026-11-02 12:04:26 +07:00
dotCooCoo ccc7e44078 .ai import: multi-layer masks open as a clipping group in each layer
Squashed from #5117 onto main.
2026-11-02 12:04:26 +07:00
dotCooCoo 795cbc060d .ai import: graphs open as their drawn art, with a note
A graph in a legacy .ai, or in a current file's editing data, is written
as `%_Gs`, its data and settings on `%_` lines, its drawn art in a group
with a `%_... Go` tag after each object, and `%_GS`. The reader stopped
on the graph operators as ones it doesn't know, so such a file did not
open through its layers.

The reader now leaves the graph's records out with their operands and
reads the drawn art as usual: the bars, lines and labels come in as the
groups, paths and type they are written as. Paint operators on `%_`
lines between `Gt` and `GT` set nothing. One note per file says the
graphs came in as plain art without their data or settings. A graph
that starts inside another, or doesn't end before its layer does, keeps
the art read so far with a second note. Graph operators outside a graph
stay unknown.
2026-11-02 12:04:24 +07:00
dotCooCoo 69e5eecde0 .ai import: artboard display options, raster effects settings and the header's author come in
Squashed from #5102 onto main.
2026-11-02 12:04:24 +07:00
dotCooCoo 13db3cd602 .ai import: anchors are smooth or corners by the case of their segment operators
In editing data and legacy .ai files, a lower-case l, c, v or y makes the
anchor the segment ends on smooth and an upper-case L, C, V or Y makes it a
corner. The reader used to guess each anchor's kind from whether its
handles are in line, so a lower-case l always read as a corner and an
upper-case C with handles in line read as smooth. A closed path's first
anchor takes the case of the segment back to it. Points and handles do not
move. Paths, compound paths, clipping paths, guide paths, area type frames
and type-on-a-path paths all keep the kinds.
2026-11-02 12:04:23 +07:00
dotCooCoo beb89ebec4 .ai import: vertical type keeps its orientation and Tate-chu-yoko runs
Squashed from #5086 onto main.
2026-11-02 12:04:08 +07:00
dotCooCoo 13fef54e7f .ai import: cap area type gutters after scaling
Gutters and the first-baseline minimum were capped at 10,000 before the
art's scale was applied, so a Large Canvas file could bring in a gutter
of up to 100,000 pt, past the limit text.areaOptions sets. The cap now
also applies to the scaled value.
2026-11-02 12:04:07 +07:00
dotCooCoo f46427167c .ai import: area type keeps its rows, columns, gutters and first baseline
Area type made from the editing data's text document came in with one
row, one column, the 18 pt gutter and the Ascent first baseline,
whatever its frame kept. Each area frame now keeps its rows and columns,
the gutter between rows and the gutter between columns, and its first
baseline (Fixed, Cap Height, Leading or x Height) with its minimum,
scaled with the art's units. Each frame of a thread keeps its own, and a
key that is missing or can't be read leaves that option as it was. A
first baseline the frame keeps is no longer replaced by the fixed one
that matches the file's first line.

AreaOptions gets row_gutter (rowGutter, left out while unset), which
layout spaces rows by; text.areaOptions takes and reports rowGutter, a
gutter given alone sets both, and the Area Type Options dialog shows
Row Gutter.
2026-11-02 12:04:07 +07:00
dotCooCoo c3093fcafb .ai import: a wraparound group's graphics wrap with no offset
Every graphic in a version 7 wraparound group (`*w` … `*W`), before
or after its type, is now a wrap object with an offset of 0 instead of
the 6 pt default, and a group with no type in it marks all of its
graphics. A graphic before the type is below it, so the type flows
around it only after it is moved above. Operands on `*w` or `*W`, and
a `*w` inside a compound path, are errors.

Docs: docs/file-format-parity.md.
2026-11-02 12:04:06 +07:00
dotCooCoo d4f5004580 .ai import: area type flows around its text wrap objects
Fixes #2763. An object whose editing-data dictionary holds the text wrap
options keeps them (offset in points, 6 pt by default, clamped to
±1000 pt; Invert Wrap), and the objects after the text in a version 7
wraparound group (`*w` … `*W`) are wrap objects at the default options.
Area type below a wrap object in its layer flows around it before the
layers are compared with the page, and threaded type flows on into its
next frames. A Large Canvas file's offset is scaled with its art.

The wrap resolver moves into `vectorcraft_doc::text::resolve_wrap` for
the import and the edit-time refresh, and builds each wrap object's
outline once per layer. A file whose wrap shapes would copy more than
2,000,000 anchors is not read from its editing data.

Docs: docs/file-format-parity.md.
2026-11-02 12:04:05 +07:00
dotCooCoo 100730d4d3 .ai import: a file whose PDF part can't be read opens from its editing data
Squashed from #5051 onto main.
2026-11-02 12:04:04 +07:00
dotCooCoo b4c01e3120 Tests: move the repeated-records timing test next to the other objects-on-path record tests 2026-11-02 12:03:57 +07:00
dotCooCoo 9596130be4 .ai import: Objects on Path records measure each group once
Each Objects on Path record measured the bounds of every object it names,
and checked its path, before it was turned down. A thousand copies of a
record whose object is a group of 20,000 paths took 5.6 s to open instead
of about 50 ms. A record now checks its group and ids first, and the path
and objects of a group are measured for one record at most.
2026-11-02 12:03:56 +07:00
dotCooCoo bb354fc92a .ai import: page art moved to its artboard takes its pattern tiles along
Art taken from a `.ai`'s PDF part on a later page is moved to where the
editing data puts that artboard. The move left pattern fills and strokes
where they were, so their tiles no longer lined up with the art. This
happened in three places: the page art that stands in for an unread
object (#5050), the page's type given to text objects, and the art on
each page when the file comes in as its PDF part with the artboard
layout kept. Each move now transforms the pattern tiles too.
2026-11-02 12:03:56 +07:00
bflatastic 54b79c02b0 .ai import: Convert to Shape records string entries through the shared dictionary parser 2026-11-02 12:03:54 +07:00
dotCooCoo e130cf3e0c .ai import: Convert to Shape opens live on paths and type
Squashed from #5142 onto main.
2026-11-02 12:03:54 +07:00
dotCooCoo 664fe8a12b .ai import: Stylize effects open with their saved settings
An object whose art style holds Drop Shadow, Inner Glow, Outer Glow,
Feather, Round Corners or Scribble now opens as itself with those
effects live, in their order. Feather is read under the name and key
the files use (`Adobe Fuzzy Mask`, `Radius`).

Each setting is read only when it is written as its kind (`/Int`,
`/Real`, a fill style for a color) with a value in range. An unknown
key or choice, a missing setting, or a Scribble with a color of its own
keeps the drawn look, where the reader used to cast, clamp or default
the value.
2026-11-02 12:03:53 +07:00
dotCooCoo 0ccc6d9df5 .ai import: paths keep their notes and Show Center
Squashed from #5129 onto main.
2026-11-02 12:03:35 +07:00
dotCooCoo c21ff5698e Tests: move the legacy type tests next to the text object tests
The legacy type helpers and tests move from the end of crates/eps/src/import/ai/tests.rs to after text_objects_are_slots_named_after_their_story, and the helper stream7 is renamed type_stream7.
2026-11-02 12:03:33 +07:00
dotCooCoo db11446502 .ai import: legacy type keeps its spacing, hanging punctuation and hyphenation
Legacy type (`To` ... `TO`) now reads `min opt max TC` and `min opt max TW`
into the paragraph's letter and word spacing, `hangingQuotes Tq` into Roman
hanging punctuation, and `hyphenate limit lead tail max Th` into hyphenation
on or off, After First, Before Last and the Hyphen Limit (0 when the limit
flag is 0). Values are kept to the ranges the Justification and Hyphenation
options take, and a spacing triple out of order goes back to its default.

Operands of the legacy type attributes are read by their place: an attribute
whose operands are missing, are not numbers, or are flags other than 0 and 1
stays as it was. Before, a value that was not a number shifted the later
operands (`5 /x 7 Ti` set indents of 5, 7 and 0), and a bad `TA` or `Ta`
turned kerning on or set the alignment to left. One long `Tx` no longer goes
past the reader's limit on characters.
2026-11-02 12:03:33 +07:00
dotCooCoo 06d037c1b2 .ai import: a clipping path keeps its stroke
A clipping path painted with a stroke (`W S`, `W B`) lost the stroke when
a .ai file opened from its editing data. The clipping path now keeps it,
and it paints over the clipped art as a clipping path's stroke does in
VectorCraft. A gradient-filled clipping path stroked with `1 BB` or `2 BB`
put the stroke on the painted copy of its fill, under the clipped art; it
now goes on the clipping path too. Several clipping paths in one group
make one compound path, which keeps their stroke, opacity and blend mode
when they all have the same ones.
2026-11-02 12:03:31 +07:00
Brandon Thomas b676c626e0 Merge pull request #5186 from dotCooCoo/fix/legacy-ai-character-rotation
Legacy .ai: type with rotated characters is written as outlines that keep the rotation
2026-11-01 23:45:34 -05:00
dotCooCoo 880a078053 Legacy .ai: type with rotated characters is written as outlines that keep the rotation
A version 7 text object has no arbitrary-angle character rotation: its
character attributes include baseline shift (Ts), scaling (Tz) and
tracking (Tt), Tv only turns Roman characters 90 degrees in vertical
text, and the positioning operators such as Tm are ignored on reading.
Type with Character Rotation was written as editable text with the
rotation left out, so rotated characters opened upright.

Such type is now written as its glyphs' outlines, the way type 8-bit text
can't hold already is, with the warning "type with rotated characters is
written as outlines, which keep the rotation, and is no longer editable
text". The outlines keep each character's rotation, baseline shift and
scale. Type without rotated characters, and type turned a whole number of
times, is still written as text.

Docs: docs/file-format-parity.md, docs/mcp.md, docs/target-app-parity.md.
2026-11-01 16:35:49 -08:00
dotCooCoo 7984606471 EPS: spot inks named All or None print as spot inks
A Separation space whose colorant is named All paints every plate, and
one named None paints nothing (ISO 32000-1:2008, 8.6.6.4). EPS export
wrote a spot swatch's name as its colorant, so a spot color named All
printed on every plate and one named None did not print.

Such an ink is now written as All Spot or None Spot, numbered All Spot 2
and on when a spot swatch already has that name, in its Separation
space, DeviceN spaces and the %%DocumentCustomColors and
%%CMYKCustomColor comments. At PostScript Level 3, a gradient that mixes
one of these inks with another spot ink or with process or gray stops is
now a DeviceN shading instead of process colors. Registration still
writes All.
2026-11-01 16:32:41 -08:00
dotCooCoo f1fd255120 .ai import: version 7 palettes keep their swatches without names
Version 7 files write each palette swatch as its paint and `Pc`, with no
name. The reader only kept a swatch with a name before `Pc`, so such a
palette gave no swatches and the document got the default set.

A swatch without a name is now named by its spot color, gradient or
pattern, or by its values in the format the swatch import already uses
for unnamed colors ("Gray K=25", "C=25 M=0 Y=0 K=0"). `Pn` and a `Pc`
with no paint before it add nothing. Named swatches keep their names.

An operator the reader does not know, or a `Pg`, now ends the paint
before it, so a later `(name) Pc` no longer takes that paint. The
swatch limit is kept with a running count instead of summing every
group at each `Pc`.
2026-11-01 06:55:53 -08:00
dotCooCoo 427bce12b0 Character: Font Size can measure the cap height, x-height or ICF box
Squashed from #5026 onto main.
2026-11-01 11:08:55 +07:00
dotCooCoo af133b1ee4 .ai import: envelopes open as live envelopes
An envelope in a .ai's editing data opened as its distorted art, with
its mesh and contents dropped. It now opens as a live envelope of its
contents: a warp with its style, bend, distortions and orientation, a
mesh with its points and handles, or a top object with its outline,
with Envelope Options, Fidelity and Edit Contents.

The envelope replaces its drawn art only when VectorCraft draws it like
that art, with a budget so that live envelopes don't make the file open
from its PDF part. When the layers still differ too much from the page,
the drawn art is put back. An envelope that can't be read, or that
draws differently, keeps its drawn art with a note. Envelopes of type
keep their drawn art and hidden type as before.
2026-11-01 11:08:49 +07:00
dotCooCoo c027947362 .ai import: Objects on Path groups open live with their path, Pivot and Rotate
Squashed from #5059 onto main.
2026-11-01 11:08:47 +07:00
dotCooCoo 70651f585e .ai import: template layers and ruler guides open as they were set
The editing data's layer options set a template layer from the flag four
places after the colour index. The colour index of nine-operand options,
and the colour index and dimming percentage of eleven and twelve operand
options, are read from their own places (they were read one place later).

A straight (N) * guide path directly on a layer that runs across the
whole canvas, or from edge to edge of one artboard, opens as a ruler
guide on that layer (an artboard guide of that artboard); other guide
paths stay guide paths. Guides scale with the document when its editing
data is in the units of a UserUnit page, and an EPS that keeps only its
first artboard keeps only that artboard's guides.

Fixes #779
2026-11-01 11:08:18 +07:00
dotCooCoo 18b9c6113e .ai import: one object the reader can't read comes in as the page's art, and the layers stay
Squashed from #5050 onto main.
2026-11-01 09:24:23 +07:00
dotCooCoo 1840b9a582 EPS export: gradients of spot colors keep their spot inks
EPS export wrote every gradient with a spot color stop in process
colors. A gradient of tints of one spot color and paper white is now
written in that ink's Separation space, at Level 2 and 3. At Level 3, a
gradient that mixes a spot color with CMYK or gray colors, or two spot
colors, is written in a DeviceN space of the process inks its stops use
and the spot inks, each spot ink with the CMYK equivalent its Separation
space has. An off-center midpoint is a stop with the inks halfway
between its two stops. Paper white is an exact white, whether or not
the stop is linked to a process swatch.

These gradients are still written in process colors, each with its own
warning: a mix with other inks at Level 2; a mix with RGB or Lab colors
in an RGB export (a CMYK export converts those stops to CMYK first); a
DeviceN space that would need more than 32 colorants or a spot ink named
Cyan, Magenta, Yellow, Black, All or None; and a Perceptual gradient.
The old warning for every spot gradient has been removed.

The header lists two or more spot colors with each %%+ line right after
the comment it continues: %%DocumentCustomColors and %%CMYKCustomColor
were interleaved, so a DSC reader took the second ink's name as part of
the first ink's %%CMYKCustomColor.
2026-11-01 06:11:12 +07:00
bflatastic f22caa6108 Fix #4938: Dynamic symbols keep per-instance appearance linked to the symbol 2026-11-01 04:53:38 +07:00
dotCooCoo d2d4315524 .ai import: a Zstandard frame that ends in a zero byte keeps it
The reader cut every trailing zero byte off the Zstandard editing data
before decoding it, to drop the padding after the last frame. A frame can
end in a zero byte itself (a raw or RLE block, or a checksum whose last
byte is zero), and cutting it left the frame short, so the file failed to
open with "its Zstandard compression is damaged". The reader now decodes
frame after frame and stops when only zeros are left.
2026-11-01 01:22:08 +07:00
dotCooCoo d6297dbfb9 .ai import: type comes from the text document alone when every shown story reads
Squashed from #4865 onto main.
2026-11-01 00:35:35 +07:00
dotCooCoo bb6cbfb2b4 Images: SVG and EPS export write colorized grayscale and 1-bit images in their fill color
SVG export ignored an image's colorize paint and wrote the image's own
black, white and gray pixels. EPS export flattened every colorized image
into a process-color raster, so a spot color lost its plate. Both now
write the image in the fill color, with the coverage the canvas and PDF
export use (darkness times alpha), so white and transparent pixels take
no color.

- SVG writes a coverage PNG as a mask over a rectangle in the color,
  shared between repeated images while each keeps its own color. Linked
  mode writes the coverage PNG as a file, and SVG Tiny 1.2 writes a PNG
  in the color. A colorized image whose pixels are missing, or whose
  grayscale pixels can't be decoded, is left out with a warning.
- EPS writes fully covered or uncovered pixels as an imagemask in the
  color's process or Separation ink (Registration on every plate), and a
  grayscale image as ink samples scaled by coverage. Level 3 masks its
  uncovered pixels out and Level 2 writes them white; gray midtones are
  written over white, with the existing warnings.
- EPS flattening no longer counts a solid colorize paint on a grayscale
  or 1-bit image as transparency by itself. Opacity, blending modes,
  opacity masks, effects and overlapping transparent art still flatten
  it, and so does a colorize paint the writer can't draw.

The svg crate's tests turn on resvg's raster-images feature to render
the embedded PNGs.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-01 00:35:27 +07:00
bflatastic 13bac047f3 EPS import: spend keeps its own doc comment 2026-10-31 19:43:17 +07:00
marshiyar bd0601c780 EPS import: big operators are charged for the bytes and objects they touch (#3584)
putinterval, copy of a string, roll, counttomark, clear to mark and cvn
worked on up to 4 MiB or 100,000 objects for the one operation each is charged,
so a small file looped over them could keep the importer busy for hours within
the operation budget. Each now spends one operation per 256 bytes or objects.

Fixes #3584
2026-10-31 19:43:17 +07:00
Don Omondi 7403003fb1 fix(eps): an envelope's contents being edited aren't read as art
An envelope whose contents are being edited is written the other way
round from one that isn't: its contents, a group on `%_` lines, then
its dictionary, which holds the mesh that shapes them. Only a group of
meshes was left out before an envelope's dictionary, so those contents
were read as art and their undistorted shapes drew over the distorted
art that follows on plain lines (and that the page draws).

The group on `%_` lines before an envelope's dictionary is now left
out whatever it holds; text in it stays as hidden text, as the
dictionary's own source text does.

# Conflicts:
#	docs/file-format-parity.md
2026-10-31 19:43:17 +07:00
Don Omondi 5b15b3f56b fix(eps): a transparency set in a dictionary's art stays after it
Art inside a dictionary in the editing data, such as an area text's
frame (`/ConfiningPath`) or a mask's art, can set a transparency
(`Xy`). The writer goes on from it: the objects after an area text
whose frame sets one have no `Xy` of their own. The reader dropped
that state when the dictionary closed, so those objects took the
transparency before the dictionary instead; in a user's file a Lighten
glow before an area text made everything after it fade into the light
background.

The last transparency the art inside a dictionary sets is now the
current one after it (and the enclosing dictionary's). Dictionaries
outside the layers (the setup's styles) are left as they were.

# Conflicts:
#	docs/file-format-parity.md
2026-10-31 19:43:17 +07:00
Don Omondi 75d2736a2a fix(eps): area type's first line goes where the file lays it out
Area type made from the editing data's text document was laid out with
its first baseline the font's ascent below the frame's top. The text
document keeps where each of its lines starts, and the file's page draws
them there: for some fonts the first line sits well above the font's
ascent (Oswald's at about its cap height), so headings came in lower
than on the page and the layers were taken to differ from it.

When the file's first line is more than half a point from where it is
laid out here, the first baseline is now fixed where the file has it
(First Baseline: Fixed), measured in the text document's own frame so a
copied object's type moves with its frame.

# Conflicts:
#	docs/file-format-parity.md
2026-10-31 19:43:17 +07:00
Don Omondi 3602bd6ea3 fix(ai-import): type from the text document takes its character style's attributes
Type made from an Illustrator file's text document took each character
attribute its run doesn't set from the document's base attributes, not
from the character style the run names ([Normal Character Style] or
another of the file's). Where the two differ the type came in wrong: a
file whose base fill is white and whose Normal Character Style is black
came in with its text white, invisible, so the layers differed from the
page wherever the page's own type didn't stand in for it.

A run now takes its own attributes, then its character style's (and the
styles that one is based on, up to 16), then the document's.
2026-10-31 19:43:17 +07:00
Don Omondi d4745fe046 fix(eps): a layer's state after its last blend stays the layer's
#4757 gives a blend the Xy written after its record. When the blend is a
layer's last object, the layer's own state block (Xy, Xd, 7 () XW, LB)
went to the blend instead, and the layer stayed opaque. The blend's state
is now tentative like a mesh's (#4748): its own 6 () XW confirms it, the
layer's LB gives it to the layer and the blend keeps what it had.

Also records the .ai/EPS audit fixes under G9, the File formats row and M4.
2026-10-31 05:11:04 +07:00
Don Omondi aa6a960062 fix(ai-import): compare a PDF of some artboards on the artboard its page shows
A PDF saved with Illustrator editing data for only some of its artboards
has pages for those artboards, while the editing data keeps them all. The
layers were compared with the first page on the first artboard, so a page
showing a later artboard never matched and the file came in as its PDF
part, without its layers.

With fewer pages than artboards, the first page is now taken to be the
artboard of its size whose art draws most like it (up to 16 weighed), and
the pages' type goes to that artboard and the ones after it. All the
artboards still come in, and the comparison still decides whether the
layers are used.
2026-10-31 04:55:14 +07:00
Don Omondi c4d2480132 .ai/EPS import: a blend takes the transparency written after its record
Squashed from #4757 onto main.
2026-10-31 04:39:05 +07:00
Don Omondi a1fc3cfdb1 fix(eps): a masked gradient mesh takes the transparency after its mask
A gradient mesh with an opacity mask has its own state (`Xy`) after the
mask's dictionary, not right after its `/End X!`. The operators of the
mask's art, on `%_` lines, cleared the flag that says a mesh just ended,
so the state was read as the next object's and each masked mesh in a
run drew with the blend mode of the one before it.

The flag now goes back to what it was when a dictionary opened once it
closes, as `after_object` does.
2026-10-31 04:21:56 +07:00