From 53f6aafe87ade4fb5aad8de00a1efd4b480834ee Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 6 Nov 2026 14:25:59 -0800 Subject: [PATCH] .ai import: type on a path keeps its effect, alignment and flip Type on a path from a file's text document opened as Rainbow on its baseline, not flipped, whatever Type on a Path Options the frame kept. Each frame now keeps its effect (Rainbow, Skew, 3D Ribbon, Stair Step, Gravity), Align to Path (Ascender, Descender, Center, Baseline) and Flip. A code or flag that can't be read leaves that option at its default. Flipped type opens with the frame's path reversed once, after a copied object takes its own art path, and an open path's end bracket stays so the type starts where the file's range ends. Spacing is still fitted from the file's layout. The fit now measures along the path when Align to Path moves the baselines off it, and it follows the glyphs' feet under 3D Ribbon, whose baselines stay level. --- crates/engine/tests/import_fuzz.rs | 42 ++++ crates/eps/src/import/ate.rs | 307 +++++++++++++++++++++++--- crates/eps/src/import/native/plan.rs | 1 + crates/eps/src/import/tests_native.rs | 109 ++++++++- docs/file-format-parity.md | 1 + 5 files changed, 428 insertions(+), 32 deletions(-) diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index 4eae16b21..6c0f3167c 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -3043,6 +3043,36 @@ fn arb_area_options() -> impl Strategy { prop::collection::vec((keys, values), 0..8).prop_map(|kv| kv.iter().map(|(k, v)| format!("/{k} {v}")).collect::>().join(" ")) } +/// Type on a Path Options as a frame might keep them at key 11: nothing, a value that isn't a +/// dictionary, or a dictionary of codes and flags of any value or type. +fn arb_path_options() -> impl Strategy { + let keys = prop::sample::select(vec!["0", "1", "2", "3", "4", "6", "7", "18", "22"]); + let values = prop::sample::select(vec![ + "0", + "1", + "2", + "3", + "4", + "5", + "-1", + "2.5", + "1e400", + "NaN", + "inf", + "(x)", + "true", + "false", + "/Name", + "[ 1 ]", + "<< /0 1 >>", + ]); + (prop::collection::vec((keys, values), 0..6), 0usize..3).prop_map(|(kv, shape)| match shape { + 0 => format!("/11 << {} >>", kv.iter().map(|(k, v)| format!("/{k} {v}")).collect::>().join(" ")), + 1 => "/11 3".to_string(), + _ => String::new(), + }) +} + /// A text document of `kinds.len()` stories, story `i` in frame `i` of kind `kinds[i]` (0 point type, /// 1 area type, 2 type on a path, others a kind the reader doesn't know), with a line break inside /// its paragraph (`\x03`) at each of `soft` (by character, wrapping, the story's end included) and @@ -3192,6 +3222,18 @@ proptest! { survive(".ai text frames with hostile area options", || Some(vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![], false, false, 1.0).0))?; } + /// The type on a path of the same text document with hostile Type on a Path Options, opened as + /// an EPS document and as a `.ai`'s editing data. + #[test] + fn text_frames_with_hostile_path_options_never_panic(options in arb_path_options()) { + let text = frames_document_text().replacen("/6 [ 0.5 2.0 ]", &format!("/6 [ 0.5 2.0 ] {options}"), 1); + let editing = text_editing_of(&text, FRAME_OBJECTS); + let bytes = editing_eps(&editing); + survive("EPS text frames with hostile path options", || vectorcraft_engine::cmd::fileio::load("x.eps", &bytes).ok().map(|l| l.doc))?; + let private = [b"%AI24_ZStandard_Data".as_slice(), &zstd(&editing)].concat(); + survive(".ai text frames with hostile path options", || Some(vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![], false, false, 1.0).0))?; + } + /// The same for vertical type and Tate-chu-yoko runs: orientations and baseline directions of /// any value, type or size. #[test] diff --git a/crates/eps/src/import/ate.rs b/crates/eps/src/import/ate.rs index 1e2347249..24a57cfa1 100644 --- a/crates/eps/src/import/ate.rs +++ b/crates/eps/src/import/ate.rs @@ -5,12 +5,13 @@ //! and `%AI11_EndTextDocument`, ASCII85 of nested dictionaries with numbers for keys. A text object //! in a layer is only a stub naming its story in it (`/StoryIndex`). //! -//! The format isn't documented. What is read here was worked out from the files of the project's -//! users, used locally and never committed, by comparing it with the type their pages draw: the -//! meaning of each key below was checked against the page's own type in more than a thousand text -//! objects, and it is only used for the keys that matched. Point type, area type and type on a path -//! are read; a story in a frame of any other kind is left alone: it has no text, and the importer -//! says so. +//! The format isn't documented. Most of what is read here was worked out from the files of the +//! project's users, used locally and never committed, by comparing it with the type their pages +//! draw: the meaning of those keys was checked against the page's own type in more than a thousand +//! text objects, and it is only used for the keys that matched. The Type on a Path Options of key +//! 11 of a frame come from test files saved with one option changed at a time, used locally and +//! never committed. Point type, area type and type on a path are read; a story in a frame of any +//! other kind is left alone: it has no text, and the importer says so. //! //! Where type sits: a story lays its lines out round a point `F` (the anchor: the start of a line of //! left-aligned type, the middle of a centred one, the end of a right-aligned one), each line with an @@ -24,7 +25,7 @@ use std::collections::BTreeMap; use kurbo::{BezPath, CubicBez, ParamCurve, ParamCurveArclen}; use vectorcraft_color::{Color, Paint}; -use vectorcraft_doc::{CharStyle, Justify, Node, NodeId, NodeKind, ParaDirection, ParaStyle, TextKind, TextObject, TextRun}; +use vectorcraft_doc::{CharStyle, Justify, Node, NodeId, NodeKind, ParaDirection, ParaStyle, PathAlign, PathEffect, TextKind, TextObject, TextRun}; use vectorcraft_geom::{Affine, PathData, Point, Rect}; /// Longest story read (bytes). @@ -254,7 +255,46 @@ enum FrameKind { /// Area Type Options the frame keeps. Area(Vec<[f64; 8]>, AreaFrame), /// Type on a path, from `start` to `end` (segment index and the place in that segment). - OnPath { segments: Vec<[f64; 8]>, start: f64, end: f64 }, + OnPath { segments: Vec<[f64; 8]>, start: f64, end: f64, options: PathFrame }, +} + +/// The Type on a Path Options a frame of type on a path keeps at key 11. Each stays at its default +/// where the frame leaves the key out or keeps a value that can't be read. A frame with key 6 true +/// keeps its defaults: what that state stores for these options isn't known. +#[derive(Debug, Clone, Copy, Default, PartialEq)] +struct PathFrame { + /// Key 1: 1 Skew, 2 3D Ribbon, 3 Stair Step, 4 Gravity. Rainbow is the key left out. + effect: PathEffect, + /// Key 2: 0 Ascender, 1 Descender, 2 Center. Baseline is the key left out. + align: PathAlign, + /// Key 0: the type is flipped across its path. The frame's path runs as it did before the flip, + /// from the start of its range. + flip: bool, +} + +impl PathFrame { + /// The options frame data `carries` keeps. + fn read(carries: &Val) -> PathFrame { + let Some(options) = carries.get("11") else { return PathFrame::default() }; + if matches!(options.get("6"), Some(Val::Bool(true))) { + return PathFrame::default(); + } + let code = |k: &str| options.get(k).and_then(Val::num); + let effect = match code("1") { + Some(1.0) => PathEffect::Skew, + Some(2.0) => PathEffect::Ribbon3d, + Some(3.0) => PathEffect::StairStep, + Some(4.0) => PathEffect::Gravity, + _ => PathEffect::Rainbow, + }; + let align = match code("2") { + Some(0.0) => PathAlign::Ascender, + Some(1.0) => PathAlign::Descender, + Some(2.0) => PathAlign::Center, + _ => PathAlign::Baseline, + }; + PathFrame { effect, align, flip: matches!(options.get("0"), Some(Val::Bool(true))) } + } } /// The Area Type Options an area frame keeps, in the units of the text document: each `None` @@ -484,7 +524,7 @@ impl Texts { let start = finite(range.first()).unwrap_or(0.0).clamp(0.0, n); (start, finite(range.get(1)).unwrap_or(n).clamp(start, n)) }; - Some(FrameKind::OnPath { segments, start, end }) + Some(FrameKind::OnPath { segments, start, end, options: PathFrame::read(carries) }) } _ => None, } @@ -527,8 +567,10 @@ impl Texts { 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. - if matches!(frames.first(), Some(FrameKind::OnPath { .. })) && !vertical { - path_spacing = self::path_spacing(f); + if let Some(FrameKind::OnPath { options, .. }) = frames.first() + && !vertical + { + path_spacing = self::path_spacing(f, options.effect); } if let Some(a) = f.get("0").and_then(|a| a.get("0")).and_then(Val::nums) && let [x, y, ..] = a.as_slice() @@ -721,11 +763,21 @@ fn length_fraction(segments: &[[f64; 8]], t: f64) -> f64 { /// (`vectorcraft_text`'s `path_steps`): between two glyphs' centres, the path's length `s`, their /// advances' mean `a` and how far it turns `t` (away from their tops) make `s + t × spacing = a`. /// Fitted over every pair of the layout's glyphs; none where the path doesn't turn under them. -fn path_spacing(f: &Val) -> f64 { +/// The length `s` is on the path: Align to Path moves the baselines off it, by the distance `G` +/// keeps. Under 3D Ribbon the glyphs' baselines stay level, so their feet show how the path turns. +fn path_spacing(f: &Val, effect: PathEffect) -> f64 { let mut found = vec![]; find_nodes(f, "G", &mut found); let (mut sum, mut turns) = (0.0, 0.0); for g in found { + // How far the baseline lies from the path, towards the glyphs' feet. Only Rainbow's and 3D + // Ribbon's feet are square to the path; Skew's are upright, so its alignment moves every + // baseline the same way and leaves the distances between them. + let offset = if matches!(effect, PathEffect::Rainbow | PathEffect::Ribbon3d) { + g.get("0").and_then(|o| o.get("0")).and_then(Val::nums).and_then(|o| o.get(1).copied()).filter(|o| o.is_finite()).unwrap_or(0.0) + } else { + 0.0 + }; // Each glyph's origin, its matrix (along its baseline, then towards its foot) and where it // ends along the line; the lists end with where the line ends. let Some(at) = g.get("21") else { continue }; @@ -743,12 +795,18 @@ fn path_spacing(f: &Val) -> f64 { }) .collect(); for w in glyphs.windows(2) { - let [(p, a0, along, foot), (q, a1, next, _)] = w else { continue }; - let turn = next.dot(*foot).atan2(next.dot(*along)); + let [(p, a0, along, foot), (q, a1, next, next_foot)] = w else { continue }; + let (along, next) = if effect == PathEffect::Ribbon3d { + (kurbo::Vec2::new(foot.y, -foot.x), kurbo::Vec2::new(next_foot.y, -next_foot.x)) + } else { + (*along, *next) + }; + let turn = next.dot(*foot).atan2(next.dot(along)); // The length of the arc between the centres (the path is taken to be round there). A // corner (a turn of more than a radian between two glyphs) isn't round: left out. let half = turn.abs() * 0.5; - let length = if half > 1e-9 { (*q - *p).hypot() * half / half.sin() } else { (*q - *p).hypot() }; + let chord = (*q - *p).hypot(); + let length = if half > 1e-9 { chord * half / half.sin() } else { chord } + turn * offset; if turn.abs() <= 1.0 && length.is_finite() && a0.is_finite() && a1.is_finite() { sum += ((a0 + a1) * 0.5 - length) * turn; turns += turn * turn; @@ -850,11 +908,12 @@ impl Story { (scale, Affine::new([a / scale, b / scale, c / scale, d / scale, at.x, at.y]), TextKind::Point) } FrameKind::Area(s, _) => (1.0, Affine::IDENTITY, TextKind::Area { frame: path_of(s, template, to_doc) }), - FrameKind::OnPath { segments, start, end } => { + FrameKind::OnPath { segments, start, end, options } => { let n = segments.len() as f64; // A closed path keeps its end bracket wherever it is (the same place as the start: a - // full turn, see `TextKind::path_span`); an open one's end at its end is none. - let end = (closed(segments) || *end < n - 0.02).then(|| length_fraction(segments, *end)); + // full turn, see `TextKind::path_span`); an open one's end at its end is none, unless + // a flip makes it the place the type starts. + let end = (closed(segments) || options.flip || *end < n - 0.02).then(|| length_fraction(segments, *end)); (1.0, Affine::IDENTITY, TextKind::OnPath { path: path_of(segments, template, to_doc), start: length_fraction(segments, *start), end }) } }; @@ -913,8 +972,13 @@ impl Story { t.path_spacing = self.path_spacing * scale; } // Each frame of a thread keeps its own options. - if let Some(FrameKind::Area(_, options)) = self.frames.get(frame) { - options.apply(&mut t.area, scale); + match self.frames.get(frame) { + Some(FrameKind::Area(_, options)) => options.apply(&mut t.area, scale), + Some(FrameKind::OnPath { options, .. }) => { + t.path_effect = options.effect; + t.path_align = options.align; + } + _ => {} } t.vertical = self.vertical; t.kind = kind; @@ -922,6 +986,17 @@ impl Story { Some(Node::new(id, NodeKind::Text(Box::new(t)))) } + /// Flips type on a path (`node`, made from frame `frame`) when its frame keeps Flip on. The + /// frame's path runs as it did before the flip, so this runs once, on the path the object ends + /// up with. + pub(super) fn flip_on_path(&self, node: &mut Node, frame: usize) { + if let Some(FrameKind::OnPath { options: PathFrame { flip: true, .. }, .. }) = self.frames.get(frame) + && let NodeKind::Text(t) = &mut node.kind + { + t.flip_on_path(); + } + } + /// Area type that the file lays lines out in, in a frame too short for its first line as it is /// laid out here: a font that isn't available shows in the fallback font, whose lines may be /// taller, and type that doesn't fit its frame isn't drawn. The frame is made taller, from its @@ -1241,9 +1316,14 @@ mod tests { /// A text document's frame on a path of `segments` (each `x0 y0 x1 y1 x2 y2 x3 y3`) with the /// range `range`, written by hand. fn on_path_frame(segments: &[[f64; 8]], range: [f64; 2]) -> Option { + on_path_frame_with(segments, range, "") + } + + /// The same with `options` in the frame data too. + fn on_path_frame_with(segments: &[[f64; 8]], range: [f64; 2], options: &str) -> Option { let nums = |v: &[f64]| v.iter().map(|n| format!("{n}")).collect::>().join(" "); let all: Vec = segments.iter().flatten().copied().collect(); - let doc = format!("/0 << /1 << /0 [ {} ] >> /2 << /0 2 /6 [ {} ] >> >>", nums(&all), nums(&range)); + let doc = format!("/0 << /1 << /0 [ {} ] >> /2 << /0 2 /6 [ {} ] {options} >> >>", nums(&all), nums(&range)); let texts = Texts { stories: vec![], fonts: vec![], @@ -1277,6 +1357,123 @@ mod tests { (*t, b - a) } + /// Key 11's codes for the effect and the alignment, each read on its own. The defaults are the + /// keys left out. + #[test] + fn type_on_a_path_keeps_its_effect_and_alignment() { + use PathAlign as A; + use PathEffect as E; + let read = |options: &str| { + let (t, _) = on_path("type", on_path_frame_with(&circle(), [0.0, 3.99], options).unwrap()); + (t.path_effect, t.path_align) + }; + assert_eq!(read(""), (E::Rainbow, A::Baseline)); + assert_eq!(read("/11 << /4 21 /18 21.0 /6 false /22 0.025 >>"), (E::Rainbow, A::Baseline)); + for (code, effect) in [(1, E::Skew), (2, E::Ribbon3d), (3, E::StairStep), (4, E::Gravity)] { + assert_eq!(read(&format!("/11 << /1 {code} >>")), (effect, A::Baseline), "effect {code}"); + } + for (code, align) in [(0, A::Ascender), (1, A::Descender), (2, A::Center)] { + assert_eq!(read(&format!("/11 << /2 {code} >>")), (E::Rainbow, align), "alignment {code}"); + } + assert_eq!(read("/11 << /1 3 /2 1 >>"), (E::StairStep, A::Descender)); + } + + /// A frame with key 6 true keeps the defaults, Flip included. + #[test] + fn type_on_a_path_with_key_6_true_keeps_the_defaults() { + let Some(FrameKind::OnPath { options, .. }) = on_path_frame_with(&circle(), [0.0, 3.99], "/11 << /6 true /0 true /1 1 /2 0 >>") else { + panic!("on a path") + }; + assert_eq!(options, PathFrame::default()); + } + + /// Key 11 where it can't be read leaves the options at their defaults, each on its own. + #[test] + fn type_on_a_path_options_that_cannot_be_read_keep_their_defaults() { + let bad = ["5", "-1", "1.5", "1e300", "(1)", "true", "/Skew", "[ 1 ]", "<< /0 1 >>"]; + for value in bad { + for key in ["1", "2", "0"] { + let Some(FrameKind::OnPath { options, .. }) = on_path_frame_with(&circle(), [0.0, 3.99], &format!("/11 << /{key} {value} >>")) else { + panic!("on a path") + }; + let expected = PathFrame { flip: key == "0" && value == "true", ..PathFrame::default() }; + assert_eq!(options, expected, "key {key} value {value}"); + } + } + for whole in ["3", "true", "(x)", "[ /1 1 ]"] { + let Some(FrameKind::OnPath { options, .. }) = on_path_frame_with(&circle(), [0.0, 3.99], &format!("/11 {whole}")) else { + panic!("on a path") + }; + assert_eq!(options, PathFrame::default(), "{whole}"); + } + // One option that can't be read leaves the other as the frame keeps it. + let Some(FrameKind::OnPath { options, .. }) = on_path_frame_with(&circle(), [0.0, 3.99], "/11 << /1 9 /2 2 >>") else { panic!("on a path") }; + assert_eq!((options.effect, options.align), (PathEffect::Rainbow, PathAlign::Center)); + } + + /// The first point of type on a path's path, and its span. + fn flipped_on_path(frames: Vec, frame: usize) -> (f64, (f64, f64)) { + let mut st = story("type", &[0.0], &Para::default()); + st.frames = frames; + let mut node = st.node(NodeId(1), frame, (0.0, 0.0), Affine::IDENTITY).unwrap(); + st.flip_on_path(&mut node, frame); + let NodeKind::Text(t) = node.kind else { panic!("not type") }; + let vectorcraft_doc::TextKind::OnPath { path, .. } = &t.kind else { panic!("type on a path") }; + let Some(kurbo::PathEl::MoveTo(first)) = path.to_bezpath().elements().first().copied() else { panic!("a path") }; + (first.x + CANVAS_CENTRE, t.kind.path_span().unwrap()) + } + + /// Flip is a flag: the frame's path and range run as before the flip, so the type flips once, + /// keeps its stretch of the path, and the path runs from its far end. + #[test] + fn flipped_type_on_a_path_runs_the_path_the_other_way_once() { + let open = [[0.0, 0.0, 30.0, 0.0, 70.0, 0.0, 100.0, 0.0], [100.0, 0.0, 130.0, 0.0, 170.0, 0.0, 200.0, 0.0]]; + let frame = |options: &str| vec![on_path_frame_with(&open, [0.5, 1.5], options).unwrap()]; + let (first, (from, to)) = flipped_on_path(frame("/11 << /0 false >>"), 0); + assert_eq!(first, 0.0); + assert!((from - 0.25).abs() < 1e-3 && (to - 0.75).abs() < 1e-3, "{from} {to}"); + let (first, (from, to)) = flipped_on_path(frame("/11 << /0 true >>"), 0); + assert_eq!(first, 200.0, "from the path's far end"); + assert!((from - 0.25).abs() < 1e-3 && (to - 0.75).abs() < 1e-3, "the same stretch: {from} {to}"); + } + + /// The end of an open path's range, which doesn't move unflipped type, is where flipped type + /// starts: a range that ends just short of the path's end starts the type that far from it. + #[test] + fn flipped_type_on_an_open_path_starts_at_the_end_of_its_range() { + let third = 100.0 / 3.0; + let line = [[0.0, 0.0, third, 0.0, 2.0 * third, 0.0, 100.0, 0.0]]; + let frame = |options: &str| vec![on_path_frame_with(&line, [0.0, 0.99], options).unwrap()]; + let (_, (from, to)) = flipped_on_path(frame("/11 << /0 true >>"), 0); + assert!((from - 0.01).abs() < 1e-3 && (to - 1.0).abs() < 1e-3, "{from} {to}"); + let (_, (from, to)) = flipped_on_path(frame(""), 0); + assert!(from.abs() < 1e-3 && (to - 1.0).abs() < 1e-3, "unflipped: {from} {to}"); + } + + /// Round a closed path, a flip keeps the stretch too, and each frame of a thread keeps its own + /// options. + #[test] + fn each_frame_of_type_on_a_path_keeps_its_options() { + let mut st = story("type", &[0.0], &Para::default()); + st.frames = vec![ + on_path_frame_with(&circle(), [0.5, 2.5], "/11 << /0 true /1 1 >>").unwrap(), + on_path_frame_with(&circle(), [0.0, 3.99], "/11 << /2 0 >>").unwrap(), + ]; + let mut first = st.node(NodeId(1), 0, (0.0, 0.0), Affine::IDENTITY).unwrap(); + let mut second = st.node(NodeId(2), 1, (0.0, 0.0), Affine::IDENTITY).unwrap(); + let before = first.clone(); + st.flip_on_path(&mut first, 0); + assert_ne!(before, first, "the first frame flips"); + let before = second.clone(); + st.flip_on_path(&mut second, 1); + assert_eq!(before, second, "the second frame doesn't"); + let (NodeKind::Text(a), NodeKind::Text(b)) = (&first.kind, &second.kind) else { panic!("not type") }; + assert_eq!((a.path_effect, a.path_align), (PathEffect::Skew, PathAlign::Baseline)); + assert_eq!((b.path_effect, b.path_align), (PathEffect::Rainbow, PathAlign::Ascender)); + let (from, to) = a.kind.path_span().unwrap(); + assert!((to - from - 0.5).abs() < 1e-3, "{from} {to}"); + } + /// Round a closed path the file writes the end bracket unwrapped, past the last segment (the /// start plus the run): the type keeps that run, nearly the whole circle here, not the bit /// from the start to the path's end. @@ -1355,6 +1552,12 @@ mod tests { /// outside it (their feet towards its centre) or inside it, the length of the circle between /// their centres `fraction` of their advances' mean. fn round_layout(advances: &[f64], radius: f64, outside: bool, fraction: f64) -> Val { + round_layout_with(advances, radius, outside, fraction, "", false) + } + + /// The same with `extra` in the line's `G`, and baselines that stay level as 3D Ribbon's do when + /// `level`. + fn round_layout_with(advances: &[f64], radius: f64, outside: bool, fraction: f64, extra: &str, level: bool) -> Val { let sign = if outside { 1.0 } else { -1.0 }; let (mut xs, mut ys, mut matrices, mut ends) = (vec![], vec![], vec![], vec![0.0]); let mut angle = std::f64::consts::PI; @@ -1365,14 +1568,15 @@ mod tests { // Outside, clockwise on the page (y down); inside, the other way round. let along = (-angle.sin() * sign, angle.cos() * sign); let centre = (radius * angle.cos(), radius * angle.sin()); - xs.push(centre.0 - along.0 * a * 0.5); - ys.push(centre.1 - along.1 * a * 0.5); - matrices.extend([along.0, along.1, -along.1, along.0, 0.0, 0.0]); + let baseline = if level { (1.0, 0.0) } else { along }; + xs.push(centre.0 - baseline.0 * a * 0.5); + ys.push(centre.1 - baseline.1 * a * 0.5); + matrices.extend([baseline.0, baseline.1, -along.1, along.0, 0.0, 0.0]); ends.push(ends[i] + a); } let nums = |v: &[f64]| v.iter().map(|n| format!("{n}")).collect::>().join(" "); let doc = format!( - "/0 << /99 /F /6 [ << /99 /L /6 [ << /99 /S /6 [ << /99 /G /21 << /1 [ {} ] /2 [ {} ] /3 [ {} ] /4 [ {} ] >> >> ] >> ] >> ] >>", + "/0 << /99 /F /6 [ << /99 /L /6 [ << /99 /S /6 [ << /99 /G {extra} /21 << /1 [ {} ] /2 [ {} ] /3 [ {} ] /4 [ {} ] >> >> ] >> ] >> ] >>", nums(&xs), nums(&ys), nums(&matrices), @@ -1388,18 +1592,59 @@ mod tests { fn the_layout_of_type_on_a_path_gives_its_spacing() { let advances = [20.0, 30.0, 10.0, 25.0, 20.0]; // Outside a circle of radius 100, 10 above the path: the centres are 100/110 of the advances apart. - let spacing = path_spacing(&round_layout(&advances, 100.0, true, 100.0 / 110.0)); + let rainbow = PathEffect::Rainbow; + let spacing = path_spacing(&round_layout(&advances, 100.0, true, 100.0 / 110.0), rainbow); assert!((spacing - 10.0).abs() < 1e-6, "{spacing}"); // Inside it, 20 above the path: 100/80 of them. - let spacing = path_spacing(&round_layout(&advances, 100.0, false, 100.0 / 80.0)); + let spacing = path_spacing(&round_layout(&advances, 100.0, false, 100.0 / 80.0), rainbow); assert!((spacing - 20.0).abs() < 1e-6, "{spacing}"); // At their advances: none. - assert!(path_spacing(&round_layout(&advances, 100.0, true, 1.0)).abs() < 1e-6); + assert!(path_spacing(&round_layout(&advances, 100.0, true, 1.0), rainbow).abs() < 1e-6); // Corners (here a turn of about four radians from glyph to glyph) aren't taken for a curve. - assert_eq!(path_spacing(&round_layout(&advances, 5.0, true, 1.0)), 0.0); + assert_eq!(path_spacing(&round_layout(&advances, 5.0, true, 1.0), rainbow), 0.0); // A straight line turns nowhere, and a layout without glyphs says nothing. - assert_eq!(path_spacing(&round_layout(&advances, 1e12, true, 0.9)), 0.0); - assert_eq!(path_spacing(&read(b"/99 /F").unwrap()), 0.0); + assert_eq!(path_spacing(&round_layout(&advances, 1e12, true, 0.9), rainbow), 0.0); + assert_eq!(path_spacing(&read(b"/99 /F").unwrap(), rainbow), 0.0); + } + + /// Align to Path moves the baselines off the path, and the spacing is the one on the path. On a + /// circle of radius 100 with spacing 10, the path's length between two centres is 100/110 of + /// their advances. Baselines on a smaller or larger circle are that much closer or further. + #[test] + fn the_spacing_of_aligned_type_is_measured_on_its_path() { + let advances = [20.0, 30.0, 10.0, 25.0, 20.0]; + let fit = + |radius: f64, extra: &str| path_spacing(&round_layout_with(&advances, radius, true, radius / 110.0, extra, false), PathEffect::Rainbow); + // 20 towards the feet: Ascender. + let spacing = fit(80.0, "/0 << /0 [ 0 20 ] >>"); + assert!((spacing - 10.0).abs() < 1e-6, "{spacing}"); + // 8 away from them: Descender. + let spacing = fit(108.0, "/0 << /0 [ 0 -8 ] >>"); + assert!((spacing - 10.0).abs() < 1e-6, "{spacing}"); + // A distance that can't be read counts as none. + for bad in ["/0 3", "/0 << /0 (x) >>", "/0 << /0 [ 0 ] >>"] { + let spacing = fit(100.0, bad); + assert!((spacing - 10.0).abs() < 1e-6, "{bad}: {spacing}"); + } + // Skew's feet are upright: its alignment moves the baselines without changing their + // lengths, so the distance doesn't count. + let layout = |extra: &str| round_layout_with(&advances, 100.0, true, 100.0 / 110.0, extra, false); + let plain = path_spacing(&layout(""), PathEffect::Skew); + let aligned = path_spacing(&layout("/0 << /0 [ 0 20 ] >>"), PathEffect::Skew); + assert!((aligned - plain).abs() < 1e-9, "{plain} {aligned}"); + // 3D Ribbon's feet are square to the path, as Rainbow's are. + let level = round_layout_with(&advances, 80.0, true, 80.0 / 110.0, "/0 << /0 [ 0 20 ] >>", true); + let spacing = path_spacing(&level, PathEffect::Ribbon3d); + assert!((spacing - 10.0).abs() < 1e-6, "{spacing}"); + } + + /// 3D Ribbon keeps its baselines level, so its glyphs' feet show how the path turns. + #[test] + fn the_spacing_of_3d_ribbon_type_follows_the_feet_of_its_glyphs() { + let advances = [20.0, 30.0, 10.0, 25.0, 20.0]; + let layout = round_layout_with(&advances, 100.0, true, 100.0 / 110.0, "", true); + let spacing = path_spacing(&layout, PathEffect::Ribbon3d); + assert!((spacing - 10.0).abs() < 1e-6, "{spacing}"); } /// The leading of the text object a story comes in as. diff --git a/crates/eps/src/import/native/plan.rs b/crates/eps/src/import/native/plan.rs index bac7108ac..3cfc3a6b5 100644 --- a/crates/eps/src/import/native/plan.rs +++ b/crates/eps/src/import/native/plan.rs @@ -74,6 +74,7 @@ pub(super) fn make_slots( // Area type and type on a path in the object's own frame: the text document's frame // of a copied object keeps its original's path. frame_text_from_art(info, &mut node, &mut region); + story.flip_on_path(&mut node, info.frame); if let NodeKind::Text(text) = &mut node.kind && story.fit_first_line(text) { diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index dde26bc8d..f52a8baa0 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -3857,6 +3857,37 @@ fn area_type_and_type_on_a_path_come_from_the_text_document() { assert!(!r.warnings.iter().any(|w| w.contains("couldn't be read")), "{:?}", r.warnings); } +/// Type on a path keeps the effect, alignment and flip its frame keeps. The flip applies to the +/// object's own path, the one its art draws, once. +#[test] +fn type_on_a_path_keeps_its_type_on_a_path_options() { + let (a, b, c) = (canvas(10.0, 70.0), canvas(30.0, 70.0), canvas(50.0, 70.0)); + let segments = + format!("{} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {}", a.0, a.1, a.0, a.1, b.0, b.1, b.0, b.1, b.0, b.1, b.0, b.1, c.0, c.1, c.0, c.1); + let frame = |options: &str| format!("<< /0 << /0 [ 0 0 ] /1 << /0 [ {segments} ] >> /2 << /0 2 /6 [ 0.5 2.0 ] {options} >> >> >>"); + let frames = [frame("/11 << /0 true /1 1 /2 0 /4 -28 /18 -28.0 /6 false /22 0.025 >>"), frame("")]; + let stories = [story("Flipped\\r", &[0], "/0 0", "/1 8.0"), story("Plain\\r", &[1], "/0 0", "/1 8.0")]; + let doc = document_of(&frames, &stories); + let objects = text_object(0) + &text_object(1); + let art = native_with_text(&(layer("Back", true, &square(10, 10)) + &layer("Spare", false, &objects)), &doc); + let r = import(&eps(&page_square(10, 10), &art)).unwrap(); + let texts = text_of(&r.document.layers[1]); + assert_eq!(texts.len(), 2, "{:?}", r.warnings); + let (flipped, plain) = (texts[0], texts[1]); + assert_eq!((flipped.path_effect, flipped.path_align), (vectorcraft_doc::PathEffect::Skew, vectorcraft_doc::PathAlign::Ascender)); + assert_eq!((plain.path_effect, plain.path_align), (vectorcraft_doc::PathEffect::Rainbow, vectorcraft_doc::PathAlign::Baseline)); + let first_x = |t: &vectorcraft_doc::TextObject| { + let vectorcraft_doc::TextKind::OnPath { path, .. } = &t.kind else { panic!("type on a path: {:?}", t.kind) }; + let Some(kurbo::PathEl::MoveTo(p)) = path.to_bezpath().elements().first().copied() else { panic!("a path") }; + p.x + }; + assert_eq!(first_x(plain), 10.0); + assert_eq!(first_x(flipped), 50.0); + // The file's range, 0.25 to the end of the path, stays that stretch of it. + let (from, to) = flipped.kind.path_span().unwrap(); + assert!(from.abs() < 1e-3 && (to - 0.75).abs() < 1e-3, "{from} {to}"); +} + /// Area type keeps the rows, columns, gutters and first baseline its frame keeps, and so does /// each frame of a thread; a frame without them keeps the defaults. #[test] @@ -4139,6 +4170,11 @@ fn a_thai_story_stays_one_text_object() { /// A story of `text` on frame 0 whose layout sets glyphs `advance` wide round the outside of a /// circle of radius `radius`, the circle's length between their centres `fraction` of an advance. fn round_story(text: &str, advance: f64, radius: f64, fraction: f64) -> String { + round_story_with(text, advance, radius, fraction, "") +} + +/// The same with `extra` in the line's `G`. +fn round_story_with(text: &str, advance: f64, radius: f64, fraction: f64, extra: &str) -> String { let n = text.chars().count(); let (mut xs, mut ys, mut matrices, mut ends) = (vec![], vec![], vec![], vec![]); for i in 0..=n { @@ -4154,7 +4190,7 @@ fn round_story(text: &str, advance: f64, radius: f64, fraction: f64) -> String { format!( "<< /0 << /0 ({text}) /5 << /0 [ << /0 << /0 << /0 () /5 << /0 0 >> /6 0 >> >> /1 {n} >> ] >> \ /6 << /0 [ << /0 << /0 << /0 () /5 0 /6 << /1 12.0 >> >> >> /1 {n} >> ] >> >> /1 << /0 [ << /0 0 >> ] \ - /2 [ << /99 /PC /6 [ << /99 /F /6 [ << /99 /L /6 [ << /99 /S /6 [ << /99 /G /21 << /1 [ {} ] /2 [ {} ] /3 [ {} ] /4 [ {} ] >> >> ] >> ] >> ] >> ] >> ] >> >>", + /2 [ << /99 /PC /6 [ << /99 /F /6 [ << /99 /L /6 [ << /99 /S /6 [ << /99 /G {extra} /21 << /1 [ {} ] /2 [ {} ] /3 [ {} ] /4 [ {} ] >> >> ] >> ] >> ] >> ] >> ] >> >>", nums(&xs), nums(&ys), matrices.join(" "), @@ -4214,6 +4250,57 @@ fn type_round_a_circle_is_spaced_as_the_file_lays_it_out() { } } +/// Type aligned to its path by its ascender: the file's baselines run on a smaller circle than the +/// path, and the type comes in spaced as it runs along the path, not along those baselines. +#[test] +fn aligned_type_round_a_circle_runs_along_its_path_as_the_file_lays_it_out() { + let k = 40.0 * 0.552_284_75; + let points = [ + (90.0, 50.0), + (90.0, 50.0 - k), + (50.0 + k, 10.0), + (50.0, 10.0), + (50.0 - k, 10.0), + (10.0, 50.0 - k), + (10.0, 50.0), + (10.0, 50.0 + k), + (50.0 - k, 90.0), + (50.0, 90.0), + (50.0 + k, 90.0), + (90.0, 50.0 + k), + (90.0, 50.0), + ]; + let all: Vec<(f64, f64)> = points.iter().map(|(x, y)| canvas(*x, *y)).collect(); + let segment = |i: usize| (0..4).map(|j| format!("{} {}", all[i * 3 + j].0, all[i * 3 + j].1)).collect::>().join(" "); + let frame = format!( + "<< /0 << /0 [ 0 0 ] /1 << /0 [ {} ] >> /2 << /0 2 /6 [ 2.0 5.99 ] /11 << /2 0 >> >> >> >>", + (0..4).map(segment).collect::>().join(" ") + ); + // Baselines 10 inside the circle of radius 40. Along the path the centres of glyphs 5 wide are + // 5 × 40/48 apart (spacing 8), so on the baselines they are 5 × 30/48 apart. + let doc = document_of(&[frame], &[round_story_with("abcdefgh", 5.0, 30.0, 30.0 / 48.0, "/0 << /0 [ 0 10 ] >>")]); + let art = native_with_text(&(layer("Back", true, &square(10, 10)) + &layer("Spare", false, &text_object(0))), &doc); + let r = import(&eps(&page_square(10, 10), &art)).unwrap(); + let texts = text_of(&r.document.layers[1]); + assert_eq!(texts.len(), 1, "{:?}", r.warnings); + let t = texts[0]; + assert_eq!(t.path_align, vectorcraft_doc::PathAlign::Ascender); + assert!((t.path_spacing - 8.0).abs() < 0.05, "{}", t.path_spacing); + let layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t); + let centres: Vec<(f64, f64)> = layout + .glyphs + .iter() + .map(|g| (g.origin.x + g.angle.cos() * g.advance * 0.5 - 50.0, g.origin.y + g.angle.sin() * g.advance * 0.5 - 50.0, g.advance)) + .map(|(x, y, a)| (y.atan2(x), a)) + .collect(); + assert_eq!(centres.len(), 8); + for w in centres.windows(2) { + let turn = (w[1].0 - w[0].0 + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU) - std::f64::consts::PI; + let ratio = turn.abs() * 40.0 / ((w[0].1 + w[1].1) * 0.5); + assert!((ratio - 40.0 / 48.0).abs() < 0.01, "{ratio}"); + } +} + #[test] fn threaded_area_type_flows_through_its_frames() { let frames = [ @@ -4430,6 +4517,26 @@ fn a_copied_area_type_object_is_framed_where_its_own_art_path_is() { assert!((b.x0 - 30.0).abs() < 1e-6 && (b.y0 - 30.0).abs() < 1e-6 && (b.x1 - 80.0).abs() < 1e-6 && (b.y1 - 50.0).abs() < 1e-6, "{b:?}"); } +/// A copy of flipped type on a path takes its own `/Art` path, and that path is the one that flips. +#[test] +fn a_copied_flipped_type_on_a_path_flips_its_own_art_path() { + let (a, b) = (canvas(10.0, 20.0), canvas(60.0, 20.0)); + let path = format!( + "<< /0 << /0 [ 0 0 ] /1 << /0 [ {} {} {} {} {} {} {} {} ] >> /2 << /0 2 /6 [ 0.0 1.0 ] /11 << /0 true >> >> >> >>", + a.0, a.1, a.0, a.1, b.0, b.1, b.0, b.1 + ); + let doc = document_of(&[path], &[story("Copied\r", &[0], "/0 0", "/1 8.0")]); + let copy = "/AI11Text :\n0 /FreeUndo ,\n0 /FrameIndex ,\n0 /StoryIndex ,\n/Art :\nX=\n30 50 m\n80 50 L\nN\n;\n/ConfiningPath ,\n2 /TextAntialiasing ,\n;\n"; + let art = native_with_text(&(layer("Back", true, &square(10, 10)) + &layer("Spare", false, copy)), &doc); + let r = import(&eps(&page_square(10, 10), &art)).unwrap(); + let texts = text_of(&r.document.layers[1]); + assert_eq!(texts.len(), 1, "{:?}", r.warnings); + let vectorcraft_doc::TextKind::OnPath { path, .. } = &texts[0].kind else { panic!("type on a path: {:?}", texts[0].kind) }; + // The copy's path runs from x 30 to x 80 at art y 50, so flipped it starts at x 80, 50 down the page. + let Some(kurbo::PathEl::MoveTo(first)) = path.to_bezpath().elements().first().copied() else { panic!("a path") }; + assert_eq!((first.x, first.y), (80.0, 50.0)); +} + /// A raster print page can carry a fully vector editing copy. The legacy mesh has no /// opacity operand and its vector mask hides half of it: ignoring either loses this import. #[test] diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 21c35d24f..862c90856 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -164,6 +164,7 @@ private editing data. VectorCraft: ### Illustrator's private editing data +the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPrivateData` streams, a PDF's `AIPDFPrivateData` streams when it was saved with the editing data (#868), Zstandard or zlib, the EPS's in ASCII85) is not documented as a whole. VectorCraft reads its structure: layers and sublayers (names, order, visibility, lock, printing, preview, dimming, colour), groups as they were nested, compound paths (painted by their last path, or the last painted one, with the paths of groups inside them kept as their paths: #1410), clipping groups and layer clipping (several clipping paths in a clipping group, a layer or an opacity mask's art clip to their intersection, and a compound path of clipping paths stays one clip; clipping paths that would nest the art past the document's 32 levels stop the editing data from being read: #5789), object names, hidden and locked objects, fills and strokes (grey, CMYK, RGB, named and spot colours, spot colours defined in Lab with their Lab values (#1032), global process colours as global swatches at their tint; a stroke of width 0, `0 w`, on a path, a gradient-filled path, legacy type or in an art style comes in as a 0.25 pt hairline, as from PDF: #1671), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), gradient and pattern names in the file up to their first newline (a name that another swatch, or a gradient or pattern defined earlier, already has is numbered "Sky 2"; a name without a newline is kept, and when another swatch has it, the pattern's swatch, or a named color sharing a gradient or pattern swatch's name, is numbered the same way), opacity, blend modes, isolation and knockout, embedded images where their matrix puts them, rotated ones too (#2269), with their alpha channel (CMYK images keep their ink amounts, as they do when a `.ai` opens from its PDF part, and an alpha channel that hides part of a CMYK image becomes a grayscale opacity mask), placed (linked) files as linked images in their place, each showing the low-resolution preview the file keeps of it until its file is found or relinked in the Links panel (#1264), with the folder the file keeps relative to the document's, where the link is looked for when its path is gone, guides, and every artboard where it is, with its center mark, cross hairs, video safe areas and video ruler pixel aspect ratio. VectorCraft reads `CropAreaActive`, an `/Int` in the dictionary that holds the artboard array, as the index of the active artboard in that array, and the file opens with that artboard active; without it, or when it names no artboard that opens, the first artboard is active. It also reads the Document Raster Effects Settings (resolution, color model, background, anti-alias, clipping mask, Add Around Object and Preserve Spot Colors), the author from the header's `%%For` (an EPS's own header when the editing data's has none), swatches for the header's `%%CMYKCustomColor` and `%%RGBCustomColor` colors that the document has none of, and `%%TemplateBox` in older files; a setting with a value of another type or out of range keeps its default. Objects on Path groups open live, with their path, their objects in order along it, Pivot and Rotate; one with a dragged spacing or other options opens as a plain group. A gradient with stops in the middle runs its whole ramp across its shape, and a gradient object does not pass its fill to the next path (#1390). An object whose art style is one fill and one stroke at most (solid colours, its own gradient, a variable-width profile) with Drop Shadow, Inner Glow, Outer Glow, Feather, Round Corners, Scribble, Offset Path, Outline Object, Outline Stroke, Convert to Shape, Tweak (only when it moves nothing) or Pucker & Bloat at 0 comes in as itself with them and their saved settings. A path, compound path or group (not a clipping group) whose only effect is Transform (not with copies or Random) or a Free Distort whose corners make a parallelogram comes in as itself with it too, when neither it nor anything in it paints with a gradient or pattern and everything in it is a path, compound path or group with no opacity mask and no effects of its own; Transform that scales, rotates or reflects needs nothing in it to be stroked. Twist as the only effect is read the same way on an unstroked path or compound path whose segments are all straight. One with more, other effects (Zig Zag, a Pucker & Bloat of another amount, a Free Distort with other corners), an effect setting that is not read (an unknown key, a missing or out-of-range value, a Scribble with a color of its own), dashes aligned to corners, a brush other than a Calligraphic one, or a Calligraphic brush with settings of its own or an angle that follows the path comes in as its drawn look (a group named after it). A path with a Calligraphic brush otherwise comes in as the path, the brush on its stroke at the stroke's weight, which scales the brush (#372). A Calligraphic brush's Pressure, Tilt, Bearing and Rotation come in as those channels (a 6D Art Pen's brushes vary with the pressure, the bearing and the barrel's rotation); its Stylus Wheel varies at random. The file's Calligraphic, Scatter, Art and Bristle brushes replace the default ones in the Brushes panel, in the file's order, also when the file comes in as its PDF part (a brush library has no layers); pattern brushes and brushes it can't read are left out with a note. Type keeps its art style as a live appearance whatever its stack, so it can be retyped: each fill and stroke of the style's stack (solid colours, gradients and pattern fills, placed as the drawn look places them) with its own opacity, blend mode and effects, the effects on the whole object, and where the characters paint among them; the effects read are Warp, Offset Path, Outline Object, Outline Stroke, Transform (with copies, Transform Objects, Transform Patterns and Scale Strokes & Effects; not with Random), Free Distort, Pucker & Bloat, Twist, Zig Zag, Tweak (only when it moves nothing, the file holding none of its random outline), Drop Shadow, Inner Glow, Outer Glow, Feather, Round Corners, Scribble, Gaussian Blur, Bas Relief (its settings at their defaults) and Convert to Shape, and a style with others, or with an effect setting that isn't read, keeps the type's drawn look. Convert to Shape keeps its Rectangle, Rounded Rectangle or Ellipse, its Absolute or Relative size (a relative size in the file is added to each side) and its corner radius; a relative Ellipse, or a setting that isn't read, keeps the drawn look. The page's drawing of such a style isn't held to look like VectorCraft's where its drawn look lay. Gradients take their matrix (`Xm`, radials and skewed ones too), focal point and the order of stops sharing a ramp point; gradient meshes, a clipping path's own fill and its stroke, and the artboards' names are read too. `editingData: "always"` (`convert --editing-data`) opens a `.ai` or an Illustrator EPS from its editing data even where it draws differently from its page, which can be older, with a note. Type is made from the file's text document, whole and editable, shown or not: point type (its frame's matrix, which newer files leave out when it is neither turned nor scaled, read as none), area type (a story in several frames as threaded type) and type on a path, with its characters, fonts, size, leading, tracking, scaling, baseline shift, fill and stroke (grey, RGB or CMYK), alignment, indents and paragraph spacing (a character attribute a run doesn't set comes from its character style, then the style that one is based on, then the document's defaults); area type keeps its frame's Area Type Options rows and columns, the gutter between rows and the gutter between columns, and its first baseline (Fixed, Cap Height, Leading or x Height) with its minimum; type on a path keeps its frame's Type on a Path effect, Align to Path and Flip, and the first frame's Spacing is fitted from the file's layout, not read from the dialog's value; the page's drawing of it (often in pieces) is left out. Vertical type keeps its orientation, its columns from right to left, and a run set to Tate-chu-yoko keeps that setting. When it makes the type of every text object that shows, the page's type is only compared with it, each text object keeps its own group, frame and layer, and a line break inside a paragraph stays one. Where it can't make a text object's type (a frame of another kind), that text object takes the page's type where the text document puts its lines, or else what is left of the page's type in painting order (no group around it); the page's type where made type draws stays that type's, and hidden type is left out with a note. Text wrap is read too: an object whose dictionary has the wrap options (offset and Invert Wrap), or any graphic of a legacy wraparound group (`*w` … `*W`, before or after its type, with no offset), is a wrap object, and area type below it in its layer flows around it when the file opens, threaded type included (#2763). Kerning, underline and the other character and paragraph options aren't read yet. A `.ai` saved without PDF compatibility (a placeholder page) opens from its editing data alone, its point type, area type and type on a path made from the text document. A `.ai` whose PDF part shows only text isn't taken for that placeholder when its page states an art, trim or bleed box: it opens from its editing data alone where that reads, else from its PDF part (#1387). A `.ai` whose PDF part draws nothing at all (an empty page, with or without those boxes) opens from its editing data alone where that reads too: there is no page art to check its layers against, and the empty page would lose all of it. Text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line (the frame is made taller, with a warning in the open result) (#1392). Symbols are read into the Symbols panel, in the file's symbol list order, with their art, their registration point and the instances in their art, and their instances come in live with their matrix, name, hide, lock, opacity, blending mode, isolation, knockout and opacity mask; a symbol whose art isn't read whole (type, placed files, an image it leaves out or anything else it doesn't read) isn't kept. Symbols are read only with the layers: a file that comes in as its page or PDF part (editing data with no layers, layers that can't be read, or any of the cases below) opens with an empty Symbols panel. Instances of symbols it can't read, instances with appearance changes of their own (a dynamic symbol's), pattern fills whose pattern it can't read, placed files the data keeps no preview of and anything else it doesn't read make the file come in as its page (on a layer that doesn't show, they are left out of that layer), with a warning saying what. An unread object on a shown layer whose box can be found instead comes in as a group of the page's objects inside that box, named after it, with the rest of the layers and groups kept and a warning listing what came in as the page's art; the file still comes in as its page when live art or another unread object shares that page art, when the object spans two artboards or lies on an artboard that has no page, when no page art lies inside its box, when that page art is inside a page group with a clip, mask, transparency or appearance, when the object holds a frame of threaded type, when the page art uses a pattern, image or swatch that the page lacks or that differs from the editing data's, or when the page art doesn't carry its transparency (#4806). The layers' art is also compared with the page (by lightness, so colours written as their CMYK equivalents compare alike, and by whether a pixel is inked at all); the page is used when they differ by more than 5% (away from type, when all the type is made from the text document), where one draws an object the other doesn't (away from type, whose lines may be laid out afresh), and when an EPS whose page is its art's box has art printed outside it (#735). A `.ai` is not compared with its page, and its PDF part is not imported, when its text document makes the type of every text object (at least one shows, and none is drawn by the appearance of the layer or group it is in), no object is unread, no placed file has a second placement, no live envelope keeps its drawn art, it declares no large canvas, and its artboards are its pages' trim boxes one for one, in points or in the first page's `UserUnit`: it opens from its layers, with the bleed of its pages' bleed boxes. When they differ because some objects draw differently (an object in another color, or one that only one of them draws), each top-level object of the layers that holds such a difference comes in as a group of the page's objects inside its box and the layers stay, with a warning that names the layer or sublayer of each such object as the Layers panel shows it (`` for one without a name) and how many came in from it; an export or Save As to the file's own path then needs `acknowledgeLoss: true`, and a wrap object replaced this way keeps its wrap options. The file still comes in as its page when only edges or type differ, when a difference lies outside every top-level object, when more than half of the top-level objects or more than 64 hold one, when such an object is or holds type or holds a wrap object inside a group, when the page's type of a text object that stays lies inside such an object's box, when the page's art can't replace such an object for one of the reasons above for an unread object, when matching the differences to the objects would take too much work, or when the result still differs from the page. `editingData: "always"` keeps the objects as read, and placed files at the other point their data gives are tried as well. An EPS whose page is its art's box keeps that page when it comes in through its layers, not the editing data's artboard, and a text object counts as drawn by the page by its colours, not only its lightness, so a red stroke stacked over a dark red one stays (#1102). `textAs: "outlines"` opens a `.ai` whose type shows from its PDF part, which has the outlines; `editingData: false` opens any of them as its page or PDF part only, for print pipelines (#735). A file whose import left something out (hidden text, art or layers) is never written over by an export or Save As to its own path unless `acknowledgeLoss: true` (`convert --replace-lossy`), so the original keeps what the document lacks. Where a `.ai` can't be read that way (or its layers are turned off in the import options), we read its PDF-compatible part, so Illustrator-only live objects arrive as appearance, and plain groups arrive ungrouped: that part draws each object on its own and marks only layers, clipping groups and groups with opacity, blending or a mask (those come back). Art outside the artboards isn't in that part at all (Illustrator's PDF part leaves it out), so it doesn't open; the import says so in a note (#472). Its pages still go where the editing data puts the artboards (a grid stays a grid), with the art on each, when it has as many artboards as pages, of the same sizes (#1068); the page of the artboard `CropAreaActive` names is then the active artboard. A `.ai` whose editing data has artboards but no layers also opens on that page under the same check, and its pages stay where they are. A `.ai` whose PDF part's pages are cropped to its art (each page the box of all the art, which is the editing data's bounding box) instead of its artboards opens from its layers, with its own artboards, where they draw as that page does. Its pages are laid out so that art one of them draws past its artboard (notes beside a design, which the part doesn't clip) doesn't lie over the page before it in the row, where it was taken for that page's art: the check against the editing data then found art its layers don't have there and opened the file as its PDF part, without the art outside the artboards. That part also has no paragraphs: area type is written line by line, and comes back as area type only where its font is installed and lays the lines out with the same breaks, else as a point type object per line (#508). Art off the page that a PDF does draw opens on the pasteboard. the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPrivateData` streams, a PDF's `AIPDFPrivateData` streams when it was saved with the editing data (#868), Zstandard or zlib, the EPS's in ASCII85) is not documented as a whole. VectorCraft reads its structure: layers and sublayers (names, order, visibility, lock, printing, preview, dimming, colour), groups as they were nested, compound paths (painted by their last path, or the last painted one, with the paths of groups inside them kept as their paths: #1410), clipping groups and layer clipping (several clipping paths in a clipping group, a layer or an opacity mask's art clip to their intersection, and a compound path of clipping paths stays one clip; clipping paths that would nest the art past the document's 32 levels stop the editing data from being read: #5789), object names, hidden and locked objects, fills and strokes (grey, CMYK, RGB, named and spot colours, spot colours defined in Lab with their Lab values (#1032), global process colours as global swatches at their tint; a stroke of width 0, `0 w`, on a path, a gradient-filled path, legacy type or in an art style comes in as a 0.25 pt hairline, as from PDF: #1671), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), gradient and pattern names in the file up to their first newline (a name that another swatch, or a gradient or pattern defined earlier, already has is numbered "Sky 2"; a name without a newline is kept, and when another swatch has it, the pattern's swatch, or a named color sharing a gradient or pattern swatch's name, is numbered the same way), opacity, blend modes, isolation and knockout, embedded images where their matrix puts them, rotated ones too (#2269), with their alpha channel (CMYK images keep their ink amounts, as they do when a `.ai` opens from its PDF part, and an alpha channel that hides part of a CMYK image becomes a grayscale opacity mask), placed (linked) files as linked images in their place, each showing the low-resolution preview the file keeps of it until its file is found or relinked in the Links panel (#1264), with the folder the file keeps relative to the document's, where the link is looked for when its path is gone, guides, and every artboard where it is, with its center mark, cross hairs, video safe areas and video ruler pixel aspect ratio. VectorCraft reads `CropAreaActive`, an `/Int` in the dictionary that holds the artboard array, as the index of the active artboard in that array, and the file opens with that artboard active; without it, or when it names no artboard that opens, the first artboard is active. It also reads the Document Raster Effects Settings (resolution, color model, background, anti-alias, clipping mask, Add Around Object and Preserve Spot Colors), the author from the header's `%%For` (an EPS's own header when the editing data's has none), swatches for the header's `%%CMYKCustomColor` and `%%RGBCustomColor` colors that the document has none of, and `%%TemplateBox` in older files; a setting with a value of another type or out of range keeps its default. Objects on Path groups open live, with their path, their objects in order along it, Pivot and Rotate; one with a dragged spacing or other options opens as a plain group. A gradient with stops in the middle runs its whole ramp across its shape, and a gradient object does not pass its fill to the next path (#1390). An object whose art style is one fill and one stroke at most (solid colours, its own gradient, a variable-width profile) with Drop Shadow, Inner Glow, Outer Glow, Feather, Round Corners, Scribble, Offset Path, Outline Object, Outline Stroke, Convert to Shape, Tweak (only when it moves nothing) or Pucker & Bloat at 0 comes in as itself with them and their saved settings. A path, compound path or group (not a clipping group) whose only effect is Transform (not with copies or Random) or a Free Distort whose corners make a parallelogram comes in as itself with it too, when neither it nor anything in it paints with a gradient or pattern and everything in it is a path, compound path or group with no opacity mask and no effects of its own; Transform that scales, rotates or reflects needs nothing in it to be stroked. Twist as the only effect is read the same way on an unstroked path or compound path whose segments are all straight. One with more, other effects (Zig Zag, a Pucker & Bloat of another amount, a Free Distort with other corners), an effect setting that is not read (an unknown key, a missing or out-of-range value, a Scribble with a color of its own), dashes aligned to corners, a brush other than a Calligraphic one, or a Calligraphic brush with settings of its own or an angle that follows the path comes in as its drawn look (a group named after it). Objects in a symbol defined before the file's art styles section, as newer files write it, take the same styles as the layers' objects. A path with a Calligraphic brush otherwise comes in as the path, the brush on its stroke at the stroke's weight, which scales the brush (#372). A Calligraphic brush's Pressure, Tilt, Bearing and Rotation come in as those channels (a 6D Art Pen's brushes vary with the pressure, the bearing and the barrel's rotation); its Stylus Wheel varies at random. The file's Calligraphic, Scatter, Art and Bristle brushes replace the default ones in the Brushes panel, in the file's order, also when the file comes in as its PDF part (a brush library has no layers); pattern brushes and brushes it can't read are left out with a note. Type keeps its art style as a live appearance whatever its stack, so it can be retyped: each fill and stroke of the style's stack (solid colours, gradients and pattern fills, placed as the drawn look places them) with its own opacity, blend mode and effects, the effects on the whole object, and where the characters paint among them; the effects read are Warp, Offset Path, Outline Object, Outline Stroke, Transform (with copies, Transform Objects, Transform Patterns and Scale Strokes & Effects; not with Random), Free Distort, Pucker & Bloat, Twist, Zig Zag, Tweak (only when it moves nothing, the file holding none of its random outline), Drop Shadow, Inner Glow, Outer Glow, Feather, Round Corners, Scribble, Gaussian Blur, Bas Relief (its settings at their defaults) and Convert to Shape, and a style with others, or with an effect setting that isn't read, keeps the type's drawn look. Convert to Shape keeps its Rectangle, Rounded Rectangle or Ellipse, its Absolute or Relative size (a relative size in the file is added to each side) and its corner radius; a relative Ellipse, or a setting that isn't read, keeps the drawn look. The page's drawing of such a style isn't held to look like VectorCraft's where its drawn look lay. Gradients take their matrix (`Xm`, radials and skewed ones too), focal point and the order of stops sharing a ramp point; gradient meshes, a clipping path's own fill and its stroke, and the artboards' names are read too. `editingData: "always"` (`convert --editing-data`) opens a `.ai` or an Illustrator EPS from its editing data even where it draws differently from its page, which can be older, with a note. Type is made from the file's text document, whole and editable, shown or not: point type (its frame's matrix, which newer files leave out when it is neither turned nor scaled, read as none), area type (a story in several frames as threaded type) and type on a path, with its characters, fonts, size, leading, tracking, scaling, baseline shift, fill and stroke (grey, RGB or CMYK), alignment, indents and paragraph spacing (a character attribute a run doesn't set comes from its character style, then the style that one is based on, then the document's defaults); area type keeps its frame's Area Type Options rows and columns, the gutter between rows and the gutter between columns, and its first baseline (Fixed, Cap Height, Leading or x Height) with its minimum; the page's drawing of it (often in pieces) is left out. Vertical type keeps its orientation, its columns from right to left, and a run set to Tate-chu-yoko keeps that setting. When it makes the type of every text object that shows, the page's type is only compared with it, each text object keeps its own group, frame and layer, and a line break inside a paragraph stays one. Where it can't make a text object's type (a frame of another kind), that text object takes the page's type where the text document puts its lines, or else what is left of the page's type in painting order (no group around it); the page's type where made type draws stays that type's, and hidden type is left out with a note. Text wrap is read too: an object whose dictionary has the wrap options (offset and Invert Wrap), or any graphic of a legacy wraparound group (`*w` … `*W`, before or after its type, with no offset), is a wrap object, and area type below it in its layer flows around it when the file opens, threaded type included (#2763). Kerning, underline and the other character and paragraph options aren't read yet. A `.ai` saved without PDF compatibility (a placeholder page) opens from its editing data alone, its point type, area type and type on a path made from the text document. A `.ai` whose PDF part shows only text isn't taken for that placeholder when its page states an art, trim or bleed box: it opens from its editing data alone where that reads, else from its PDF part (#1387). A `.ai` whose PDF part draws nothing at all (an empty page, with or without those boxes) opens from its editing data alone where that reads too: there is no page art to check its layers against, and the empty page would lose all of it. Text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line (the frame is made taller, with a warning in the open result) (#1392). Symbols are read into the Symbols panel, in the file's symbol list order, with their art, their registration point and the instances in their art, and their instances come in live with their matrix, name, hide, lock, opacity, blending mode, isolation, knockout and opacity mask; a symbol whose art isn't read whole (type, placed files, an image it leaves out or anything else it doesn't read) isn't kept. Symbols are read only with the layers: a file that comes in as its page or PDF part (editing data with no layers, layers that can't be read, or any of the cases below) opens with an empty Symbols panel. Instances of symbols it can't read, instances with appearance changes of their own (a dynamic symbol's), pattern fills whose pattern it can't read, placed files the data keeps no preview of and anything else it doesn't read make the file come in as its page (on a layer that doesn't show, they are left out of that layer), with a warning saying what. An unread object on a shown layer whose box can be found instead comes in as a group of the page's objects inside that box, named after it, with the rest of the layers and groups kept and a warning listing what came in as the page's art; the file still comes in as its page when live art or another unread object shares that page art, when the object spans two artboards or lies on an artboard that has no page, when no page art lies inside its box, when that page art is inside a page group with a clip, mask, transparency or appearance, when the object holds a frame of threaded type, when the page art uses a pattern, image or swatch that the page lacks or that differs from the editing data's, or when the page art doesn't carry its transparency (#4806). The layers' art is also compared with the page (by lightness, so colours written as their CMYK equivalents compare alike, and by whether a pixel is inked at all); the page is used when they differ by more than 5% (away from type, when all the type is made from the text document), where one draws an object the other doesn't (away from type, whose lines may be laid out afresh), and when an EPS whose page is its art's box has art printed outside it (#735). A `.ai` is not compared with its page, and its PDF part is not imported, when its text document makes the type of every text object (at least one shows, and none is drawn by the appearance of the layer or group it is in), no object is unread, no placed file has a second placement, no live envelope keeps its drawn art, it declares no large canvas, and its artboards are its pages' trim boxes one for one, in points or in the first page's `UserUnit`: it opens from its layers, with the bleed of its pages' bleed boxes. When they differ because some objects draw differently (an object in another color, or one that only one of them draws), each top-level object of the layers that holds such a difference comes in as a group of the page's objects inside its box and the layers stay, with a warning that names the layer or sublayer of each such object as the Layers panel shows it (`` for one without a name) and how many came in from it; an export or Save As to the file's own path then needs `acknowledgeLoss: true`, and a wrap object replaced this way keeps its wrap options. The file still comes in as its page when only edges or type differ, when a difference lies outside every top-level object, when more than half of the top-level objects or more than 64 hold one, when such an object is or holds type or holds a wrap object inside a group, when the page's type of a text object that stays lies inside such an object's box, when the page's art can't replace such an object for one of the reasons above for an unread object, when matching the differences to the objects would take too much work, or when the result still differs from the page. `editingData: "always"` keeps the objects as read, and placed files at the other point their data gives are tried as well. An EPS whose page is its art's box keeps that page when it comes in through its layers, not the editing data's artboard, and a text object counts as drawn by the page by its colours, not only its lightness, so a red stroke stacked over a dark red one stays (#1102). `textAs: "outlines"` opens a `.ai` whose type shows from its PDF part, which has the outlines; `editingData: false` opens any of them as its page or PDF part only, for print pipelines (#735). A file whose import left something out (hidden text, art or layers) is never written over by an export or Save As to its own path unless `acknowledgeLoss: true` (`convert --replace-lossy`), so the original keeps what the document lacks. Where a `.ai` can't be read that way (or its layers are turned off in the import options), we read its PDF-compatible part, so Illustrator-only live objects arrive as appearance, and plain groups arrive ungrouped: that part draws each object on its own and marks only layers, clipping groups and groups with opacity, blending or a mask (those come back). Art outside the artboards isn't in that part at all (Illustrator's PDF part leaves it out), so it doesn't open; the import says so in a note (#472). Its pages still go where the editing data puts the artboards (a grid stays a grid), with the art on each, when it has as many artboards as pages, of the same sizes (#1068); the page of the artboard `CropAreaActive` names is then the active artboard. A `.ai` whose editing data has artboards but no layers also opens on that page under the same check, and its pages stay where they are. A `.ai` whose PDF part's pages are cropped to its art (each page the box of all the art, which is the editing data's bounding box) instead of its artboards opens from its layers, with its own artboards, where they draw as that page does. Its pages are laid out so that art one of them draws past its artboard (notes beside a design, which the part doesn't clip) doesn't lie over the page before it in the row, where it was taken for that page's art: the check against the editing data then found art its layers don't have there and opened the file as its PDF part, without the art outside the artboards. That part also has no paragraphs: area type is written line by line, and comes back as area type only where its font is installed and lays the lines out with the same breaks, else as a point type object per line (#508). Art off the page that a PDF does draw opens on the pasteboard. the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPrivateData` streams, a PDF's `AIPDFPrivateData` streams when it was saved with the editing data (#868), Zstandard or zlib, the EPS's in ASCII85) is not documented as a whole. VectorCraft reads its structure: layers and sublayers (names, order, visibility, lock, printing, preview, dimming, colour), groups as they were nested, compound paths (painted by their last path, or the last painted one, with the paths of groups inside them kept as their paths: #1410), clipping groups and layer clipping (several clipping paths in a clipping group, a layer or an opacity mask's art clip to their intersection, and a compound path of clipping paths stays one clip; clipping paths that would nest the art past the document's 32 levels stop the editing data from being read: #5789), object names, hidden and locked objects, fills and strokes (grey, CMYK, RGB, named and spot colours, spot colours defined in Lab with their Lab values (#1032), global process colours as global swatches at their tint; a stroke of width 0, `0 w`, on a path, a gradient-filled path, legacy type or in an art style comes in as a 0.25 pt hairline, as from PDF: #1671), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), gradient and pattern names in the file up to their first newline (a name that another swatch, or a gradient or pattern defined earlier, already has is numbered "Sky 2"; a name without a newline is kept, and when another swatch has it, the pattern's swatch, or a named color sharing a gradient or pattern swatch's name, is numbered the same way), opacity, blend modes, isolation and knockout, embedded images where their matrix puts them, rotated ones too (#2269), with their alpha channel (CMYK images keep their ink amounts, as they do when a `.ai` opens from its PDF part, and an alpha channel that hides part of a CMYK image becomes a grayscale opacity mask), placed (linked) files as linked images in their place, each showing the low-resolution preview the file keeps of it until its file is found or relinked in the Links panel (#1264), with the folder the file keeps relative to the document's, where the link is looked for when its path is gone, guides, and every artboard where it is, with its center mark, cross hairs, video safe areas and video ruler pixel aspect ratio. VectorCraft reads `CropAreaActive`, an `/Int` in the dictionary that holds the artboard array, as the index of the active artboard in that array, and the file opens with that artboard active; without it, or when it names no artboard that opens, the first artboard is active. It also reads the Document Raster Effects Settings (resolution, color model, background, anti-alias, clipping mask, Add Around Object and Preserve Spot Colors), the author from the header's `%%For` (an EPS's own header when the editing data's has none), swatches for the header's `%%CMYKCustomColor` and `%%RGBCustomColor` colors that the document has none of, and `%%TemplateBox` in older files; a setting with a value of another type or out of range keeps its default. Objects on Path groups open live, with their path, their objects in order along it, Pivot and Rotate; one with a dragged spacing or other options opens as a plain group. A gradient with stops in the middle runs its whole ramp across its shape, and a gradient object does not pass its fill to the next path (#1390). An object whose art style is one fill and one stroke at most (solid colours, its own gradient, a variable-width profile) with Drop Shadow, Inner Glow, Outer Glow, Feather, Round Corners, Scribble, Offset Path, Outline Object, Outline Stroke, Convert to Shape, Tweak (only when it moves nothing) or Pucker & Bloat at 0 comes in as itself with them and their saved settings. A path, compound path or group (not a clipping group) whose only effect is Transform (not with copies or Random) or a Free Distort whose corners make a parallelogram comes in as itself with it too, when neither it nor anything in it paints with a gradient or pattern and everything in it is a path, compound path or group with no opacity mask and no effects of its own; Transform that scales, rotates or reflects needs nothing in it to be stroked. Twist as the only effect is read the same way on an unstroked path or compound path whose segments are all straight. One with more, other effects (Zig Zag, a Pucker & Bloat of another amount, a Free Distort with other corners), an effect setting that is not read (an unknown key, a missing or out-of-range value, a Scribble with a color of its own), dashes aligned to corners, a brush other than a Calligraphic or Art one, a Calligraphic brush with settings of its own or an angle that follows the path, or an Art brush other than the ones described next comes in as its drawn look (a group named after it). A path with a Calligraphic brush otherwise comes in as the path, the brush on its stroke at the stroke's weight, which scales the brush (#372), and so does a path with an Art brush, whose weight scales the art across the path, when all of these hold: the brush stretches its art along the path, unflipped, at 100% in Tints; the path's settings are the brush's own, with no entry that isn't read, no variable width and no effects; its stroke isn't a swatch; and the art is paths, compound paths and groups in solid fills, with no strokes, masks or effects, and has some length along the path. A stroke takes the brush the Brushes panel keeps under its name, and one whose name the panel doesn't keep comes in as its drawn look. Type with an Art brush keeps its drawn look under the type. A Calligraphic brush's Pressure, Tilt, Bearing and Rotation come in as those channels (a 6D Art Pen's brushes vary with the pressure, the bearing and the barrel's rotation); its Stylus Wheel varies at random. The file's Calligraphic, Scatter, Art and Bristle brushes replace the default ones in the Brushes panel, in the file's order, also when the file comes in as its PDF part (a brush library has no layers); pattern brushes and brushes it can't read are left out with a note. Type keeps its art style as a live appearance whatever its stack (an Art brush aside), so it can be retyped: each fill and stroke of the style's stack (solid colours, gradients and pattern fills, placed as the drawn look places them) with its own opacity, blend mode and effects, the effects on the whole object, and where the characters paint among them; the effects read are Warp, Offset Path, Outline Object, Outline Stroke, Transform (with copies, Transform Objects, Transform Patterns and Scale Strokes & Effects; not with Random), Free Distort, Pucker & Bloat, Twist, Zig Zag, Tweak (only when it moves nothing, the file holding none of its random outline), Drop Shadow, Inner Glow, Outer Glow, Feather, Round Corners, Scribble, Gaussian Blur, Bas Relief (its settings at their defaults) and Convert to Shape, and a style with others, or with an effect setting that isn't read, keeps the type's drawn look. Convert to Shape keeps its Rectangle, Rounded Rectangle or Ellipse, its Absolute or Relative size (a relative size in the file is added to each side) and its corner radius; a relative Ellipse, or a setting that isn't read, keeps the drawn look. The page's drawing of such a style isn't held to look like VectorCraft's where its drawn look lay. Gradients take their matrix (`Xm`, radials and skewed ones too), focal point and the order of stops sharing a ramp point; gradient meshes, a clipping path's own fill and its stroke, and the artboards' names are read too. `editingData: "always"` (`convert --editing-data`) opens a `.ai` or an Illustrator EPS from its editing data even where it draws differently from its page, which can be older, with a note. Type is made from the file's text document, whole and editable, shown or not: point type (its frame's matrix, which newer files leave out when it is neither turned nor scaled, read as none), area type (a story in several frames as threaded type) and type on a path, with its characters, fonts, size, leading, tracking, scaling, baseline shift, fill and stroke (grey, RGB or CMYK), alignment, indents and paragraph spacing (a character attribute a run doesn't set comes from its character style, then the style that one is based on, then the document's defaults); area type keeps its frame's Area Type Options rows and columns, the gutter between rows and the gutter between columns, and its first baseline (Fixed, Cap Height, Leading or x Height) with its minimum; the page's drawing of it (often in pieces) is left out. Vertical type keeps its orientation, its columns from right to left, and a run set to Tate-chu-yoko keeps that setting. When it makes the type of every text object that shows, the page's type is only compared with it, each text object keeps its own group, frame and layer, and a line break inside a paragraph stays one. Where it can't make a text object's type (a frame of another kind), that text object takes the page's type where the text document puts its lines, or else what is left of the page's type in painting order (no group around it); the page's type where made type draws stays that type's, and hidden type is left out with a note. Text wrap is read too: an object whose dictionary has the wrap options (offset and Invert Wrap), or any graphic of a legacy wraparound group (`*w` … `*W`, before or after its type, with no offset), is a wrap object, and area type below it in its layer flows around it when the file opens, threaded type included (#2763). Kerning, underline and the other character and paragraph options aren't read yet. A `.ai` saved without PDF compatibility (a placeholder page) opens from its editing data alone, its point type, area type and type on a path made from the text document. A `.ai` whose PDF part shows only text isn't taken for that placeholder when its page states an art, trim or bleed box: it opens from its editing data alone where that reads, else from its PDF part (#1387). A `.ai` whose PDF part draws nothing at all (an empty page, with or without those boxes) opens from its editing data alone where that reads too: there is no page art to check its layers against, and the empty page would lose all of it. Text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line (the frame is made taller, with a warning in the open result) (#1392). Symbols are read into the Symbols panel, in the file's symbol list order, with their art, their registration point and the instances in their art, and their instances come in live with their matrix, name, hide, lock, opacity, blending mode, isolation, knockout and opacity mask; a symbol whose art isn't read whole (type, placed files, an image it leaves out or anything else it doesn't read) isn't kept. Symbols are read only with the layers: a file that comes in as its page or PDF part (editing data with no layers, layers that can't be read, or any of the cases below) opens with an empty Symbols panel. Instances of symbols it can't read, instances with appearance changes of their own (a dynamic symbol's), pattern fills whose pattern it can't read, placed files the data keeps no preview of and anything else it doesn't read make the file come in as its page (on a layer that doesn't show, they are left out of that layer), with a warning saying what. An unread object on a shown layer whose box can be found instead comes in as a group of the page's objects inside that box, named after it, with the rest of the layers and groups kept and a warning listing what came in as the page's art; the file still comes in as its page when live art or another unread object shares that page art, when the object spans two artboards or lies on an artboard that has no page, when no page art lies inside its box, when that page art is inside a page group with a clip, mask, transparency or appearance, when the object holds a frame of threaded type, when the page art uses a pattern, image or swatch that the page lacks or that differs from the editing data's, or when the page art doesn't carry its transparency (#4806). The layers' art is also compared with the page (by lightness, so colours written as their CMYK equivalents compare alike, and by whether a pixel is inked at all); the page is used when they differ by more than 5% (away from type, when all the type is made from the text document), where one draws an object the other doesn't (away from type, whose lines may be laid out afresh), and when an EPS whose page is its art's box has art printed outside it (#735). A `.ai` is not compared with its page, and its PDF part is not imported, when its text document makes the type of every text object (at least one shows, and none is drawn by the appearance of the layer or group it is in), no object is unread, no placed file has a second placement, no live envelope keeps its drawn art, it declares no large canvas, and its artboards are its pages' trim boxes one for one, in points or in the first page's `UserUnit`: it opens from its layers, with the bleed of its pages' bleed boxes. When they differ because some objects draw differently (an object in another color, or one that only one of them draws), each top-level object of the layers that holds such a difference comes in as a group of the page's objects inside its box and the layers stay, with a warning that names the layer or sublayer of each such object as the Layers panel shows it (`` for one without a name) and how many came in from it; an export or Save As to the file's own path then needs `acknowledgeLoss: true`, and a wrap object replaced this way keeps its wrap options. The file still comes in as its page when only edges or type differ, when a difference lies outside every top-level object, when more than half of the top-level objects or more than 64 hold one, when such an object is or holds type or holds a wrap object inside a group, when the page's type of a text object that stays lies inside such an object's box, when the page's art can't replace such an object for one of the reasons above for an unread object, when matching the differences to the objects would take too much work, or when the result still differs from the page. `editingData: "always"` keeps the objects as read, and placed files at the other point their data gives are tried as well. An EPS whose page is its art's box keeps that page when it comes in through its layers, not the editing data's artboard, and a text object counts as drawn by the page by its colours, not only its lightness, so a red stroke stacked over a dark red one stays (#1102). `textAs: "outlines"` opens a `.ai` whose type shows from its PDF part, which has the outlines; `editingData: false` opens any of them as its page or PDF part only, for print pipelines (#735). A file whose import left something out (hidden text, art or layers) is never written over by an export or Save As to its own path unless `acknowledgeLoss: true` (`convert --replace-lossy`), so the original keeps what the document lacks. Where a `.ai` can't be read that way (or its layers are turned off in the import options), we read its PDF-compatible part, so Illustrator-only live objects arrive as appearance, and plain groups arrive ungrouped: that part draws each object on its own and marks only layers, clipping groups and groups with opacity, blending or a mask (those come back). Art outside the artboards isn't in that part at all (Illustrator's PDF part leaves it out), so it doesn't open; the import says so in a note (#472). Its pages still go where the editing data puts the artboards (a grid stays a grid), with the art on each, when it has as many artboards as pages, of the same sizes (#1068); the page of the artboard `CropAreaActive` names is then the active artboard. A `.ai` whose editing data has artboards but no layers also opens on that page under the same check, and its pages stay where they are. A `.ai` whose PDF part's pages are cropped to its art (each page the box of all the art, which is the editing data's bounding box) instead of its artboards opens from its layers, with its own artboards, where they draw as that page does. Its pages are laid out so that art one of them draws past its artboard (notes beside a design, which the part doesn't clip) doesn't lie over the page before it in the row, where it was taken for that page's art: the check against the editing data then found art its layers don't have there and opened the file as its PDF part, without the art outside the artboards. That part also has no paragraphs: area type is written line by line, and comes back as area type only where its font is installed and lays the lines out with the same breaks, else as a point type object per line (#508). Art off the page that a PDF does draw opens on the pasteboard.