Merge pull request #5965 from dotCooCoo/feat/ai-editing-data-s5

.ai save: the editing copy keeps type as type
This commit is contained in:
BFlat
2026-11-07 09:10:24 +08:00
committed by GitHub
20 changed files with 2347 additions and 40 deletions
+1 -1
View File
@@ -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; proprietary editing data isn't written);
- 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);
- 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);
+2 -2
View File
@@ -23,8 +23,8 @@ 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 can't be saved back with Illustrator's
editing data, the interaction details have never been checked side by side, and some machines fail to start the
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
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
@@ -803,6 +803,65 @@ fn images_come_back_from_the_editing_copy() {
}
}
/// The type of `doc`'s art, one line each: its kind, point type's matrix or area type's frame on the
/// document (rounded to hundredths), its characters, and each run's font, size and fill.
fn type_outline(doc: &vectorcraft_doc::Document) -> Vec<String> {
use vectorcraft_doc::{NodeKind, TextKind};
let mut out = vec![];
let round = |v: f64| (v * 100.0).round() / 100.0 + 0.0;
doc.walk(|n| {
if let NodeKind::Text(t) = &n.kind {
let (kind, xf) = match &t.kind {
TextKind::Point => ("point", t.xf.as_coeffs().map(round).to_vec()),
TextKind::Area { frame } => {
let b = frame.transformed(t.xf).bounds().unwrap();
("area", vec![round(b.x0), round(b.y0), round(b.x1), round(b.y1)])
}
TextKind::OnPath { .. } => ("path", vec![]),
};
let runs: Vec<String> = t
.runs
.iter()
.map(|r| format!("{:?} {} {} {} {:?}", r.text, r.style.font_family, r.style.font_style, round(r.style.size), r.style.fill))
.collect();
out.push(format!("{kind} {xf:?} {runs:?}"));
}
});
out
}
/// Point type of two paragraphs, turned point type and area type come back from the editing copy
/// for other apps alone as type, with every save option that writes the copy.
#[test]
fn type_comes_back_from_the_editing_copy() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap();
s.execute("text.create", &json!({"x": 20, "y": 40, "text": "Hello\nWorld", "size": 24})).unwrap();
s.execute("text.create", &json!({"x": 150, "y": 100, "text": "Turned"})).unwrap();
// The new type is selected.
s.execute("object.rotate", &json!({"angle": 30})).unwrap();
s.execute("text.create", &json!({"x": 20, "y": 120, "text": "Area type in a frame", "area": {"width": 120, "height": 60}})).unwrap();
let path = tmp("type.ai");
s.execute("document.save", &json!({"path": path})).unwrap();
let before = type_outline(&s.doc().unwrap().doc);
assert_eq!(before.len(), 3, "{before:#?}");
for opts in [
json!({"format": "ai"}),
json!({"format": "ai", "compress": false}),
json!({"format": "ai", "embedProfiles": false}),
json!({"format": "ai", "includeLinked": true}),
json!({"format": "ai", "range": "all"}),
json!({"format": "ai", "pdfCompatible": false}),
] {
let r = s.execute("document.export", &opts).unwrap();
assert!(!r["warnings"].to_string().contains("type"), "{opts} {}", r["warnings"]);
let copy = load::load_with(&path, &b64(&r), &LoadOptions { native: false, ..Default::default() }).unwrap();
assert!(copy.editing_data && !copy.restored, "{opts} {:?}", copy.warnings);
assert!(!copy.warnings.iter().any(|w| w.contains("editing data")), "{opts} {:?}", copy.warnings);
assert_eq!(type_outline(&copy.doc), before, "{opts}");
}
}
/// A document of two artboards (the art on the first): a rectangle filled with a linear gradient of
/// three stops placed across part of it, an ellipse filled with an elliptical radial gradient whose
/// focal point is off its center, a rectangle stroked with a gradient, rectangles filled and
+48
View File
@@ -3413,6 +3413,54 @@ proptest! {
survive("hostile .ai images and placed files", || Some(vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![], false, false, 1.0).0))?;
}
/// A `.ai` whose type (point type turned, vertical type, area type, threaded area type and type
/// on a path, their stubs with the frames' paths, and the text document they name, as the `.ai`
/// writer writes them) is damaged: read from its editing data alone and beside a page.
#[test]
fn ai_text_documents_never_panic(
cut in 0usize..30_000,
edits in prop::collection::vec((0usize..30_000, prop::sample::select(vec!['0', '1', '9', ' ', '\n', '(', ')', '[', ']', '<', '>', '/', '%', '_', 'n', 'f', 'z', '~'])), 0..16),
) {
use vectorcraft_doc::{CharStyle, Node, NodeKind, TextKind, TextObject};
use vectorcraft_geom::{Affine, Point, Rect, shapes};
let mut doc = Document::new(200.0, 100.0);
let layer = doc.layers[0].id;
let mut point = TextObject::point(Point::new(10.0, 30.0), "Turned\ntype", CharStyle::default());
point.xf *= Affine::rotate(0.4);
let mut area = TextObject::point(Point::ZERO, "Area type", CharStyle::default());
area.kind = TextKind::Area { frame: shapes::rectangle(Rect::new(20.0, 40.0, 90.0, 90.0)) };
area.xf = Affine::IDENTITY;
let mut on_path = TextObject::point(Point::ZERO, "On a path", CharStyle::default());
on_path.kind = TextKind::OnPath { path: shapes::ellipse(Rect::new(100.0, 20.0, 180.0, 80.0)), start: 0.25, end: None };
on_path.xf = Affine::IDENTITY;
let mut down = TextObject::point(Point::new(190.0, 10.0), "Down", CharStyle::default());
down.vertical = true;
let mut down_area = area.clone();
down_area.vertical = true;
down_area.kind = TextKind::Area { frame: shapes::rectangle(Rect::new(150.0, 40.0, 180.0, 90.0)) };
let mut first = area.clone();
first.kind = TextKind::Area { frame: shapes::rectangle(Rect::new(20.0, 5.0, 50.0, 35.0)) };
first.runs = vec![vectorcraft_doc::TextRun::new("A story through two frames", CharStyle::default())];
let mut second = first.clone();
second.kind = TextKind::Area { frame: shapes::rectangle(Rect::new(60.0, 5.0, 90.0, 35.0)) };
second.runs = vec![vectorcraft_doc::TextRun::new("", CharStyle::default())];
for t in [point, area, on_path, down, down_area] {
let n = Node::new(doc.alloc_id(), NodeKind::Text(Box::new(t)));
doc.insert(Some(layer), usize::MAX, n).unwrap();
}
let (first, second) = (Node::new(doc.alloc_id(), NodeKind::Text(Box::new(first))), Node::new(doc.alloc_id(), NodeKind::Text(Box::new(second))));
doc.text_threads.push(vec![first.id, second.id]);
doc.insert(Some(layer), usize::MAX, first).unwrap();
doc.insert(Some(layer), usize::MAX, second).unwrap();
let program = vectorcraft_eps::export_ai_editing(&doc, &vectorcraft_eps::AiEditingOptions::default()).unwrap().program;
let written = String::from_utf8_lossy(&program);
prop_assert_eq!(written.matches("/AI11Text :").count(), 7, "every text object is written as text");
let text = mutate_text(&written, cut, &edits);
let private = [b"%AI24_ZStandard_Data".as_slice(), &zstd(&text)].concat();
survive("hostile .ai type", || vectorcraft_eps::ai_alone(&private).ok().map(|r| r.0))?;
survive("hostile .ai type beside a page", || Some(vectorcraft_eps::layered_ai(&private, Document::new(200.0, 100.0), vec![], false, false, 1.0).0))?;
}
/// A `.ai` whose variable library is damaged or hostile: the library is counted for the warning
/// whether the layers are read or not (an inserted `Z` can make an operator the reader doesn't
/// handle, and then the layers aren't read).
+15 -1
View File
@@ -78,12 +78,23 @@
//! (`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.
//! - 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
//! `writer::texts`), in the form the reader's `ate.rs` reads: the characters, the fonts, sizes,
//! leading, tracking, baseline shift, scales, solid fills and strokes and capitals of each run,
//! 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.
//!
//! 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 as its outlines and its inline graphics' art, freeform gradients, gradients
//! 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,
//! 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
@@ -225,6 +236,9 @@ pub fn export_ai_editing(doc: &Document, o: &AiEditingOptions) -> Result<AiEditi
w.definitions(&mut s);
s.push_str(&palette);
artboards(&mut s, doc, to_art, origin);
if !w.texts.is_empty() {
s.push_str(&w.texts.section(doc));
}
s.push_str("%%EndSetup\n");
s.push_str(&w.body);
s.push_str("%%PageTrailer\ngsave annotatepage grestore showpage\n%%Trailer\n%%EOF\n");
File diff suppressed because it is too large Load Diff
+69 -5
View File
@@ -7,7 +7,7 @@ use std::sync::Arc;
use vectorcraft_color::cms::Cms;
use vectorcraft_color::swatch::{REGISTRATION, Swatch};
use vectorcraft_color::{BlendMode, Color, Paint};
use vectorcraft_doc::{AppearanceItem, Document, FillLayer, Knockout, LAYER_COLORS, LayerColor, Node, NodeKind, OpacityMask, StrokeLayer};
use vectorcraft_doc::{AppearanceItem, Document, FillLayer, Knockout, LAYER_COLORS, LayerColor, Node, NodeId, NodeKind, OpacityMask, StrokeLayer};
use vectorcraft_effects::stroke;
use vectorcraft_geom::{Affine, FillRule, PathData, Shape, SubPath};
use vectorcraft_pathops::BoolOp;
@@ -17,19 +17,22 @@ use crate::ps;
mod images;
mod paints;
mod texts;
pub(super) use images::PAYLOAD;
#[cfg(test)]
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 texts::TYPE_ATTRIBUTES;
/// How deep symbols, live objects and pattern tiles may nest in one another (deeper art is left
/// out). Groups and layers need no limit of their own: a document is never deeper than
/// [`vectorcraft_doc::MAX_DEPTH`].
const MAX_NEST: u32 = 8;
pub(super) const TYPE: &str = "type is written as its outlines in the editing data for other apps";
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 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";
@@ -157,6 +160,8 @@ pub(super) struct Writer<'a> {
/// How many parts of the data written on `%_` lines (opacity masks' art, patterns' tiles) the
/// art being written is in.
hidden: u32,
/// The text document: the stories of the type written as text.
pub(super) texts: texts::TextDoc,
}
impl<'a> Writer<'a> {
@@ -176,6 +181,7 @@ impl<'a> Writer<'a> {
canvas_origin: origin,
payloads: vec![],
hidden: 0,
texts: texts::TextDoc::default(),
cms: vectorcraft_color::cms::active(),
body: String::new(),
warnings: vec![],
@@ -332,6 +338,16 @@ impl<'a> Writer<'a> {
}
}
/// [`Self::set_xy`] for an object that writes no paint before its transparency (type, an image,
/// a guide): `XR` comes first, as in [`Self::paint_state`], because an `Xy` right after a compound
/// path or a text object would be theirs.
fn set_object_xy(&mut self, xy: Xy) {
if self.gs != Some(xy) {
self.line("0 XR");
self.write_xy(xy);
}
}
/// `mode opacity isolate knockout shape Xy`.
fn write_xy(&mut self, xy: Xy) {
let knockout = match xy.knockout {
@@ -350,6 +366,11 @@ impl<'a> Writer<'a> {
/// dictionary goes between the two ([`Self::node`]).
fn end_group(&mut self, close: &str, xy: Xy) {
self.line(close);
self.state_block(xy);
}
/// The state block of the group or text object just written ([`Self::end_group`]).
fn state_block(&mut self, xy: Xy) {
self.block_start = self.body.len();
if xy == Xy::PLAIN {
self.line("9 () XW");
@@ -391,7 +412,7 @@ impl<'a> Writer<'a> {
if *guide {
// Its own transparency, not the one the object before it set.
if path.subpaths.iter().any(|sp| !sp.anchors.is_empty()) {
self.set_xy(xy.object());
self.set_object_xy(xy.object());
}
ai_ops::guide(&mut self.body, self.to_art, path, true);
} else {
@@ -830,7 +851,7 @@ impl<'a> Writer<'a> {
/// The paint of a path of transparency `xy` filled by `rule`: its fill color and `O`, its stroke
/// color and `R`, `Xy`, the line style, `XR`. Without paint, `XR` comes first: an `Xy` right
/// after a group or compound path would be theirs.
/// after a compound path or a text object would be theirs.
fn paint_state(&mut self, fill: Option<&FillLayer>, stroke: Option<&StrokeLayer>, xy: Xy, rule: FillRule) {
let rule = if rule == FillRule::EvenOdd { "1 XR" } else { "0 XR" };
if let Some(f) = fill {
@@ -880,12 +901,43 @@ 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.
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() {
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.
let placed = self.texts.thread(doc, thread, n.id, self.canvas, &|id| !cut_to_clipping_object(doc, id));
placed.map(|p| (p, self.texts.take_lost()))
} else if texts::holds(n, t) {
self.texts.add(t, self.canvas).map(|story| ((story, 0), texts::loses(t)))
} else {
None
};
let Some(((story, frame), lost)) = placed else { return self.outlined_text(n, xy) };
if lost {
self.warn(texts::TYPE_ATTRIBUTES);
}
// The stub takes the transparency set before it, as a path does. Its isolation and knockout
// are read only from a state block after it, as a group's are.
self.set_object_xy(xy.object());
texts::stub(&mut self.body, story, frame, t, self.to_art);
if xy.isolate || xy.knockout != Knockout::Neutral {
self.state_block(xy);
}
}
/// Type `n` as a group of what Type › Create Outlines makes of it
/// ([`vectorcraft_effects::outline_text`]): each run's glyphs with their underline and
/// strikethrough, its color glyphs, and the object's own fills and strokes; then its inline
/// graphics as their symbols' art where the layout places them, as the PDF page draws them;
/// the group has the type's transparency `xy`.
fn text(&mut self, n: &Node, xy: Xy) {
fn outlined_text(&mut self, n: &Node, xy: Xy) {
self.warn(TYPE);
self.line("u");
let doc = self.doc;
@@ -1150,6 +1202,18 @@ fn compound_path(n: &Node) -> PathData {
PathData::new(subpaths)
}
/// Is object `id` of `doc` in (or inside) a clipping group or clipping layer whose clipping object
/// isn't a path or compound path (its art is written cut to that object, [`Writer::clip_group`])?
/// Type that is a clipping object is one of these: it isn't a path.
fn cut_to_clipping_object(doc: &Document, id: NodeId) -> bool {
let ancestry = doc.ancestry(id).unwrap_or_default();
let above = ancestry.split_last().map(|(_, a)| a).unwrap_or_default();
above.iter().filter_map(|a| doc.node(*a)).any(|p| {
matches!(p.kind, NodeKind::Group { clip: true, .. } | NodeKind::Layer { clip: true, .. })
&& p.children().and_then(|c| c.first()).is_none_or(|c| clip_path(c).is_none())
})
}
/// The outline and fill rule a clipping path or compound path `n` clips by; `None` for other
/// clipping objects and for a path without points (it clips everything away, see
/// [`Writer::clip_contents`]).
+1 -1
View File
@@ -115,7 +115,7 @@ impl Writer<'_> {
if cut.is_some() {
self.line("q");
}
self.set_xy(xy.object());
self.set_object_xy(xy.object());
// The object places the image's own pixel grid (its width × height) wherever the file's is.
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));
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -71,8 +71,9 @@ use interp::{Fault, Interp};
use obj::PsError;
pub(crate) use ai::MAX_GRADIENT_STOPS;
pub(crate) use native::scale_document;
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};
pub use text::family_style;
/// The page of a PostScript file without a bounding box (US Letter).
+21 -2
View File
@@ -66,8 +66,8 @@ fn number_before(head: &[u8], key: &[u8]) -> Option<f64> {
number(words(head.get(..k)?).last()?)
}
/// A path in the format's path operators (`m`, `l`/`L`, `c`/`C`, `v`/`V`, `y`/`Y`; any other
/// operator ends the numbers before it).
/// A path in the format's path operators (`m`, `l`/`L`, `c`/`C`, `v`/`V`, `y`/`Y`, closed by `n`,
/// `f`, `s`, `b` or `h`; any other operator ends the numbers before it).
fn path(text: &[u8]) -> Option<BezPath> {
let mut bp = BezPath::new();
let mut nums: Vec<f64> = vec![];
@@ -106,6 +106,12 @@ fn path(text: &[u8]) -> Option<BezPath> {
bp.curve_to(p(n - 4)?, q, q);
cur = Some(q);
}
// The painting operators that close the path first (lower case), and `h`: an area's frame
// is closed, its last point the first again.
b"n" | b"f" | b"s" | b"b" | b"h" if cur.is_some() => {
bp.close_path();
cur = None;
}
_ => {}
}
nums.clear();
@@ -135,4 +141,17 @@ mod tests {
assert_eq!(f[&(49, 0)].bounding_box(), kurbo::Rect::new(1.0, 2.0, 3.0, 4.0));
assert!(text_frames(b"/AI11Text : garbage").is_empty());
}
/// An area's frame painted with `n` (as the format writes it, the last point the first again)
/// is a closed path of its corners; one painted with `N` stays open.
#[test]
fn a_frame_painted_closed_is_closed() {
let doc = b"/AI11Text :\r0 /FreeUndo ,\r0 /FrameIndex ,\r3 /StoryIndex ,\r/Art :\rX=\r40 130 m\r40 250 L\r240 250 L\r240 130 L\r40 130 L\rn\rX+\r; /ConfiningPath ,\r;\r\
/AI11Text :\r0 /FreeUndo ,\r0 /FrameIndex ,\r4 /StoryIndex ,\r/Art :\rX=\r40 130 m\r40 250 L\r240 250 L\rN\rX+\r; /ConfiningPath ,\r;\r";
let f = text_frames(doc);
let closed = vectorcraft_geom::PathData::from_bezpath(&f[&(3, 0)]);
assert!(closed.subpaths[0].closed);
assert_eq!(closed.subpaths[0].anchors.len(), 4, "the last point merges with the first");
assert!(!vectorcraft_geom::PathData::from_bezpath(&f[&(4, 0)]).subpaths[0].closed);
}
}
+3
View File
@@ -35,6 +35,9 @@ impl<'a> Reader<'a> {
// The type takes the state's transparency, as a path does; an `Xy` after it sets the slot's own.
n.opacity = gs.opacity;
n.blend = gs.blend;
n.knockout_shape = gs.knockout_shape;
// An `Xy` before the text object was the object's, not its layer's, when the layer's `LB` follows.
self.layer_state = None;
let hidden = self.obj_hidden;
self.add(n, hidden)
}
+22 -10
View File
@@ -17,8 +17,8 @@
//! offset from it. A right-to-left paragraph (key `33` of its attributes) swaps the two: its
//! "left"-aligned lines end at the anchor and its "right"-aligned ones start there. The frame of the
//! story gives a matrix that carries that layout onto the canvas of the app, which is 16383 points a
//! side; the canvas's centre, 8191.5, is the centre of the file's `%AI3_TemplateBox`. See
//! [`Story::place`].
//! side; the canvas's centre, 8191.5, is the centre of the file's `%AI3_TemplateBox`. A story without
//! a layout has its point type's anchor at its frame's point. See [`Story::place`].
use std::collections::BTreeMap;
@@ -28,13 +28,13 @@ use vectorcraft_doc::{CharStyle, Justify, Node, NodeId, NodeKind, ParaDirection,
use vectorcraft_geom::{Affine, PathData, Point, Rect};
/// Longest story read (bytes).
const MAX_STORY: usize = 1 << 20;
pub(crate) const MAX_STORY: usize = 1 << 20;
/// Biggest text document read (bytes, decoded).
const MAX_BYTES: usize = 64 << 20;
pub(crate) const MAX_BYTES: usize = 64 << 20;
/// Deepest nesting read.
const MAX_DEPTH: usize = 64;
/// Most values read.
const MAX_VALUES: usize = 4_000_000;
pub(crate) const MAX_VALUES: usize = 4_000_000;
/// The centre of the app's canvas, in the units of its text document.
pub(super) const CANVAS_CENTRE: f64 = 8191.5;
@@ -241,8 +241,8 @@ const MAX_SHEETS: usize = 4096;
const MAX_SHEETS_BASED_ON: usize = 16;
/// Most frames read of one story, and most path segments of a frame.
const MAX_FRAMES: usize = 4096;
const MAX_SEGMENTS: usize = 1 << 16;
pub(crate) const MAX_FRAMES: usize = 4096;
pub(crate) const MAX_SEGMENTS: usize = 1 << 16;
/// Where a frame puts its story's text.
#[derive(Debug, Clone, PartialEq)]
@@ -513,8 +513,17 @@ impl Texts {
if indices.iter().any(|n| self.frame_vertical(*n) != vertical) {
return None;
}
// The first frame's layout, when the file keeps one: where its lines start.
let (mut anchor, mut lines, mut path_spacing) = ((0.0, 0.0), vec![], 0.0);
// The first frame's layout, when the file keeps one: where its lines start. Without one, the
// anchor of point type is its frame's point (key 0, where the layout's anchor is too).
let point = indices.first().and_then(|n| self.frames.get(*n)).and_then(|f| f.get("0")).and_then(|f| f.get("0")).and_then(Val::nums);
let (mut anchor, mut lines, mut path_spacing) = (
match point.as_deref() {
Some([x, y]) if matches!(frames.first(), Some(FrameKind::Point(_))) => (*x, *y),
_ => (0.0, 0.0),
},
vec![],
0.0,
);
if let Some(f) = view.get("2").and_then(|l| find_node(l, "F")) {
// Vertical type on a path turns its glyphs across the path: their matrices don't give
// the spacing.
@@ -854,8 +863,11 @@ impl Story {
let text: String = if threaded {
String::new()
} else {
// The `\r` that ends the story's last paragraph; one before it ends a paragraph, and the
// empty paragraph after it is the story's last.
self.text
.trim_end_matches('\r')
.strip_suffix('\r')
.unwrap_or(&self.text)
.chars()
.map(|c| match c {
'\r' => '\n',
+1
View File
@@ -56,6 +56,7 @@ use compare::{
};
pub use layers::{AiLayers, ai_alone, ai_alone_on_page, ai_layers_alone, layered_ai, layered_ai_from};
pub(super) use layers::{layered, program_alone};
pub(crate) use slots::MAX_TEXT;
pub use slots::is_loss;
#[cfg(test)]
use slots::join_pieces;
+12 -7
View File
@@ -43,7 +43,7 @@ pub(super) const MAX_SLOTS: usize = 5000;
const MAX_PIECES: usize = 2000;
/// Most bytes of text in the text objects of the layers (a story named twice counts twice).
pub(super) const MAX_TEXT: usize = 16 << 20;
pub(crate) const MAX_TEXT: usize = 16 << 20;
/// Most pairs of a text object and a piece of the page's type compared, and of a glyph the page
/// kept as outlines and a node of the page's type.
@@ -286,11 +286,12 @@ pub(super) fn fill_slots(
let Some(n) = nodes.get(i) else { break };
if let Some(story) = slot_of(n.name.as_deref()) {
let slot = n.id;
let visible = n.visible;
let (visible, locked) = (n.visible, n.locked);
let name = names.get(&n.id);
let frame = ai::slot_frame(n.name.as_deref());
let state = (n.opacity, n.blend, n.isolate, n.knockout);
let transparent = see_through(n);
let shape = n.knockout_shape;
let transparent = see_through(n) || shape;
// A text object that is a wrap object stays one.
let wrap = n.extras().wrap;
// An art style read onto the slot (fills, strokes and effects round the characters) is the type's.
@@ -307,6 +308,9 @@ pub(super) fn fill_slots(
if !visible {
Arc::make_mut(&mut p).visible = false;
}
if locked {
Arc::make_mut(&mut p).locked = true;
}
if let Some(name) = name.filter(|_| matches!(p.kind, NodeKind::Text(_))) {
Arc::make_mut(&mut p).name = Some(name.clone());
}
@@ -333,10 +337,11 @@ pub(super) fn fill_slots(
}
if let [p] = parts.as_mut_slice() {
let p = Arc::make_mut(p);
(p.opacity, p.blend, p.isolate, p.knockout) = state;
(p.opacity, p.blend, p.isolate, p.knockout, p.knockout_shape) = (state.0, state.1, state.2, state.3, shape);
} else if transparent {
let mut group = Node::group(doc.alloc_id(), std::mem::take(&mut parts));
(group.opacity, group.blend, group.isolate, group.knockout) = state;
(group.opacity, group.blend, group.isolate, group.knockout, group.knockout_shape) =
(state.0, state.1, state.2, state.3, shape);
parts.push(Arc::new(group));
}
}
@@ -372,7 +377,7 @@ pub(super) fn fill_slots(
if let Some(story) = story {
made.push((story, frame, text.id));
}
text.visible = visible;
(text.visible, text.locked) = (visible, locked);
// Type with an art style is what the page draws as the style's look: it is seen.
let seen = seen || styled.is_some();
if let Some(appearance) = styled {
@@ -384,7 +389,7 @@ pub(super) fn fill_slots(
// Type the page shows is kept; other made type is weighed against the page.
text.name = if seen { name.cloned() } else { Some(format!("{MADE}{}", name.map_or("", String::as_str))) };
// The type takes the slot's transparency.
(text.opacity, text.blend, text.isolate, text.knockout) = state;
(text.opacity, text.blend, text.isolate, text.knockout, text.knockout_shape) = (state.0, state.1, state.2, state.3, shape);
if wrap.is_some() {
text.extras_mut().wrap = wrap;
}
+35
View File
@@ -3417,6 +3417,41 @@ fn type_keeps_the_opacity_and_blend_mode_of_its_text_object() {
assert!(group.children().unwrap().iter().all(|p| (p.opacity, p.blend) == (1.0, BlendMode::Normal)));
}
/// The knockout shape flag of a text object's `Xy` is its type's, made from the text document or
/// taken from the page, and is on the group of the two where the type comes with other art.
#[test]
fn type_keeps_the_knockout_shape_of_its_text_object() {
let shapes = |d: &Document| {
let mut v = vec![];
d.walk(|n| {
if let NodeKind::Text(_) = &n.kind {
v.push(n.knockout_shape);
}
});
v
};
let doc = text_document(&[("Hi there\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]);
let page = "0 0 0 1 setcmykcolor /Helvetica findfont 14 scalefont setfont 58.5 61.5 moveto (Hi there) show";
for body in [format!("1 0.2 0 0 1 Xy\n{}", text_object(0)), format!("{}1 0.2 0 0 1 Xy\n", text_object(0))] {
let art = native_with_text(&layer("Art", true, &body), &doc);
let (d, notes) = crate::ai_alone(&private(&art)).unwrap();
assert_eq!(shapes(&d), [true], "{body}{notes:?}");
let r = import(&eps(page, &art)).unwrap();
assert_eq!(shapes(&r.document), [true], "{body}{:?}", r.warnings);
}
let art = native_with_text(&layer("Art", true, &format!("1 0.2 0 0 0 Xy\n{}", text_object(0))), &doc);
assert_eq!(shapes(&crate::ai_alone(&private(&art)).unwrap().0), [false]);
let page = format!(
"{}\n/Helvetica findfont 12 scalefont setfont 10 50 moveto (Hi) show\n10 50 moveto (Hi) false charpath 2 setlinewidth stroke",
page_square(10, 10)
);
let words = layer("Words", true, "0 1 0 0 1 Xy\n/AI11Text :\n0 /FreeUndo ,\n;\n");
let r = import(&eps(&page, &native(&(layer("Art", true, &square(10, 10)) + &words)))).unwrap();
let words = &r.document.layers[1];
assert_eq!(kinds(words), ["group[2]"], "{:?}", r.warnings);
assert!(words.children().unwrap()[0].knockout_shape);
}
#[test]
fn a_commented_copy_of_a_story_that_has_a_plain_text_object_is_not_another_object() {
// The file writes some stories twice (plain, then as comments) and others only as comments.
+11 -3
View File
@@ -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, 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, 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 |
@@ -73,8 +73,16 @@ 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. Type, other patterns, symbols, other clipping
objects and live objects (placed documents included) are written as plain filled and stroked paths, and freeform gradients, gradients along
masks and pattern tiles as embedded images, 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
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
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
gradients, and gradient stops linked to global process colors as their colors, each with a warning. The opacity
+7
View File
@@ -199,6 +199,13 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
art with strokes, gradients or patterns) and type with an Art brush keep the drawn look.
- **Missing:** faithful layer and group structure from the editing data in real files (#951: 109 layers instead
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
([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
of 14), open speed on large files (#758), live effects, Scatter, Pattern and Bristle brushes on paths, the Art
brushes listed above as keeping the drawn look, a path's own Calligraphic or Art 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). Writing Illustrator's own editing data is out of scope
(undocumented): a PDF-compatible `.ai` opens in Illustrator 2018 without its layers and groups (#868). Save As
+1 -1
View File
@@ -88,7 +88,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, which Illustrator opens without its own editing data (#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 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) |
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).
+6 -4
View File
@@ -199,7 +199,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), no writing of Illustrator's editing data (#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 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. |
| 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 |
@@ -279,8 +279,9 @@ The discounts, with the evidence:
about `.ai`/EPS, the most common single format theme. 20 of the 28 are fixed (often the same day). The 8 open
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 can't write Illustrator's editing data (undocumented), so its PDF-compatible `.ai` opens
in Illustrator as plain art; the legacy `.ai` of version 7 it also writes keeps layers, groups and type as text (one
(#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
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
@@ -1209,7 +1210,8 @@ 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`).
In a `.ai` or EPS opened from its editing data, type has its text object's opacity and blend mode, whether it is made from the file's text document or taken from the page. 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.
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`).
A `.ai` or EPS graphic style library, whose editing data holds the styles and their list but no layers, opens with those styles in list order. Other Library… loads another document's graphic styles ([Apply graphic styles](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-graphic-styles/apply-graphic-styles.html)) and lists such a library's styles in a library panel. A `.ai` or EPS read as its PDF part or page because its layers can't be used also gets the styles of its list in place of the default ones (`crates/eps/src/import/tests_native.rs`, `cmd::fileio::tests_aiimport::a_graphic_style_library_in_a_ai_file_without_layers_loads_its_styles`).
A `.ai` or EPS opened from its editing data whose text document makes the type of every text object that shows takes all its type from the text document: each text object stays in its own group, frame and layer, the page's type isn't matched with them, a file of more than 5,000 text objects still opens with its layers, and type the page draws differently (in another font) keeps the layers, with a warning. A line break inside a paragraph stays a line break in it (`crates/eps/src/import/tests_native.rs`, `crates/eps/src/import/ate.rs`).