mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 06:31:19 +00:00
.ai save: the editing copy keeps symbols and their instances
The editing copy of a PDF-compatible .ai wrote every symbol instance as its art. Symbols are now written as symbols, in the form the .ai reader reads them: - Every symbol is defined in the setup, in a non-printing block after the patterns: %AI14_BeginSymbol, its name, its art in the symbol's own space, its registration point as AI13PatternRegistrationType and %AI10_EndSymbol, each after the symbols its art uses. The nine points are numbered 1 to 9 row by row from the top left, in the order the scripting reference lists them. A definition is written the same wherever its first instance lies. The symbol list after the palette keeps the Symbols panel's order. A hidden root of a symbol's art is written shown, as the canvas draws it. - Each instance is a /SymbolInstance with its symbol's name and the matrix that places the art (to ten decimals), followed by its name, opacity mask, opacity and blending mode as a group's. In a symbol's art, the matrices of instances, gradients and images are written in the canvas's space about the canvas point of the ruler origin. - Pattern fills in a symbol's art are written as their tiles cut to the shape. A symbol whose definition holds such tiles, from its own objects, a brushed stroke's brush art, a live object, an opacity mask, a nested symbol or an inline graphic, has its instances written as their art. - Instances with a fill, appearance changes, isolation, knockout or knockout shape of their own, instances of symbols with 9-slice guides or pattern paint, instances in pattern tiles or in art cut to a clipping object, instances scaled almost flat, instances that turn, mirror or scale symbols holding type, live objects, instances written as their art, shapes with rounded live corners or strokes written as outlines or brush art (the canvas works these out again where the instance puts them), instances that scale their symbol more than 100 times, nested instances included (numbers are written to four decimals), and instances on the layers of a large canvas are written as their art (the symbols are still defined). Symbols past the 4,096 the reader keeps are left out and their instances written as art; an instance of a symbol the document doesn't have is an empty group. 9-slice guides, export types and the dynamic type are left out, and linked images and type in a symbol's art are embedded and written as outlines. Each comes with a warning. The writer takes the reader's limit of 4,096 definitions (MAX_SYMBOLS) and its symbol_art_member check, which now are pub(crate). The editingData option's description lists what the copy now holds, type written as text included. Symbols and their instances: https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/edit-symbols.html https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-and-place-symbols.html The nine registration points, in the order the scripting reference lists them: https://web.archive.org/web/20180219005134/https://wwwimages.adobe.com/content/dam/acom/en/devnet/illustrator/pdf/Illustrator-Scripting-Reference-JavaScript-cs6.pdf#page=252 Tests: the round trip of symbols, nested symbols and instances in every state, symbols whose art is a group with a state of its own, images and gradients in a symbol's art, every registration point, symbol names with special characters, duplicate and missing symbols, a symbol first used deep in opacity masks, symbols past the reader's count (one found only while its holder's art is written included), a large canvas, symbols with pattern fills (directly, nested, in an opacity mask, through an inline graphic and through a brushed stroke), scaled instances of symbols with an outside stroke, rounded live corners, type, a blend, a tiny or thin object, an ellipse or an instance of a symbol with 9-slice guides, a symbol whose root is hidden, instance matrices from a fixed seed, a second write of what is read back giving the same bytes, the forms written, the instances written as art and the options reported. Docs: docs/file-format-parity.md, README.md, ROADMAP.md, docs/gaps.md, docs/roadmap.md and docs/target-app-parity.md describe the editing copy a saved .ai holds.
This commit is contained in:
@@ -253,7 +253,7 @@ The scores are self-assessed from our code against public documentation;
|
||||
[target-app-parity.md](docs/target-app-parity.md#how-this-was-measured-2026-10-10) explains how far to trust them.
|
||||
|
||||
**What's missing:**
|
||||
- reliable exchange of .ai files (layer structure in real files; the editing copy a save writes leaves out live effects and writes symbols and live objects as their art);
|
||||
- reliable exchange of .ai files (layer structure in real files; the editing copy a save writes leaves out live effects and writes live objects as their art);
|
||||
- an interaction-fidelity pass covering every tool's modifiers and small behaviours;
|
||||
- 3D and Materials beyond the initial live Revolve (Extrude & Bevel, Inflate, Rotate, materials);
|
||||
- filter thumbnails and a preview area inside the Effect Gallery (the gallery itself and all 57 Photoshop-style raster filters, approximated, are in);
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ and the work list in [`docs/gaps.md`](docs/gaps.md).
|
||||
**Why alpha:** the core workflows exist end to end (drawing and path editing, Pathfinder, paint and appearance,
|
||||
type, SVG/PDF/EPS, print, export) and feature depth is ~75%, but a professional can't yet switch: Illustrator's
|
||||
own `.ai` files open with a wrong layer structure in real cases and are saved back with an editing copy that
|
||||
leaves out live effects and writes symbols and live objects as their art, the interaction details have never been checked side by side, and some machines fail to start the
|
||||
leaves out live effects and writes live objects as their art, the interaction details have never been checked side by side, and some machines fail to start the
|
||||
app. Beta needs those closed (the beta list in [gaps.md](docs/gaps.md)). It passes the core-workflow
|
||||
gate: all six core workflows (path drawing and editing, paint and style, type, layers/artboards/export, print
|
||||
production, file exchange) work end to end on macOS with save and reopen, and only the last is partial, without
|
||||
|
||||
@@ -109,7 +109,7 @@ pub const AI_OPTIONS: &[FormatOption] = &[
|
||||
name: "editingData",
|
||||
ty: "boolean",
|
||||
default: "true",
|
||||
description: "also carry an editing copy of the artwork in the form other apps that open .ai files read: layers and sublayers, groups, compound paths, paths, fills and strokes, colors, linear and radial gradients, patterns on a grid of tiles, swatches, artboards and which one is active, the transparency of objects and layers, objects' opacity masks, clipping groups and layers clipped by a path, embedded images with their pixels, and linked images as placed files with a preview, with type, other patterns, symbols, other clipping objects and live objects as plain filled and stroked paths, freeform gradients, gradients along or across a stroke and gradients of more than 256 stops as their average color, gradients that dither or mix perceptually as classic ones, and gradient stops linked to global process colors as their colors (written only with every artboard, as Preserve Editing is)",
|
||||
description: "also carry an editing copy of the artwork in the form other apps that open .ai files read: layers and sublayers, groups, compound paths, paths, fills and strokes, colors, linear and radial gradients, patterns on a grid of tiles, swatches, artboards and which one is active, the transparency of objects and layers, objects' opacity masks, clipping groups and layers clipped by a path, embedded images with their pixels, linked images as placed files with a preview, point type, area type and type on a path as text, and symbols and their instances, with other type, other patterns, other clipping objects, other symbol instances and live objects as plain filled and stroked paths, freeform gradients, gradients along or across a stroke and gradients of more than 256 stops as their average color, gradients that dither or mix perceptually as classic ones, and gradient stops linked to global process colors as their colors (written only with every artboard, as Preserve Editing is)",
|
||||
},
|
||||
FormatOption {
|
||||
name: "aiVersion",
|
||||
|
||||
@@ -77,7 +77,8 @@
|
||||
//! a preview of at most 256 pixels a side between `%AI26_BeginPlacedObjectPreview` and its end
|
||||
//! (`w h rowBytes 32 17 `, then rows of premultiplied ARGB), each in versioned content, `~`,
|
||||
//! `%AI5_EndPlace`. The box is the file's pixels at art space's origin on the canvas; the matrix
|
||||
//! takes it into art space. In opacity masks and pattern tiles, linked images are embedded.
|
||||
//! takes it into art space. In opacity masks, pattern tiles and symbols, linked images are
|
||||
//! embedded.
|
||||
//! - Type: a text object `/AI11Text :` naming its story and frame (`/StoryIndex`, `/FrameIndex`),
|
||||
//! for area type and type on a path with its frame's path (`/Art :` … `; /ConfiningPath ,`), and
|
||||
//! the text document after the document data (`%AI11_BeginTextDocument`, see
|
||||
@@ -86,26 +87,49 @@
|
||||
//! the alignment, indents, spacing, hyphenation and direction of each paragraph, and the frame:
|
||||
//! point type's point and matrix, area type's frame, rows, columns, gutters and first baseline,
|
||||
//! type on a path's path and span, vertical type, and the frames a threaded story flows through.
|
||||
//! - Symbols, as the reader reads them ([`crate::import`]'s `.ai` reader, `read.rs`): every symbol
|
||||
//! of the document defined in the setup after the patterns, in a non-printing block
|
||||
//! (`%AI14_BeginSymbol`, `(name)`, `0 A`, `0 Xw`, `0 Ae`, the art in the symbol's space with y
|
||||
//! down, the registration point as `AI13PatternRegistrationType` 1 to 9 from the top left, row by
|
||||
//! row (VectorCraft's numbering, in the order the scripting reference lists the points), in the
|
||||
//! art's dictionary, `%AI10_EndSymbol`), each after those its art uses; the symbol
|
||||
//! list after the palette (`%AI24_BeginSymbolList`, the names in the Symbols panel's order,
|
||||
//! `%AI24_EndSymbolList`); each instance as `/SymbolInstance :`, `(name) /SymbolRef ,`, the
|
||||
//! matrix taking the art to art space as `a b c d tx ty /RTransformMatrix`, `;`, then its name
|
||||
//! and opacity mask and a state block as a group's with its opacity and blending mode (an
|
||||
//! instance with isolation, knockout or knockout shape of its own is written as its art). The
|
||||
//! definitions are at most the reader's 4,096. In a symbol's art, an instance's matrix and
|
||||
//! a gradient's take the art to the canvas's space turned y up about art space's origin there.
|
||||
//!
|
||||
//! Lines end in CR LF. Strings are written in UTF-8. Art space is ruler space: y up, its origin at
|
||||
//! the first artboard's top-left corner. On the canvas ([`CANVAS_SIZE`] points square, y down) the
|
||||
//! artboards together are centered; each artboard's `RulerOrigin` is its top-left corner there, and
|
||||
//! `%AI3_TemplateBox` holds the canvas's center in art space. What the form above can't hold is written as plain filled and stroked paths, each such
|
||||
//! loss a warning: type the text document can't hold (in opacity masks, pattern tiles and art cut to
|
||||
//! a clipping object, mirrored, turned or unevenly scaled area type and type on a path, inline graphics, ruby,
|
||||
//! loss a warning: type the text document can't hold (in opacity masks, pattern tiles, symbols and
|
||||
//! art cut to a clipping object, mirrored, turned or unevenly scaled area type and type on a path, inline graphics, ruby,
|
||||
//! gradient or pattern paint, an appearance of its own) as its outlines and its inline graphics'
|
||||
//! art, the character and paragraph attributes the text document doesn't hold, freeform gradients, gradients
|
||||
//! along or across a stroke and gradients without stops, with more than 256 stops or without extent
|
||||
//! as their average color
|
||||
//! (gradients that dither or mix perceptually are written as classic ones, and gradient stops linked
|
||||
//! to global process colors as their colors), pattern fills whose tiles aren't a grid, whose art reaches past
|
||||
//! the tile or is painted with patterns as their tiles cut to the shape, clipping groups and clipping layers whose clipping object isn't a path,
|
||||
//! the tile or is painted with patterns, and pattern fills in symbols, as their tiles cut
|
||||
//! to the shape, clipping groups and clipping layers whose clipping object isn't a path,
|
||||
//! and the clipping groups inside them, as their art cut to that object (its fill cut too, its
|
||||
//! stroke not; an image in a clipping group of the region it is cut to), images that can't be
|
||||
//! decoded or have more than 33,554,432 pixels as their outlines filled mid gray, images of 16 bits
|
||||
//! a channel at 8 bits, linked images in opacity masks and pattern tiles as embedded images,
|
||||
//! symbols and live objects (placed documents included) as their art (symbol art past
|
||||
//! [`vectorcraft_doc::MAX_SYMBOL_NODES`] objects in all left out), strokes a plain line can't draw
|
||||
//! a channel at 8 bits, linked images in opacity masks, pattern tiles and symbols as embedded images,
|
||||
//! symbol instances with a fill, appearance changes, isolation, knockout or knockout shape of their
|
||||
//! own, instances of symbols with 9-slice guides or pattern paint, instances in pattern tiles or in art cut to a
|
||||
//! clipping object, instances scaled almost flat, instances that turn, mirror or scale symbols holding
|
||||
//! type, live objects, instances written as their art, shapes with rounded live corners or strokes
|
||||
//! written as outlines or brush art,
|
||||
//! instances that scale their symbol more than 100 times, instances on the layers of a large canvas
|
||||
//! and instances of
|
||||
//! symbols past 4,096 as their art (symbol art past [`vectorcraft_doc::MAX_SYMBOL_NODES`] objects
|
||||
//! in all left out), instances of symbols the document doesn't have as empty groups, symbols' 9-slice
|
||||
//! guides, export types and dynamic type left out, live objects (placed documents included) as their art,
|
||||
//! strokes a plain line can't draw
|
||||
//! as their filled outlines, several fills or strokes as a group of a path for each, freeform
|
||||
//! gradient swatches, gradient swatches of more than 256 stops, swatches of patterns written as
|
||||
//! tiles and the placement of pattern swatches that move, turn or scale their pattern, the opacity and blending modes of single fills and strokes, opacity masks on
|
||||
@@ -206,6 +230,8 @@ pub fn export_ai_editing(doc: &Document, o: &AiEditingOptions) -> Result<AiEditi
|
||||
}
|
||||
d
|
||||
});
|
||||
// Symbols are defined at their own size, whatever the canvas.
|
||||
let symbol_doc = doc;
|
||||
let doc = reduced.as_ref().unwrap_or(doc);
|
||||
let board = doc.artboards.first().map_or(board, |a| a.rect);
|
||||
// The document is y down; art space is ruler space: y up, its origin the first artboard's
|
||||
@@ -213,10 +239,15 @@ pub fn export_ai_editing(doc: &Document, o: &AiEditingOptions) -> Result<AiEditi
|
||||
let to_art = Affine::new([1.0, 0.0, 0.0, -1.0, -board.x0, board.y0]);
|
||||
let origin = ruler_origin(doc, to_art);
|
||||
let mut w = Writer::new(doc, to_art, origin);
|
||||
// On a large canvas the layers' symbol instances are written as their art.
|
||||
w.symbols_live = scale == 1.0;
|
||||
w.symbol_doc = symbol_doc;
|
||||
w.reserve_swatch_gradients();
|
||||
for layer in &doc.layers {
|
||||
w.layer(layer);
|
||||
}
|
||||
// The symbols no instance uses too; their art adds definitions.
|
||||
w.symbols();
|
||||
// The palette first: its gradient and pattern swatches add definitions.
|
||||
let mut palette = String::new();
|
||||
w.palette(&mut palette);
|
||||
@@ -234,7 +265,9 @@ pub fn export_ai_editing(doc: &Document, o: &AiEditingOptions) -> Result<AiEditi
|
||||
let mut s = header.clone();
|
||||
s.push_str("%%BeginProlog\n%%EndProlog\n%%BeginSetup\n");
|
||||
w.definitions(&mut s);
|
||||
w.symbol_definitions(&mut s);
|
||||
s.push_str(&palette);
|
||||
w.symbol_list(&mut s);
|
||||
artboards(&mut s, doc, to_art, origin);
|
||||
if !w.texts.is_empty() {
|
||||
s.push_str(&w.texts.section(doc));
|
||||
|
||||
+1022
-13
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ use crate::ps;
|
||||
|
||||
mod images;
|
||||
mod paints;
|
||||
mod symbols;
|
||||
mod texts;
|
||||
|
||||
pub(super) use images::PAYLOAD;
|
||||
@@ -25,6 +26,8 @@ pub(super) use images::{IMAGE_DEPTH, IMAGES, LINKS_EMBEDDED};
|
||||
#[cfg(test)]
|
||||
pub(super) use paints::{GRADIENT_MIX, GRADIENT_SHAPES, GRADIENTS, STOP_LINKS, SWATCH_PLACEMENT, free_gradient_name};
|
||||
#[cfg(test)]
|
||||
pub(super) use symbols::{MISSING_SYMBOL, SYMBOL_ART, SYMBOL_COUNT, SYMBOL_OPTIONS};
|
||||
#[cfg(test)]
|
||||
pub(super) use texts::TYPE_ATTRIBUTES;
|
||||
|
||||
/// How deep symbols, live objects and pattern tiles may nest in one another (deeper art is left
|
||||
@@ -32,14 +35,14 @@ pub(super) use texts::TYPE_ATTRIBUTES;
|
||||
/// [`vectorcraft_doc::MAX_DEPTH`].
|
||||
const MAX_NEST: u32 = 8;
|
||||
|
||||
pub(super) const TYPE: &str = "type in opacity masks, pattern tiles and art cut to a clipping object, mirrored type, area type and type on a path that is turned or unevenly scaled or whose frame or path has several parts, area type in an open frame, type on an open path whose ends meet, area type that flows around a text wrap, type of only white space, threads of type on a path, threads whose frames differ in orientation or hold a frame written as outlines, type of sizes past what the reader reads, stories past what the reader reads (1,048,576 bytes a story, 16,777,216 bytes in all, and the size of text document it reads), and type with inline graphics, ruby, gradient or pattern paint, or fills, strokes, effects or an opacity mask of the object's own, is written as its outlines in the editing data for other apps";
|
||||
pub(super) const PATTERNS: &str = "pattern fills whose tiles aren't a grid or whose art reaches past the tile, pattern fills in another pattern's tile and pattern fills nested more than 8 deep are written as their tiles cut to the shape in the editing data for other apps";
|
||||
pub(super) const TYPE: &str = "type in opacity masks, pattern tiles, symbols and art cut to a clipping object, mirrored type, area type and type on a path that is turned or unevenly scaled or whose frame or path has several parts, area type in an open frame, type on an open path whose ends meet, area type that flows around a text wrap, type of only white space, threads of type on a path, threads whose frames differ in orientation or hold a frame written as outlines, type of sizes past what the reader reads, stories past what the reader reads (1,048,576 bytes a story, 16,777,216 bytes in all, and the size of text document it reads), and type with inline graphics, ruby, gradient or pattern paint, or fills, strokes, effects or an opacity mask of the object's own, is written as its outlines in the editing data for other apps";
|
||||
pub(super) const PATTERNS: &str = "pattern fills whose tiles aren't a grid or whose art reaches past the tile, pattern fills in another pattern's tile or in a symbol, and pattern fills nested more than 8 deep are written as their tiles cut to the shape in the editing data for other apps";
|
||||
pub(super) const PATTERN_TILES: &str = "pattern fills with more tiles than a fill places are written filled gray in the editing data for other apps";
|
||||
pub(super) const CLIPPING: &str = "clipping groups and clipping layers whose clipping object isn't a path, and the clipping groups inside them, are written as their art cut to that object in the editing data for other apps";
|
||||
pub(super) const CLIP_PAINT: &str = "the fills and strokes of clipping paths that a plain fill and line can't draw are written as paths of their own inside the clipping (fills under the clipped art, strokes over it) in the editing data for other apps";
|
||||
pub(super) const CLIP_PAINT_MASKS: &str = "the opacity masks of clipping paths whose fills and strokes are written as paths of their own stay on the clipping path, not on those paths, in the editing data for other apps";
|
||||
pub(super) const LIVE: &str =
|
||||
"symbols, blends, envelopes, meshes, repeats, placed documents and intertwines are written as their art in the editing data for other apps";
|
||||
"blends, envelopes, meshes, repeats, placed documents and intertwines are written as their art in the editing data for other apps";
|
||||
const STROKES: &str = "strokes a plain line can't draw (inside or outside the path, variable width, dots, dashes fitted to corners, gradients, patterns, arrowheads) are written as filled outlines, and brushed strokes as their brush's art, in the editing data for other apps";
|
||||
pub(super) const ITEM_TRANSPARENCY: &str =
|
||||
"the opacity and blending modes of single fills and strokes are left out of the editing data for other apps";
|
||||
@@ -162,6 +165,49 @@ pub(super) struct Writer<'a> {
|
||||
hidden: u32,
|
||||
/// The text document: the stories of the type written as text.
|
||||
pub(super) texts: texts::TextDoc,
|
||||
/// Symbol instances on the layers may be written as instances ([`Self::live_instance`]); false
|
||||
/// on a large canvas, where they are written as their art.
|
||||
pub(super) symbols_live: bool,
|
||||
/// The document symbols are defined from: on a large canvas, the document at its own size
|
||||
/// (symbols' art is in its own space, which the canvas's scale doesn't change).
|
||||
pub(super) symbol_doc: &'a Document,
|
||||
/// A symbol's art is being written ([`Self::define_symbol`]).
|
||||
in_symbol: bool,
|
||||
/// The document → the space an instance's `RTransformMatrix` takes its symbol's art to: art
|
||||
/// space, and in a symbol's art the canvas's turned y up about art space's origin there.
|
||||
instance_space: Affine,
|
||||
/// The symbol definitions written (`%AI14_BeginSymbol` … `%AI10_EndSymbol`), each after those
|
||||
/// its art uses, and their names.
|
||||
symbol_defs: Vec<String>,
|
||||
defined_symbols: HashSet<String>,
|
||||
/// The symbols whose definitions are being written.
|
||||
defining_symbols: Vec<String>,
|
||||
/// Whether each symbol's own art paints with a pattern, and the symbols its instances use
|
||||
/// ([`Self::symbol_own_patterns`]).
|
||||
pattern_symbols: HashMap<String, (bool, Vec<String>)>,
|
||||
/// Whether each symbol's art paints with a pattern, nested symbols included
|
||||
/// ([`Self::symbol_paints_pattern`]).
|
||||
patterned_symbols: HashMap<String, bool>,
|
||||
/// Pattern tiles were written in the art being written ([`Self::define_symbol`] starts it over
|
||||
/// for each symbol's art).
|
||||
wrote_pattern: bool,
|
||||
/// The symbols whose definitions hold pattern tiles: a brushed stroke's brush art or a live
|
||||
/// object's art can paint with a pattern that the symbol's own objects don't name.
|
||||
pattern_written: HashSet<String>,
|
||||
/// Art that draws as it should only where it is was written in the art being written
|
||||
/// ([`Self::define_symbol`] starts it over for each symbol's art): a stroke as its outline or its
|
||||
/// brush's art, type as its outlines, a live object or a symbol instance as its art, or a shape
|
||||
/// with rounded live corners.
|
||||
wrote_fixed: bool,
|
||||
/// The symbols whose definitions hold such art, nested symbols' included
|
||||
/// ([`Self::keeps_scale`]).
|
||||
fixed_symbols: HashSet<String>,
|
||||
/// The largest scale a live instance in the art being written applies, nested symbols' scales
|
||||
/// included, and that of each symbol's definition.
|
||||
inner_scale: f64,
|
||||
symbol_scales: HashMap<String, f64>,
|
||||
/// The entries a symbol's dictionary adds to its art's dictionary, and the art's id.
|
||||
root_entries: Option<(vectorcraft_doc::NodeId, String)>,
|
||||
}
|
||||
|
||||
impl<'a> Writer<'a> {
|
||||
@@ -195,6 +241,22 @@ impl<'a> Writer<'a> {
|
||||
next: HashMap::new(),
|
||||
brushes: None,
|
||||
symbol_nodes: 0,
|
||||
symbols_live: true,
|
||||
symbol_doc: doc,
|
||||
in_symbol: false,
|
||||
instance_space: to_art,
|
||||
symbol_defs: vec![],
|
||||
defined_symbols: HashSet::new(),
|
||||
defining_symbols: vec![],
|
||||
pattern_symbols: HashMap::new(),
|
||||
patterned_symbols: HashMap::new(),
|
||||
wrote_pattern: false,
|
||||
pattern_written: HashSet::new(),
|
||||
wrote_fixed: false,
|
||||
fixed_symbols: HashSet::new(),
|
||||
inner_scale: 1.0,
|
||||
symbol_scales: HashMap::new(),
|
||||
root_entries: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,6 +456,9 @@ impl<'a> Writer<'a> {
|
||||
if let NodeKind::Layer { .. } = n.kind {
|
||||
return self.write_layer(n, &[]);
|
||||
}
|
||||
if self.in_symbol {
|
||||
self.wrote_fixed |= symbols::redrawn(n);
|
||||
}
|
||||
self.effects(n);
|
||||
self.state(n.locked, !n.visible);
|
||||
let xy = Xy::of(n);
|
||||
@@ -422,10 +487,18 @@ impl<'a> Writer<'a> {
|
||||
NodeKind::Compound { rule, .. } => self.shape(n, &compound_path(n), *rule, xy),
|
||||
NodeKind::Text(_) => self.text(n, xy),
|
||||
NodeKind::Image(im) => self.image(im, xy),
|
||||
NodeKind::SymbolInstance { symbol, xf } if self.live_instance(n, symbol, *xf) => self.symbol_instance(symbol, *xf, xy),
|
||||
NodeKind::SymbolInstance { symbol, xf } => {
|
||||
self.warn(LIVE);
|
||||
self.line("u");
|
||||
// The canvas places an instance inside a symbol's art where the outer instance puts
|
||||
// it (9-slice guides, stains and patterns included), and this art doesn't move with it.
|
||||
self.wrote_fixed |= self.in_symbol;
|
||||
let doc = self.doc;
|
||||
if doc.symbols.iter().any(|s| &s.name == symbol) {
|
||||
self.warn(symbols::SYMBOL_ART);
|
||||
} else {
|
||||
self.warn(symbols::MISSING_SYMBOL);
|
||||
}
|
||||
self.line("u");
|
||||
if let Some(sym) = doc.symbols.iter().find(|s| &s.name == symbol)
|
||||
&& self.nest < MAX_NEST
|
||||
{
|
||||
@@ -492,7 +565,8 @@ impl<'a> Writer<'a> {
|
||||
}
|
||||
|
||||
/// `n`'s `/ArtDictionary`, after it, when it has a name (`AI10_ArtUID`, see [`Self::take_id`])
|
||||
/// or an opacity mask (`AI9_Mask`, see [`Self::mask`]).
|
||||
/// or an opacity mask (`AI9_Mask`, see [`Self::mask`]), or is a symbol's art (its symbol's
|
||||
/// entries first, see [`Self::define_symbol`]).
|
||||
fn dictionary(&mut self, n: &Node) {
|
||||
let name = n.name.as_deref().filter(|s| !s.is_empty());
|
||||
let mut mask = n.extras().mask.as_deref();
|
||||
@@ -501,10 +575,17 @@ impl<'a> Writer<'a> {
|
||||
self.warn(DEEP_MASKS);
|
||||
mask = None;
|
||||
}
|
||||
if name.is_none() && mask.is_none() {
|
||||
let entries = match &self.root_entries {
|
||||
Some((id, _)) if *id == n.id => self.root_entries.take().map(|(_, e)| e),
|
||||
_ => None,
|
||||
};
|
||||
if name.is_none() && mask.is_none() && entries.is_none() {
|
||||
return;
|
||||
}
|
||||
self.body.push_str("%_/ArtDictionary :\n");
|
||||
if let Some(e) = entries {
|
||||
self.body.push_str(&e);
|
||||
}
|
||||
if let Some(name) = name {
|
||||
let id = self.take_id(name);
|
||||
let _ = writeln!(self.body, "%_/XMLUID : {} ; (AI10_ArtUID) ,", ps::bytes(id.as_bytes()));
|
||||
@@ -751,6 +832,7 @@ impl<'a> Writer<'a> {
|
||||
AppearanceItem::Stroke(s) if self.direct_stroke(s) => self.path_object(path, rule, None, Some(s), xy),
|
||||
AppearanceItem::Stroke(s) => {
|
||||
self.warn(STROKES);
|
||||
self.wrote_fixed = true;
|
||||
if self.brush_art(path, s, xy) {
|
||||
return;
|
||||
}
|
||||
@@ -812,6 +894,7 @@ impl<'a> Writer<'a> {
|
||||
let flat = FillLayer { paint: Paint::solid(Color::gray(0.5)), ..f.clone() };
|
||||
return self.path_object(path, rule, Some(&flat), None, xy);
|
||||
}
|
||||
self.wrote_pattern = true;
|
||||
let outer = self.clip.clone();
|
||||
let region = self.cut(path, rule);
|
||||
self.clip = Some(region);
|
||||
@@ -850,6 +933,7 @@ impl<'a> Writer<'a> {
|
||||
self.plain_object(®ion, FillRule::NonZero, fill, None, own);
|
||||
}
|
||||
if let Some(s) = stroke {
|
||||
self.wrote_fixed = true;
|
||||
let outline = stroke::outline_region(path, rule, s);
|
||||
let region = self.cut(&outline, FillRule::NonZero);
|
||||
let fill = FillLayer { overprint: s.overprint, ..FillLayer::new(s.paint.clone()) };
|
||||
@@ -913,13 +997,13 @@ impl<'a> Writer<'a> {
|
||||
// ---------- type ----------
|
||||
|
||||
/// Type `n`: a text object stub naming its story in the text document ([`texts`]) when the
|
||||
/// text document can hold it, outside opacity masks, pattern tiles and art cut to a clipping
|
||||
/// object; else its outlines ([`Self::outlined_text`]). A frame of threaded area type names the
|
||||
/// story of its thread, which its first frame holds.
|
||||
/// text document can hold it, outside opacity masks, pattern tiles, symbols' art and art cut to
|
||||
/// a clipping object; else its outlines ([`Self::outlined_text`]). A frame of threaded area type
|
||||
/// names the story of its thread, which its first frame holds.
|
||||
fn text(&mut self, n: &Node, xy: Xy) {
|
||||
let NodeKind::Text(t) = &n.kind else { return };
|
||||
let doc = self.doc;
|
||||
let placed = if self.hidden > 0 || self.clip.is_some() {
|
||||
let placed = if self.hidden > 0 || self.clip.is_some() || self.in_symbol {
|
||||
None
|
||||
} else if let Some(thread) = doc.text_threads.iter().find(|th| th.contains(&n.id)) {
|
||||
// A thread is checked for what it loses, in every frame, when its story is written.
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::ps;
|
||||
pub(in crate::ai_editing) const IMAGES: &str = "images that can't be decoded or have more than 33,554,432 pixels are written as their outlines filled mid gray in the editing data for other apps";
|
||||
pub(in crate::ai_editing) const IMAGE_DEPTH: &str = "images of 16 bits a channel are written at 8 bits a channel in the editing data for other apps";
|
||||
pub(in crate::ai_editing) const LINKS_EMBEDDED: &str =
|
||||
"linked images in opacity masks and pattern tiles are written as embedded images in the editing data for other apps";
|
||||
"linked images in opacity masks, pattern tiles and symbols are written as embedded images in the editing data for other apps";
|
||||
|
||||
/// The most pixels an image is written with: as many as the reader reads.
|
||||
pub(super) const MAX_PIXELS: u64 = 1 << 25;
|
||||
@@ -94,7 +94,7 @@ impl Samples {
|
||||
impl Writer<'_> {
|
||||
/// Image object `im` of transparency `xy`: embedded, a raster ([`Self::raster`]); linked, a
|
||||
/// placed file with a preview ([`Self::placed_file`]), except in a part of the data written on
|
||||
/// `%_` lines. One whose pixels can't be read is its outline filled mid gray. Where the art is
|
||||
/// `%_` lines and in a symbol's art. One whose pixels can't be read is its outline filled mid gray. Where the art is
|
||||
/// cut to a clipping path, the image is in a clipping group of that path.
|
||||
pub(super) fn image(&mut self, im: &ImageObject, xy: Xy) {
|
||||
let doc = self.doc;
|
||||
@@ -120,7 +120,7 @@ impl Writer<'_> {
|
||||
let fit =
|
||||
Affine::scale_non_uniform(f64::from(im.width.max(1)) / f64::from(samples.width), f64::from(im.height.max(1)) / f64::from(samples.height));
|
||||
match &im.link {
|
||||
Some(link) if self.hidden == 0 => self.placed_file(link, &samples, (im.width.max(1), im.height.max(1)), im.xf),
|
||||
Some(link) if self.hidden == 0 && !self.in_symbol => self.placed_file(link, &samples, (im.width.max(1), im.height.max(1)), im.xf),
|
||||
other => {
|
||||
if other.is_some() {
|
||||
self.warn(LINKS_EMBEDDED);
|
||||
@@ -154,9 +154,12 @@ impl Writer<'_> {
|
||||
/// `[matrix] 0 0 w h w h 8 type alpha reserved 1 0 4 4 0 0`, `%%BeginData:` counting `XI`, its
|
||||
/// line end and the color samples, `XI`, the color samples then the alpha channel in binary,
|
||||
/// `%%EndData`, `XH`, `%AI5_EndRaster`, `N`. The matrix is an art-space matrix applied to the
|
||||
/// pixels with their rows running downwards, `(u, -v)`, as §8.1's image matrix.
|
||||
/// pixels with their rows running downwards, `(u, -v)`, as §8.1's image matrix; in a symbol's
|
||||
/// art (outside a pattern's tile in it), a matrix in the canvas's space turned y up about art
|
||||
/// space's origin there, as an instance's matrix in it is.
|
||||
fn raster(&mut self, s: &Samples, xf: Affine) {
|
||||
let m = matrix(self.to_art * xf * Affine::FLIP_Y);
|
||||
let space = if self.in_symbol && self.defining.is_empty() { self.instance_space } else { self.to_art };
|
||||
let m = matrix(space * xf * Affine::FLIP_Y);
|
||||
let (w, h) = (s.width, s.height);
|
||||
let alpha = u8::from(s.alpha.is_some());
|
||||
let mut payload = Vec::with_capacity(2 + s.color.len() + s.alpha.as_ref().map_or(0, Vec::len));
|
||||
@@ -233,7 +236,7 @@ fn matrix(m: Affine) -> String {
|
||||
}
|
||||
|
||||
/// `v` rounded to ten decimals, without trailing zeros (0 when not finite).
|
||||
fn precise(v: f64) -> String {
|
||||
pub(super) fn precise(v: f64) -> String {
|
||||
let r = (v * 1e10).round() / 1e10;
|
||||
if !r.is_finite() {
|
||||
return if v.is_finite() { format!("{}", v + 0.0) } else { "0".into() };
|
||||
|
||||
@@ -341,9 +341,10 @@ impl Writer<'_> {
|
||||
|
||||
/// Can pattern `name` be written as a definition and painted with from here: the document has
|
||||
/// it, its tiles make a grid, its art is within its tile and holds something, and it isn't used
|
||||
/// in another pattern's tile or nested more than [`MAX_NEST`] deep?
|
||||
/// in another pattern's tile or a symbol's art (the reader keeps no symbol whose art paints with
|
||||
/// a pattern) or nested more than [`MAX_NEST`] deep?
|
||||
pub(super) fn pattern_writable(&self, name: &str) -> bool {
|
||||
if !self.defining.is_empty() || self.nest >= MAX_NEST {
|
||||
if !self.defining.is_empty() || self.nest >= MAX_NEST || self.in_symbol {
|
||||
return false;
|
||||
}
|
||||
if self.patterns.iter().any(|p| p.name == name) {
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
//! Symbols: their definitions, the symbol list and their instances (see [`crate::ai_editing`] for
|
||||
//! the form).
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Write;
|
||||
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_doc::{Knockout, Node, NodeKind, SymbolExportType};
|
||||
use vectorcraft_geom::Affine;
|
||||
|
||||
use super::{MAX_NEST, Writer, Xy};
|
||||
use crate::import::MAX_SYMBOLS;
|
||||
use crate::ps;
|
||||
|
||||
pub(in crate::ai_editing) const SYMBOL_ART: &str = "symbol instances with a fill, appearance changes, isolation, knockout or knockout shape of their own, instances of symbols with 9-slice guides or pattern paint, instances in pattern tiles or in art cut to a clipping object, instances scaled almost flat, instances that turn, mirror or scale symbols holding type, live objects, instances written as their art, shapes with rounded live corners or strokes written as outlines or brush art, instances that scale their symbol more than 100 times, instances on the layers of a large canvas and instances of symbols past 4,096 are written as their art in the editing data for other apps";
|
||||
pub(in crate::ai_editing) const SYMBOL_OPTIONS: &str =
|
||||
"the 9-slice guides, export types and dynamic type of symbols are left out of the editing data for other apps";
|
||||
pub(in crate::ai_editing) const MISSING_SYMBOL: &str =
|
||||
"symbol instances of symbols the document doesn't have are written as empty groups in the editing data for other apps";
|
||||
/// The most an instance may scale its symbol's art, nested instances' scales included: the art's
|
||||
/// numbers are written to four decimals, so a scale of 100 places them within 0.005 pt.
|
||||
const MAX_SCALE: f64 = 100.0;
|
||||
pub(in crate::ai_editing) const SYMBOL_COUNT: &str = "symbols past 4,096 are left out of the editing data for other apps";
|
||||
|
||||
/// Whether the canvas works `n` itself (not its children) out again after placing an instance:
|
||||
/// type, live objects and shapes with rounded live corners, whose strokes, steps and corners depend
|
||||
/// on where the instance puts them.
|
||||
pub(super) fn redrawn(n: &Node) -> bool {
|
||||
let rounded = |l: &vectorcraft_doc::LiveShape| match l {
|
||||
vectorcraft_doc::LiveShape::Rectangle { radii, .. } => radii.iter().any(|r| *r > 0.0),
|
||||
vectorcraft_doc::LiveShape::Polygon { radii, .. } | vectorcraft_doc::LiveShape::Path { radii, .. } => radii.iter().any(|r| *r > 0.0),
|
||||
vectorcraft_doc::LiveShape::Ellipse { .. } | vectorcraft_doc::LiveShape::Line { .. } => false,
|
||||
};
|
||||
match &n.kind {
|
||||
NodeKind::Path { live: Some(l), .. } => rounded(l),
|
||||
NodeKind::Text(_)
|
||||
| NodeKind::Blend { .. }
|
||||
| NodeKind::Envelope { .. }
|
||||
| NodeKind::Mesh(_)
|
||||
| NodeKind::Repeat(_)
|
||||
| NodeKind::PlacedDocument(_)
|
||||
| NodeKind::Intertwine { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
impl Writer<'_> {
|
||||
/// Whether instance `n` of symbol `symbol`, placed by `xf`, is written as an instance: not on the
|
||||
/// layers of a large canvas, in a pattern's tile or in art cut to a clipping object, without a
|
||||
/// fill or appearance changes of its own (which stain or change the art it draws) and without
|
||||
/// isolation, knockout or knockout shape of its own (which an instance's transparency doesn't
|
||||
/// hold), of a symbol without 9-slice guides and without pattern paint in its art (the canvas
|
||||
/// keeps an instance's pattern tiles where the document puts them, and a definition's art moves
|
||||
/// with each instance) whose definition can be written ([`Self::define_symbol`]), by a matrix
|
||||
/// that keeps the art's area as it is written and scales it as the definition allows
|
||||
/// ([`Self::keeps_scale`]).
|
||||
pub(super) fn live_instance(&mut self, n: &Node, symbol: &str, xf: Affine) -> bool {
|
||||
let own = vectorcraft_brush::stain(n).is_some() || !n.extras().symbol_overrides.is_empty();
|
||||
let group_state = n.isolate || n.knockout != Knockout::Neutral || n.knockout_shape;
|
||||
// The matrix as written (to ten decimals) must still keep the art's area.
|
||||
let m = Affine::new((self.instance_space * xf).as_coeffs().map(|v| (v * 1e10).round() / 1e10));
|
||||
(self.symbols_live || self.in_symbol)
|
||||
&& self.defining.is_empty()
|
||||
&& self.clip.is_none()
|
||||
&& !own
|
||||
&& !group_state
|
||||
&& self.doc.symbol_nine_slice(symbol).is_none()
|
||||
&& m.as_coeffs().iter().all(|v| v.is_finite())
|
||||
&& m.determinant().abs() >= 1e-12
|
||||
&& self.define_symbol(symbol)
|
||||
&& !self.symbol_paints_pattern(symbol)
|
||||
&& self.keeps_scale(symbol, xf)
|
||||
}
|
||||
|
||||
/// Whether instance matrix `xf` places symbol `symbol`'s definition as the canvas draws the
|
||||
/// instance. A definition holding art the canvas works out again after placing an instance
|
||||
/// ([`redrawn`], strokes written as outlines or brush art, and instances written as their art)
|
||||
/// takes only matrices that move it;
|
||||
/// any other takes a scale up to [`MAX_SCALE`], nested instances' scales included. A live
|
||||
/// instance in a symbol's art passes both to that symbol.
|
||||
fn keeps_scale(&mut self, symbol: &str, xf: Affine) -> bool {
|
||||
let [a, b, c, d, ..] = xf.as_coeffs();
|
||||
let fixed = self.fixed_symbols.contains(symbol);
|
||||
let scale = a.hypot(b).max(c.hypot(d)) * self.symbol_scales.get(symbol).copied().unwrap_or(1.0);
|
||||
let keeps = if fixed { (a - 1.0).abs() < 1e-9 && b.abs() < 1e-9 && c.abs() < 1e-9 && (d - 1.0).abs() < 1e-9 } else { scale <= MAX_SCALE };
|
||||
if keeps && self.in_symbol {
|
||||
self.wrote_fixed |= fixed;
|
||||
self.inner_scale = self.inner_scale.max(scale);
|
||||
}
|
||||
keeps
|
||||
}
|
||||
|
||||
/// Whether the art of symbol `name`, an opacity mask in it or the art of a symbol it holds an
|
||||
/// instance of, at any depth, fills or strokes something (type included) with a pattern, or its
|
||||
/// definition holds pattern tiles. Worked out once a symbol, after it is defined.
|
||||
fn symbol_paints_pattern(&mut self, name: &str) -> bool {
|
||||
if self.pattern_written.contains(name) {
|
||||
return true;
|
||||
}
|
||||
if let Some(known) = self.patterned_symbols.get(name) {
|
||||
return *known;
|
||||
}
|
||||
let mut seen = HashSet::from([name.to_string()]);
|
||||
let mut todo = vec![name.to_string()];
|
||||
let mut found = false;
|
||||
while let Some(next) = todo.pop() {
|
||||
let (own, used) = self.symbol_own_patterns(&next);
|
||||
if own {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
for u in used {
|
||||
if seen.insert(u.clone()) {
|
||||
todo.push(u);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.patterned_symbols.insert(name.to_string(), found);
|
||||
found
|
||||
}
|
||||
|
||||
/// Whether the art of symbol `name` or an opacity mask in it fills or strokes something (type
|
||||
/// included) with a pattern, and the symbols its instances and inline graphics there use; looked
|
||||
/// at once a symbol.
|
||||
fn symbol_own_patterns(&mut self, name: &str) -> (bool, Vec<String>) {
|
||||
if let Some(known) = self.pattern_symbols.get(name) {
|
||||
return known.clone();
|
||||
}
|
||||
let doc = self.symbol_doc;
|
||||
let mut own = false;
|
||||
let mut used = vec![];
|
||||
if let Some(sym) = doc.symbols.iter().find(|s| s.name == name) {
|
||||
let pattern = |p: &Paint| matches!(p, Paint::Pattern { .. });
|
||||
vectorcraft_doc::swatches::walk_with_masks(&sym.art, &mut |m| {
|
||||
own |= vectorcraft_doc::pattern::uses_pattern(m, None);
|
||||
match &m.kind {
|
||||
NodeKind::Text(t) => {
|
||||
own |= t.runs.iter().any(|r| pattern(&r.style.fill) || pattern(&r.style.stroke));
|
||||
// Inline graphics are written as their symbols' art.
|
||||
used.extend(t.runs.iter().filter_map(|r| r.inline.as_ref()).map(|art| art.symbol.clone()));
|
||||
}
|
||||
NodeKind::SymbolInstance { symbol, .. } => used.push(symbol.clone()),
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
self.pattern_symbols.insert(name.to_string(), (own, used.clone()));
|
||||
(own, used)
|
||||
}
|
||||
|
||||
/// An instance of `symbol` placed by `xf`, of transparency `xy`: `/SymbolInstance :`, `(name)
|
||||
/// /SymbolRef ,`, the matrix taking the symbol's art to art space (in a symbol's art, to the
|
||||
/// canvas's space turned y up about art space's origin there) as `a b c d tx ty
|
||||
/// /RTransformMatrix` (to ten decimals), `;`, then its state block as a group's: `Xy`, `0 0 Xd`
|
||||
/// and `6 () XW` when it has transparency, else `9 () XW`. Its dictionary goes between the two.
|
||||
pub(super) fn symbol_instance(&mut self, symbol: &str, xf: Affine, xy: Xy) {
|
||||
let m = self.instance_space * xf;
|
||||
let _ = write!(self.body, "/SymbolInstance :\n{} /SymbolRef ,\n", ps::bytes(symbol.as_bytes()));
|
||||
for v in m.as_coeffs() {
|
||||
self.body.push_str(&super::images::precise(v));
|
||||
self.body.push(' ');
|
||||
}
|
||||
self.body.push_str("/RTransformMatrix\n;\n");
|
||||
// An instance with isolation, knockout or knockout shape of its own is written as its art
|
||||
// ([`Self::live_instance`]); these are the values of the others.
|
||||
let xy = Xy { isolate: false, knockout: Knockout::Neutral, shape: false, ..xy };
|
||||
self.block_start = self.body.len();
|
||||
if xy == Xy::PLAIN {
|
||||
self.line("9 () XW");
|
||||
} else {
|
||||
self.write_xy(xy);
|
||||
self.line("0 0 Xd\n6 () XW");
|
||||
}
|
||||
self.block_end = self.body.len();
|
||||
}
|
||||
|
||||
/// Write symbol `name`'s definition, once: `%AI14_BeginSymbol`, `(name)`, `0 A`, `0 Xw`, `0 Ae`,
|
||||
/// the art in the symbol's space (y down: its numbers are the symbol's own) as a group (`u` …
|
||||
/// `U`; art that isn't a group, or is a plain group of one object, in a group of its own), the
|
||||
/// symbol's registration point (`AI13PatternRegistrationType`, 1 to 9 from the top left, row by
|
||||
/// row) in that group's dictionary, `9 () XW`, `%AI10_EndSymbol`. Linked images in the
|
||||
/// art are embedded. The symbols its art's instances use are defined first, and the art is
|
||||
/// written from the top of the nesting whatever the instance that asks for it, so a definition
|
||||
/// is the same wherever its first instance lies. False when it can't be: the document doesn't
|
||||
/// have the symbol, it is being written (its art holds an instance of itself), it is reached
|
||||
/// through more than [`MAX_NEST`] definitions being written, or the definitions written and
|
||||
/// being written leave no place under [`MAX_SYMBOLS`].
|
||||
pub(super) fn define_symbol(&mut self, name: &str) -> bool {
|
||||
if self.defined_symbols.contains(name) {
|
||||
return true;
|
||||
}
|
||||
let doc = self.symbol_doc;
|
||||
let Some(sym) = doc.symbols.iter().find(|s| s.name == name) else { return false };
|
||||
if self.defining_symbols.iter().any(|d| d == name) || self.defining_symbols.len() >= MAX_NEST as usize {
|
||||
return false;
|
||||
}
|
||||
// The reader keeps no more definitions than this, and each definition being written takes
|
||||
// one when it ends.
|
||||
if self.symbol_defs.len() + self.defining_symbols.len() >= MAX_SYMBOLS {
|
||||
self.warn(SYMBOL_COUNT);
|
||||
return false;
|
||||
}
|
||||
if sym.dynamic || doc.symbol_export_type(name) != SymbolExportType::default() || doc.symbol_nine_slice(name).is_some() {
|
||||
self.warn(SYMBOL_OPTIONS);
|
||||
}
|
||||
self.defining_symbols.push(name.to_string());
|
||||
let mut used = vec![];
|
||||
vectorcraft_doc::swatches::walk_with_masks(&sym.art, &mut |m| {
|
||||
if let NodeKind::SymbolInstance { symbol, .. } = &m.kind {
|
||||
used.push(symbol.as_str());
|
||||
}
|
||||
});
|
||||
for u in used {
|
||||
self.define_symbol(u);
|
||||
}
|
||||
if self.symbol_defs.len() + self.defining_symbols.len() > MAX_SYMBOLS {
|
||||
self.defining_symbols.pop();
|
||||
self.warn(SYMBOL_COUNT);
|
||||
return false;
|
||||
}
|
||||
// The art is written in the symbol's space, from the state `0 A` and `0 Xw` set, from the
|
||||
// document at its own size.
|
||||
let origin = self.canvas_origin;
|
||||
let about_origin = Affine::new([1.0, 0.0, 0.0, -1.0, -origin.0, origin.1]);
|
||||
let page_doc = std::mem::replace(&mut self.doc, doc);
|
||||
let saved = (
|
||||
std::mem::take(&mut self.body),
|
||||
(self.to_art, self.grad_space, self.canvas, self.instance_space),
|
||||
self.state.replace((false, false)),
|
||||
self.gs.replace(Xy::OPAQUE),
|
||||
self.clip.take(),
|
||||
(self.block_start, self.block_end),
|
||||
(std::mem::replace(&mut self.in_symbol, true), std::mem::take(&mut self.hidden)),
|
||||
self.root_entries.take(),
|
||||
std::mem::take(&mut self.wrote_pattern),
|
||||
(std::mem::take(&mut self.wrote_fixed), std::mem::replace(&mut self.inner_scale, 1.0)),
|
||||
);
|
||||
// Gradients' matrices and instances' matrices are written about the canvas point of art
|
||||
// space's origin, y up; `Bg` places a gradient on the art as it lies in the symbol's space.
|
||||
(self.to_art, self.grad_space, self.canvas, self.instance_space) = (Affine::IDENTITY, about_origin, Affine::IDENTITY, about_origin);
|
||||
self.block_end = usize::MAX;
|
||||
let registration = u16::from(doc.symbol_registration(name).0.min(8)) + 1;
|
||||
let entries = format!("%_{registration} /Int (AI13PatternRegistrationType) ,\n");
|
||||
// The canvas draws a symbol's art even when its root is hidden, so the root is written shown.
|
||||
let shown;
|
||||
let root: &Node = if sym.art.visible {
|
||||
&sym.art
|
||||
} else {
|
||||
let mut a = (*sym.art).clone();
|
||||
a.visible = true;
|
||||
shown = a;
|
||||
&shown
|
||||
};
|
||||
// A definition holds a group: art of another kind, and a plain group of one object (which
|
||||
// the reader takes for that group), are written in one.
|
||||
let wrap = !matches!(root.kind, NodeKind::Group { clip: false, .. }) || crate::import::symbol_art_member(root).is_some();
|
||||
let nest = std::mem::replace(&mut self.nest, 1);
|
||||
if wrap {
|
||||
self.line("u");
|
||||
self.node(root);
|
||||
let _ = write!(self.body, "U\n%_/ArtDictionary :\n{entries}%_;\n%_\n9 () XW\n");
|
||||
} else {
|
||||
// A group always writes its dictionary ([`Self::dictionary`]), which takes the entries.
|
||||
self.root_entries = Some((root.id, entries));
|
||||
self.node(root);
|
||||
}
|
||||
self.nest = nest;
|
||||
// Objects the writing doesn't pass through one by one (clipping paths, a clipping group's
|
||||
// own path) count as well.
|
||||
vectorcraft_doc::swatches::walk_with_masks(&sym.art, &mut |m| self.wrote_fixed |= redrawn(m));
|
||||
if self.wrote_pattern {
|
||||
self.pattern_written.insert(name.to_string());
|
||||
}
|
||||
if self.wrote_fixed {
|
||||
self.fixed_symbols.insert(name.to_string());
|
||||
}
|
||||
if self.inner_scale > 1.0 {
|
||||
self.symbol_scales.insert(name.to_string(), self.inner_scale);
|
||||
}
|
||||
let art;
|
||||
(
|
||||
art,
|
||||
(self.to_art, self.grad_space, self.canvas, self.instance_space),
|
||||
self.state,
|
||||
self.gs,
|
||||
self.clip,
|
||||
(self.block_start, self.block_end),
|
||||
(self.in_symbol, self.hidden),
|
||||
self.root_entries,
|
||||
self.wrote_pattern,
|
||||
(self.wrote_fixed, self.inner_scale),
|
||||
) = saved;
|
||||
self.doc = page_doc;
|
||||
let art = std::mem::replace(&mut self.body, art);
|
||||
self.defining_symbols.pop();
|
||||
let text = format!("%AI14_BeginSymbol\n{}\n0 A\n0 Xw\n0 Ae\n{art}%AI10_EndSymbol\n", ps::bytes(name.as_bytes()));
|
||||
self.defined_symbols.insert(name.to_string());
|
||||
self.symbol_defs.push(text);
|
||||
true
|
||||
}
|
||||
|
||||
/// Every symbol of the document defined, in its order (those its art uses before it): the
|
||||
/// Symbols panel keeps the ones no instance uses too, and on a large canvas all of them.
|
||||
pub(in crate::ai_editing) fn symbols(&mut self) {
|
||||
let doc = self.symbol_doc;
|
||||
for s in &doc.symbols {
|
||||
self.define_symbol(&s.name);
|
||||
}
|
||||
}
|
||||
|
||||
/// The symbol definitions, in the setup after the patterns: `%AI5_Begin_NonPrinting`, `Np`, each
|
||||
/// definition, `%AI5_End_NonPrinting--`.
|
||||
pub(in crate::ai_editing) fn symbol_definitions(&self, s: &mut String) {
|
||||
if self.symbol_defs.is_empty() {
|
||||
return;
|
||||
}
|
||||
s.push_str("%AI5_Begin_NonPrinting\nNp\n");
|
||||
for d in &self.symbol_defs {
|
||||
s.push_str(d);
|
||||
}
|
||||
s.push_str("%AI5_End_NonPrinting--\n");
|
||||
}
|
||||
|
||||
/// The symbol list, after the palette: the names of the symbols defined in the document's order
|
||||
/// (the Symbols panel's), between `%AI24_BeginSymbolList` and `%AI24_EndSymbolList` in a
|
||||
/// non-printing block.
|
||||
pub(in crate::ai_editing) fn symbol_list(&self, s: &mut String) {
|
||||
if self.symbol_defs.is_empty() {
|
||||
return;
|
||||
}
|
||||
s.push_str("%AI5_Begin_NonPrinting\nNp\n%AI24_BeginSymbolList\n");
|
||||
for sym in &self.symbol_doc.symbols {
|
||||
if self.defined_symbols.contains(&sym.name) {
|
||||
let _ = writeln!(s, "{}", ps::bytes(sym.name.as_bytes()));
|
||||
}
|
||||
}
|
||||
s.push_str("%AI24_EndSymbolList\n%AI5_End_NonPrinting--\n");
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ use graphics::{GState, Out};
|
||||
use interp::{Fault, Interp};
|
||||
use obj::PsError;
|
||||
|
||||
pub(crate) use ai::MAX_GRADIENT_STOPS;
|
||||
pub(crate) use ai::{MAX_GRADIENT_STOPS, MAX_SYMBOLS, symbol_art_member};
|
||||
pub(crate) use ate::{MAX_BYTES as MAX_TEXT_DOCUMENT, MAX_FRAMES, MAX_SEGMENTS, MAX_STORY, MAX_VALUES as MAX_TEXT_VALUES};
|
||||
pub use native::{AiLayers, ai_alone, ai_alone_on_page, ai_layers_alone, is_loss, layered_ai, layered_ai_from};
|
||||
pub(crate) use native::{MAX_TEXT, scale_document};
|
||||
|
||||
@@ -41,7 +41,7 @@ pub use read::{
|
||||
BindingRef, Brushes, IMAGE_TOO_LARGE, PaletteSwatches, Structure, artboards, bindings_note, brushes, graphic_styles, palette, read, read_beside,
|
||||
variables,
|
||||
};
|
||||
pub(crate) use read::{join_and, slot_frame, slot_of};
|
||||
pub(crate) use read::{MAX_SYMBOLS, join_and, slot_frame, slot_of, symbol_art_member};
|
||||
|
||||
pub(crate) use read::is_look_slot;
|
||||
|
||||
|
||||
@@ -145,6 +145,10 @@ pub(super) use types::{blend_mode, blend_name};
|
||||
#[path = "legacy_text.rs"]
|
||||
mod legacy_text;
|
||||
|
||||
// The `.ai` writer defines symbols as this reader keeps them.
|
||||
pub(crate) use directives::symbol_art_member;
|
||||
pub(crate) use types::MAX_SYMBOLS;
|
||||
|
||||
/// What the editing data holds: the document of its art, and where its art space is.
|
||||
#[derive(Debug)]
|
||||
pub struct Structure {
|
||||
|
||||
@@ -41,7 +41,7 @@ pub(crate) const FLIP: Affine = Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, 0.0]);
|
||||
pub(super) const MAX_PATTERNS: usize = 4096;
|
||||
|
||||
/// Most symbol definitions read (instances of others come in as the page draws them).
|
||||
pub(super) const MAX_SYMBOLS: usize = 4096;
|
||||
pub(crate) const MAX_SYMBOLS: usize = 4096;
|
||||
|
||||
/// Most names a symbol list orders.
|
||||
pub(super) const MAX_LISTED: usize = 4 * MAX_SYMBOLS;
|
||||
|
||||
@@ -18,7 +18,7 @@ from the public specifications, and users' own `.ai` files are used locally only
|
||||
| | Illustrator | VectorCraft | Notes |
|
||||
|---|---:|---:|---|
|
||||
| Formats opened or placed | ~30 | 29 | missing: FreeHand, CorelDRAW, DWG, HEIC/HEIF, PICT, PS |
|
||||
| Formats saved (Save As) | 6 (AI, AIT, PDF, EPS, SVG, SVGZ) | 6 + native | `.ai` written PDF-compatible (with an editing copy of the art: layers, groups, paths, colors, gradients, patterns, images, type, artboards, transparency, opacity masks, clipping) or as legacy version 7 (layers and groups, one artboard per file) |
|
||||
| Formats saved (Save As) | 6 (AI, AIT, PDF, EPS, SVG, SVGZ) | 6 + native | `.ai` written PDF-compatible (with an editing copy of the art: layers, groups, paths, colors, gradients, patterns, images, type, symbols, artboards, transparency, opacity masks, clipping) or as legacy version 7 (layers and groups, one artboard per file) |
|
||||
| Formats exported | ~16 | 17 | missing: DWG, 3D (OBJ/USDA/glTF); extra: GIF/PNG-8 palette export, DXF up to 2018, layered PSD |
|
||||
| **Overall (weighted by use)** | | **~75%** | `.ai` and PDF dominate the weighting |
|
||||
|
||||
@@ -75,15 +75,26 @@ private editing data. VectorCraft:
|
||||
RGB or CMYK ink amounts, and their alpha channel) and linked images as placed files with their path, their folder
|
||||
relative to the document's and a preview; images that can't be decoded or have more than 33,554,432 pixels are
|
||||
written as their outlines filled mid gray, images of 16 bits a channel at 8 bits, and linked images in opacity
|
||||
masks and pattern tiles as embedded images, each with a warning. Point type, area type (with its rows, columns,
|
||||
masks, pattern tiles and symbols as embedded images, each with a warning. Every symbol is written as a symbol
|
||||
definition (its art, its registration point and its place in the Symbols panel's order, nested symbols included)
|
||||
and its instances as symbol instances with their matrix, name, hide, lock, opacity, blending mode and opacity
|
||||
mask; instances with a fill, appearance changes, isolation, knockout or knockout shape of their own, instances of
|
||||
symbols with 9-slice guides or pattern paint, instances in pattern tiles or in art cut to a clipping object,
|
||||
instances scaled almost flat, instances that turn, mirror or scale symbols holding type, live objects,
|
||||
instances written as their art, shapes with rounded live corners or strokes written as outlines or brush art,
|
||||
instances that scale their symbol more than 100 times and instances on the layers of a large canvas are written as their art, pattern fills in a
|
||||
symbol's art are written as their tiles cut to the shape, symbols past 4,096 are written only as their
|
||||
instances' art, and symbols' 9-slice guides, export types and dynamic type are left out, each with a
|
||||
warning.
|
||||
Point type, area type (with its rows, columns,
|
||||
gutters and first baseline, threaded frames included) and type on a path are written as text: a text object for
|
||||
each, naming its story in the text document, with the characters, fonts, sizes, leading, tracking, baseline shift,
|
||||
scales, solid fills and strokes, capitals and paragraph alignment, indents, spacing and direction; other character
|
||||
and paragraph attributes are left out with a warning. Type in opacity masks, pattern tiles and art cut to a
|
||||
and paragraph attributes are left out with a warning. Type in opacity masks, pattern tiles, symbols and art cut to a
|
||||
clipping object, mirrored type, turned or unevenly scaled area type and type on a path, area type in an open frame,
|
||||
type on an open path whose ends meet, area type that flows around a text wrap, type of only white space, threads of
|
||||
type on a path, threads whose frames differ in orientation, type and stories past the sizes VectorCraft's reader reads, and
|
||||
type with inline graphics, ruby, gradient or pattern paint or an appearance or opacity mask of its own, other patterns, symbols, other clipping objects and live
|
||||
type with inline graphics, ruby, gradient or pattern paint or an appearance or opacity mask of its own, other patterns, other clipping objects and live
|
||||
objects (placed documents included) are written as plain filled and stroked paths, and freeform gradients, gradients along
|
||||
or across a stroke and gradients without stops, with more than 256 stops or without extent as their average
|
||||
color, each with a warning. Gradients that dither or mix their colors perceptually are written as classic
|
||||
|
||||
+1
-1
@@ -203,7 +203,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
of 14), open speed on large files (#758), live effects, Scatter, Art, Pattern and Bristle brushes on paths, a
|
||||
path's own Calligraphic brush settings, and symbols as live objects (#637; plain art styles read Drop Shadow, Outer Glow and Feather as live effects; pattern fills and global colours are read since #1025, placed (linked) files since #1264). A PDF-compatible `.ai` carries an editing copy of the art in the form
|
||||
VectorCraft's reader reads (layers, groups, paths, colors, gradients, patterns, images, transparency, opacity masks,
|
||||
clipping and type as text), with live effects left out and symbols and live objects written as their art
|
||||
clipping, type as text and symbols), with live effects left out and live objects written as their art
|
||||
([file-format-parity.md](file-format-parity.md)). Save As
|
||||
`.ai` › Version: Legacy (version 7) writes the PostScript form from the published version 7 specification,
|
||||
keeping layers (names, order, visibility, lock), groups, compound paths, clipping groups, gradients and
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ Checked 2026-10-10 from the code, tests and open issues; no workflow fails, so V
|
||||
| Set type: point and area type, Character and Paragraph styles, threading, outlines | yes | type core 80%; styles, threading, Create Outlines and font matching tested ([type-parity.md](type-parity.md)); justification options missing (not blocking) | 0 |
|
||||
| Organise layers and artboards and export assets (Export for Screens, SVG, PNG, PDF per artboard) | yes | Layers panel rework, Artboards panel and tool, Export for Screens and Asset Export done; Art Optimized supersample closes ghost borders (#983 / #1037) | 0 |
|
||||
| Print production: CMYK and spot colour, bleed and marks, PDF/X out, separations preview | yes | PDF/X-1a/3/4, presets, marks and bleed, ICC output intent, Separations Preview, Print with PostScript output | 0 |
|
||||
| Exchange files with Illustrator users: open their `.ai`/EPS/PDF, hand back a file they can open | partial (doesn't block alpha) | `.ai`, EPS and PDF open as editable art, but real files can get the wrong layer structure (#951); Save As `.ai` is PDF-compatible with an editing copy of the art (layers, groups, paths, colors, gradients, patterns, images, transparency, opacity masks, clipping and type as text, with live effects left out and symbols and live objects written as their art; #868), or legacy version 7, which keeps layers, groups and type as text, one artboard per file, and waits for users' testing. EPS legacy mesh corners, vector opacity masks and raster print-page comparison now preserve editable art under synthetic regressions. This is the beta main-format gap ([gaps.md](gaps.md) G9) | 10–20 (for beta) |
|
||||
| Exchange files with Illustrator users: open their `.ai`/EPS/PDF, hand back a file they can open | partial (doesn't block alpha) | `.ai`, EPS and PDF open as editable art, but real files can get the wrong layer structure (#951); Save As `.ai` is PDF-compatible with an editing copy of the art (layers, groups, paths, colors, gradients, patterns, images, transparency, opacity masks, clipping, type as text and symbols, with live effects left out and live objects written as their art; #868), or legacy version 7, which keeps layers, groups and type as text, one artboard per file, and waits for users' testing. EPS legacy mesh corners, vector opacity masks and raster print-page comparison now preserve editable art under synthetic regressions. This is the beta main-format gap ([gaps.md](gaps.md) G9) | 10–20 (for beta) |
|
||||
|
||||
Hindi interface follow-up (2026-11-06): the catalog and zero/one plural rule are implemented;
|
||||
native wording review, layout checks and a web Devanagari font remain. [Notes](localization-hi.md).
|
||||
|
||||
@@ -201,7 +201,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de
|
||||
| Image Trace, graphs, image tools | 3 | 75% | graph Column designs, Image Trace Create Strokes depth (Create Object Mosaic is done; junction-returning piece survival has source-generated regressions in `image-trace-quality.md`; large images trace with a warning; Auto Color keeps rare distinct colours) | 5–8 | estimated (raised from 72%); quick presets reveal adjustment controls, Properties reopens them |
|
||||
| Layers, artboards, document setup | 5 | 78% | Selected-artboard deletion is consistent across panel actions (all-selected retains artboard 1); history prunes removed indices while preserving valid multiselection; remaining: 30.0's artboard locking and colours and managing artboards from the canvas, Layers panel row context menus and Collect for Export (click-to-fit objects is done, #1227) | 5–8 | estimated (lowered from 82%: target moved); Artboard deletion keys keep artwork and the first of an all-selected set; Artboard Options' Show Center Mark and Show Cross Hairs draw on the canvas only (exports and print leave them out), regression-tested; Artboard Options' Show Video Safe Areas and Video Ruler Pixel Aspect Ratio (per artboard, native format) are done ([video display options](#evidence-what-ships-today-by-area)), score unchanged |
|
||||
| View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged); document-tab artwork hover/copy and lossless transfer tested (2026-10-22; score/hours unchanged); cached canvas reprojection preserves scale during resize, rotation and DPI changes (regression and contributor original-file evidence below; scores/hours unchanged); cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission (regression evidence below) |
|
||||
| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), a saved `.ai`'s editing copy without live effects and with symbols and live objects as their art (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. |
|
||||
| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), a saved `.ai`'s editing copy without live effects and with live objects as their art (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. |
|
||||
| View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged); document-tab artwork hover/copy and lossless transfer tested (2026-10-22; score/hours unchanged); cached canvas reprojection preserves scale during resize, rotation and DPI changes (regression and contributor original-file evidence below; scores/hours unchanged) |
|
||||
| View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged) |
|
||||
| Guides, grids, smart guides, snapping, rulers | 3 | 80% | "intersect" smart guides for a moved selection's anchors (drawn points and dragged anchors snap to intersections, #1658), distance readouts while drawing | 4–7 | estimated (lowered from 85%: target moved); new ruler-guide Alt orientation implemented; video rulers (View › Rulers › Show Video Rulers, #2849) done, score unchanged |
|
||||
@@ -282,8 +282,8 @@ The discounts, with the evidence:
|
||||
ones are about structure, not missing art: layers flattened or multiplied (#951: 109 layers instead of 14;
|
||||
#868), guides lost (#779), slower opening (#758), live effects, brushes and symbols as their drawn look
|
||||
(#637). VectorCraft's PDF-compatible `.ai` carries an editing copy of the art (layers, groups, paths, colors,
|
||||
gradients, patterns, images, transparency, opacity masks, clipping and type as text), with live effects left out
|
||||
and symbols and live objects written as their art; the legacy `.ai` of version 7 it also writes keeps layers, groups and type as text (one
|
||||
gradients, patterns, images, transparency, opacity masks, clipping, type as text and symbols), with live effects left out
|
||||
and live objects written as their art; the legacy `.ai` of version 7 it also writes keeps layers, groups and type as text (one
|
||||
artboard per file, no transparency) but hasn't been tested by users yet (#868). That blocks beta (the main-format rule) for people who hand files back to Illustrator users,
|
||||
but it doesn't stop someone opening and reworking their own archive.
|
||||
- **Interaction fidelity (×0.9).** Pen, Direct Selection, smart guides and snapping are deep
|
||||
@@ -1218,6 +1218,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
At a precision of 9 decimals or fewer, SVG export writes each number from an integer when the integer rounds it as the decimal formatter does; the formatter still writes values halfway between two written numbers and values too large for the integer. Exporting one path of 200,000 anchors takes a median of 43.4 ms instead of 167.0 ms on Windows 11, and 20,000 rectangles 81.4 ms instead of 141.4 ms, with the same SVG (`crates/svg/src/lib.rs`).
|
||||
SVG import reads each `<circle>` and `<ellipse>` as the ellipse VectorCraft draws: four smooth anchors on the axes, starting at the right, with handles of the standard length, so an ellipse exported as `<ellipse>` comes back with the anchors it was written from and draws the same (`circles_and_ellipses_import_as_the_ellipse_drawn` in `crates/svg/tests/it/basic_shapes.rs`, `filled_ellipse_on_the_artboard_edge_survives_visually` in `prop_svg.rs`).
|
||||
SVG import keeps attributes in other applications' XML namespaces (`plotter:pen="2"`) on the objects and layers they belong to, as it keeps their `data-*` attributes: copies, duplicates and `.vectorcraft` files keep them, and SVG export writes them back with each namespace declared once on the root (the rest of #522; `crates/svg/tests/it/foreign_attributes.rs`, `tests/attributes.rs`).
|
||||
A PDF-compatible `.ai` saved with Include Editing Data for Other Apps defines every symbol in its editing copy with its art, registration point and place in the Symbols panel's order, and writes its instances as symbol instances, which VectorCraft's `.ai` reader opens as symbols and instances again (`symbols_and_their_instances_are_read_back` in `crates/eps/src/ai_editing/tests.rs`; [file-format-parity.md](file-format-parity.md) lists the instances written as their art).
|
||||
A PDF-compatible `.ai` saved with Include Editing Data for Other Apps writes point type, area type and type on a path as type in its editing copy, and objects next to it keep their own transparency (`crates/eps/src/ai_editing/tests.rs`, `cmd::fileio::tests_pdfedit::type_comes_back_from_the_editing_copy`; [file-format-parity.md](file-format-parity.md) lists what is written as outlines).
|
||||
In a `.ai` or EPS opened from its editing data, type has its text object's opacity, blend mode and knockout shape flag, whether it is made from the file's text document or taken from the page, and type made from the text document takes its text object's lock. Where such type has transparency and comes in with other art (the rest of its look, or page type and strokes that go with it), a group of them has it. Where the page draws its type, or the strokes around it, with transparency that is neither its text object's nor its text object's combined with that of the objects around it, the page's type and strokes keep their own instead (`crates/eps/src/import/tests_native.rs`). Where the page draws type of no text object over text objects made from the text document, with their words in any case, that type is theirs as the page draws it and is left out; the page's other type of no text object goes only to text objects whose type the text document can't make, or else is left out with a warning.
|
||||
`.ai` editing-data import restores the document's listed graphic styles in panel order and links objects that use a listed style; unsupported appearances keep their drawn look (`crates/eps/src/import/ai/tests.rs`).
|
||||
|
||||
Reference in New Issue
Block a user