From b664216a2abebb4fa612f03456953497e3439841 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 2 Nov 2026 16:19:59 +0700 Subject: [PATCH] .ai import: type keeps its Distort & Transform effects live Squashed from #5139 onto main. --- crates/effects/src/distort.rs | 6 +- crates/effects/src/tests.rs | 12 ++ crates/engine/tests/import_fuzz.rs | 78 ++++++++ crates/eps/src/import/ai/style.rs | 238 +++++++++++++++++++++--- crates/eps/src/import/ai/style/tests.rs | 220 ++++++++++++++++++++++ crates/eps/src/import/tests_native.rs | 65 +++++++ docs/file-format-parity.md | 2 +- 7 files changed, 597 insertions(+), 24 deletions(-) diff --git a/crates/effects/src/distort.rs b/crates/effects/src/distort.rs index 5b0c14c62..1b496a9c5 100644 --- a/crates/effects/src/distort.rs +++ b/crates/effects/src/distort.rs @@ -298,7 +298,8 @@ pub fn tweak(path: &PathData, b: Rect, p: &Value) -> PathData { } /// Twist: rotation that is strongest at the centre and fades to zero at the bounding circle, -/// making at most `max` anchors ([`map_nonlinear_within`]). +/// clockwise on the page for a positive `angle`, making at most `max` anchors +/// ([`map_nonlinear_within`]). pub fn twist(path: &PathData, b: Rect, angle: f64, max: usize) -> PathData { let ang = angle.clamp(-3600.0, 3600.0).to_radians(); if ang == 0.0 { @@ -309,7 +310,8 @@ pub fn twist(path: &PathData, b: Rect, angle: f64, max: usize) -> PathData { let f = move |q: Point| { let d = q - c; let t = (1.0 - d.hypot() / r).max(0.0); - let a = -ang * t; + // y runs down, so a positive turn is clockwise on the page. + let a = ang * t; let (s, co) = a.sin_cos(); c + Vec2::new(d.x * co - d.y * s, d.x * s + d.y * co) }; diff --git a/crates/effects/src/tests.rs b/crates/effects/src/tests.rs index bc309ce07..611909b96 100644 --- a/crates/effects/src/tests.rs +++ b/crates/effects/src/tests.rs @@ -942,6 +942,18 @@ fn round_corners_keeps_where_a_closed_path_starts() { assert_eq!(stylize::round_corners(&d, f64::INFINITY), stylize::round_corners(&d, 1e9)); } +#[test] +fn twist_turns_clockwise_for_positive_angles() { + // A positive angle turns clockwise, a negative one counterclockwise + // (https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html). + // The document's y runs down, so clockwise on the page takes a point right of the center down. + let right = PathData::single(SubPath::polyline(&[Point::new(55.0, 50.0), Point::new(56.0, 50.0)], false)); + let bounds = Rect::new(0.0, 0.0, 100.0, 100.0); + let turned = |angle: f64| apply_geometry(&[fx("distort.twist", json!({"angle": angle}))], &right, bounds).subpaths[0].anchors[0].p; + assert!(turned(45.0).y > 50.5, "{:?}", turned(45.0)); + assert!(turned(-45.0).y < 49.5, "{:?}", turned(-45.0)); +} + #[test] fn twist_fixes_the_outer_circle_and_rotates_inside() { let out = run("distort.twist", json!({"angle": 0}), &square()); diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index 1b001cf87..bd9069bd2 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -3906,6 +3906,29 @@ fn arb_ai_token() -> impl Strategy { "(Fill Style Filter) 0 0 /Filter ,", "(Adobe Outer Glow) 1 0 /Filter ,", "(Adobe Drop Shadow) 1 0 /Filter ,", + "(Adobe Transform) 1 0 /Filter ,", + "(Adobe Free Distort) 1 0 /Filter ,", + "(Adobe Punk and Bloat) 1 0 /Filter ,", + "(Adobe Roughen) 1 0 /Filter ,", + "(Adobe Scribble and Tweak) 1 0 /Filter ,", + "(Adobe Twirl) 1 0 /Filter ,", + "(Adobe Zigzag) 1 0 /Filter ,", + "1e308 /Real (scaleH_Factor) ,", + "-1e308 /Real (rotate_Radians) ,", + "1000 /Int (numCopies) ,", + "8 /Int (pinPoint) ,", + "0 /Bool (scaleLines) ,", + "0 /Bool (transformPatterns) ,", + "0 /Real (src0h) ,", + "1e-300 /Real (src1h) ,", + "-1 /Real (src2v) ,", + "1e308 /Real (dst3v) ,", + "-200 /Real (d_factor) ,", + "3600 /Real (angle) ,", + "1 /Real (absoluteness) ,", + "100 /Real (ridges) ,", + "1e4 /Real (amount) ,", + "0 /Bool (anch) ,", "(Adobe Inner Glow) 1 0 /Filter ,", "(Adobe Fuzzy Mask) 1 0 /Filter ,", "(Adobe Round Corners) 1 0 /Filter ,", @@ -4146,6 +4169,61 @@ proptest! { survive("Path effect art style in an EPS", || vectorcraft_engine::cmd::fileio::load("x.eps", &eps).ok().map(|l| l.doc))?; } + /// Art styles with a Distort & Transform effect (Transform, Free Distort, Pucker & Bloat, Roughen, + /// Tweak, Twist, Zig Zag) whose dictionary holds any of its entries, and unknown ones, as any kind, + /// with any value (huge, tiny, not a number, a string), repeated or left out, on a path and on type, + /// on the whole object or on its fill. + #[test] + fn ai_distort_effects_never_panic( + name in prop::sample::select(vec![ + "Adobe Transform", + "Adobe Free Distort", + "Adobe Punk and Bloat", + "Adobe Roughen", + "Adobe Scribble and Tweak", + "Adobe Twirl", + "Adobe Zigzag", + ]), + entries in prop::collection::vec(( + prop::sample::select(vec![ + "scaleH_Factor", "scaleV_Factor", "scaleH_Percent", "rotate_Radians", "rotate_Degrees", "moveH_Pts", "moveV_Pts", + "numCopies", "pinPoint", "reflectX", "reflectY", "randomize", "transformObjects", "transformPatterns", "scaleLines", + "src0h", "src0v", "src1h", "src1v", "src2h", "src2v", "src3h", "src3v", "dst0h", "dst0v", "dst1h", "dst1v", "dst2h", + "dst2v", "dst3h", "dst3v", "d_factor", "absoluteness", "size", "asiz", "dtal", "roundness", "horz", "vert", "ahor", + "aver", "anch", "in", "out", "angle", "amount", "relAmount", "ridges", "futureKey", + ]), + prop::sample::select(vec!["Int", "Real", "Bool"]), + prop_oneof![ + (-1e4f64..1e4).prop_map(|v| v.to_string()), + prop::sample::select(vec!["0", "1", "-1", "0.5", "1e-300", "1e308", "-1e308", "1e999", "NaN", "(x)", ""]).prop_map(str::to_string), + ], + ), 0..24), + on_fill in any::(), + ) { + use vectorcraft_testkit::ai; + let keys: String = entries.iter().map(|(k, kind, v)| format!("{v} /{kind} ({k}) ,\n")).collect(); + let basic = |name: &str, body: &str| format!("/BasicFilter :\n({name}) 0 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n{body}; /Dict ;\n /Part ,\n"); + let compound = |name: &str, kids: &[String]| format!("/CompoundFilter :\n({name}) 0 0 /Filter ,\n1 /Visible ,\n{};\n /Part ,\n", kids.concat()); + let fill = basic("Fill Style Filter", "/FillStyle : 0 O\n0 0 0 0 0 0 1 Xa\n0 1 Xd\n/Def ;\n (FillStyle) ,\n"); + let effect = basic(name, &keys); + let root = if on_fill { + compound("Chain Style Filter", &[compound("Stack Style Filter", &[compound("Chain Style Filter", &[fill, effect]), basic("Conduit Filter", "")])]) + } else { + compound("Chain Style Filter", &[compound("Stack Style Filter", &[fill, basic("Conduit Filter", "")]), effect]) + }; + let styles = format!("%AI9_BeginArtStyles\n/KnownStyle :\n(Fx) /Name ,\n/ActiveStyle :\n{root}%AI9_EndArtStyles\n"); + let hidden = ai::rect(20.0, 20.0, 80.0, 60.0).replace('\n', "\n%_"); + let art = format!( + "0 Ae\nu\n0 0 1 0 k\n{}f\nU\n%_{hidden}f\n1 (Fx) XW\nu\n/AI11Text :\n1 /FreeUndo ,\n;\n0 0 1 0 k\n{}f\nU\n1 (Fx) XW\n", + ai::rect(20.0, 20.0, 80.0, 60.0), + ai::rect(100.0, 20.0, 180.0, 60.0) + ); + let data = ai::editing_data(200.0, 100.0, &format!("{styles}{}", ai::layer("L", &art))); + survive("distort art style", || Some(vectorcraft_eps::layered_ai(data.as_bytes(), Document::new(200.0, 100.0), vec![], false, false, 1.0).0))?; + let eps = ai::eps(data.as_bytes(), ai::page_ps()); + survive("distort art style in an EPS", || vectorcraft_engine::cmd::fileio::load("x.eps", &eps).ok().map(|l| l.doc))?; + } + /// Layer options (`Lb`) of any number of operands with any values, and guide paths (`(N) *`, /// `(S) *`) of any points, among them ones across the whole canvas or from edge to edge of /// artboards that overlap or are the same, on layers and sublayers. diff --git a/crates/eps/src/import/ai/style.rs b/crates/eps/src/import/ai/style.rs index a41b0a22a..043714518 100644 --- a/crates/eps/src/import/ai/style.rs +++ b/crates/eps/src/import/ai/style.rs @@ -60,6 +60,11 @@ const OFFSET: &str = "Adobe Offset Path"; // brand-ok: the filter's name in the const OUTLINE_TYPE: &str = "Adobe Outline Type"; // brand-ok: the filter's name in the files const OUTLINE_STROKE: &str = "Adobe Outline Stroke"; // brand-ok: the filter's name in the files const TRANSFORM: &str = "Adobe Transform"; // brand-ok: the filter's name in the files +const FREE_DISTORT: &str = "Adobe Free Distort"; // brand-ok: the filter's name in the files +const PUCKER_BLOAT: &str = "Adobe Punk and Bloat"; // brand-ok: the filter's name in the files +const TWEAK: &str = "Adobe Scribble and Tweak"; // brand-ok: the filter's name in the files +const TWIST: &str = "Adobe Twirl"; // brand-ok: the filter's name in the files +const ZIG_ZAG: &str = "Adobe Zigzag"; // brand-ok: the filter's name in the files const GAUSSIAN: &str = "Adobe PSL Gaussian Blur"; // brand-ok: the filter's name in the files const BAS_RELIEF: &str = "PSAdapter_plugin_BsRl"; const SHAPE_EFFECTS: &str = "Adobe Shape Effects"; // brand-ok: the filter's name in the files @@ -546,6 +551,186 @@ fn width_profile(arrays: &HashMap>) -> Option { (!uniform).then_some(WidthProfile { points }) } +/// Entry `key` of `f`'s dictionary, written as `kind`: `Ok(None)` when the dictionary doesn't hold +/// it, `Err` when it holds it as another kind, without a finite number, or as an `/Int` that isn't +/// whole or a `/Bool` other than 0 and 1. +fn entry(f: &Filter, key: &str, kind: Kind) -> Result, ()> { + let Some(k) = f.kinds.get(key) else { return Ok(None) }; + match f.params.get(key) { + Some(v) + if *k == kind + && v.is_finite() + && match kind { + Kind::Real => true, + Kind::Int => v.fract() == 0.0, + Kind::Bool => *v == 0.0 || *v == 1.0, + Kind::Fill | Kind::Other => false, + } => + { + Ok(Some(*v)) + } + _ => Err(()), + } +} + +/// Entry `key` as a switch, 0 or 1 written as `kind`: `Ok(None)` when it is missing. +fn switch(f: &Filter, key: &str, kind: Kind) -> Result, ()> { + match entry(f, key, kind)? { + None => Ok(None), + Some(0.0) => Ok(Some(false)), + Some(1.0) => Ok(Some(true)), + Some(_) => Err(()), + } +} + +/// Real entry `key` within `range`, `missing` when the dictionary doesn't hold it. +fn real(f: &Filter, key: &str, missing: f64, range: std::ops::RangeInclusive) -> Option { + entry(f, key, Kind::Real).ok()?.map_or(Some(missing), |v| range.contains(&v).then_some(v)) +} + +/// What an effect in a style could change the paint of, for the Transform options whose value, +/// when the dictionary leaves it out, comes from the app's settings rather than the file. +#[derive(Clone, Copy, Debug)] +struct Under { + /// A stroke, or other effects with distances that Scale Strokes & Effects would scale. + strokes: bool, + /// A pattern. + patterns: bool, +} + +/// Anything: an effect on the whole object, whose contents can paint in any way. +const ANYTHING: Under = Under { strokes: true, patterns: true }; + +/// The Transform effect `f` (the factor and radian entries; the percent and degree ones are what +/// its dialog shows). `None` keeps the drawn look: an entry not read, Random (the file holds no +/// seed), or a missing Scale Strokes & Effects or Transform Patterns that would change `under`. +fn transform(f: &Filter, under: Under) -> Option { + for key in ["scaleH_Percent", "scaleV_Percent", "rotate_Degrees"] { + entry(f, key, Kind::Real).ok()?; + } + // The renderer's limits on scales and moves (`vectorcraft_effects`' `MAX_SCALE` and `MAX_MOVE`). + let scale = |key: &str| real(f, key, 1.0, -1000.0..=1000.0).map(|v| v * 100.0); + let (h, v) = (scale("scaleH_Factor")?, scale("scaleV_Factor")?); + let (move_h, move_v) = (real(f, "moveH_Pts", 0.0, -1e7..=1e7)?, real(f, "moveV_Pts", 0.0, -1e7..=1e7)?); + let rotate = Some(entry(f, "rotate_Radians", Kind::Real).ok()?.unwrap_or(0.0).to_degrees()).filter(|d| d.is_finite())?; + let copies = entry(f, "numCopies", Kind::Int).ok()?.unwrap_or(0.0); + let reference = entry(f, "pinPoint", Kind::Int).ok()?.unwrap_or(4.0); + if !(0.0..=1000.0).contains(&copies) || !(0.0..=8.0).contains(&reference) { + return None; + } + let on = |key: &str| switch(f, key, Kind::Bool); + let (reflect_x, reflect_y) = (on("reflectX").ok()?.unwrap_or(false), on("reflectY").ok()?.unwrap_or(false)); + let objects = on("transformObjects").ok()?.unwrap_or(true); + let (strokes, patterns) = (on("scaleLines").ok()?, on("transformPatterns").ok()?); + if on("randomize").ok()?.unwrap_or(false) { + return None; + } + let moves = h != 100.0 || v != 100.0 || move_h != 0.0 || move_v != 0.0 || rotate != 0.0 || reflect_x || reflect_y; + let scales = ((h * v).abs().sqrt() - 100.0).abs() > 1e-9; + if (strokes.is_none() && scales && under.strokes) || (patterns.is_none() && moves && under.patterns) { + return None; + } + // Art space has y up: a move up is a negative `moveV` in the document. + Some(json!({ + "scaleH": h, + "scaleV": v, + "moveH": move_h, + "moveV": 0.0 - move_v, + "rotate": rotate, + "copies": copies as i64, + "reflectX": reflect_x, + "reflectY": reflect_y, + "reference": reference as i64, + "objects": objects, + "patterns": patterns.unwrap_or(false), + "strokes": strokes.unwrap_or(false), + })) +} + +/// Free Distort's corners: the destination corners `dst0h`… measured in the source frame `src0h`… +/// (top left, top right, bottom left, bottom right, y up), as the catalog's unit-box corners (top +/// left, top right, bottom right, bottom left, y down). The source frame is only the unit the +/// corners are measured in: the distorted box is laid over the object's own bounds, as the catalog +/// effect lays it, also when the frame is another size (a unit square, or bounds with the stroke). +/// No corners is no change; some but not all, or a frame that isn't an upright rectangle, isn't read. +fn free_distort(f: &Filter) -> Option { + let mut corners = [[[0.0; 2]; 4]; 2]; + let mut held = 0; + for (which, c) in ["src", "dst"].into_iter().zip(&mut corners) { + for (i, corner) in c.iter_mut().enumerate() { + for (axis, v) in ["h", "v"].into_iter().zip(corner.iter_mut()) { + if let Some(x) = entry(f, &format!("{which}{i}{axis}"), Kind::Real).ok()? { + *v = x; + held += 1; + } + } + } + } + if held == 0 { + return Some(json!({"corners": [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]})); + } + let [[tl, tr, bl, br], dst] = corners; + let (left, top) = (tl[0], tl[1]); + let (width, height) = (tr[0] - left, top - bl[1]); + let tolerance = 1e-5 * width.abs().max(height.abs()); + let upright = (tr[1] - top).abs() <= tolerance + && (br[1] - bl[1]).abs() <= tolerance + && (bl[0] - left).abs() <= tolerance + && (br[0] - tr[0]).abs() <= tolerance; + let extent = |d: f64| d.is_finite() && d > 0.0; + if held < 16 || !upright || !extent(width) || !extent(height) { + return None; + } + let mut out = vec![]; + for i in [0, 1, 3, 2] { + let [h, v] = *dst.get(i)?; + let (x, y) = ((h - left) / width, (top - v) / height); + if !(x.is_finite() && y.is_finite() && x.abs() <= 1e4 && y.abs() <= 1e4) { + return None; + } + out.push([x, y]); + } + Some(json!({ "corners": out })) +} + +/// A `0` relative, `1` absolute switch (`absoluteness`), `absolute` when it is missing, and the size +/// it picks: `relative_key` in percent or `absolute_key` in points, 0 when missing. +fn sized(f: &Filter, absolute: bool, relative_key: &str, absolute_key: &str) -> Option<(bool, f64)> { + let absolute = switch(f, "absoluteness", Kind::Real).ok()?.unwrap_or(absolute); + let relative = real(f, relative_key, 0.0, 0.0..=1e4)?; + let points = real(f, absolute_key, 0.0, 0.0..=1e4)?; + Some((absolute, if absolute { points } else { relative })) +} + +/// Corner (`roundness` 0, or missing) or Smooth (1). +fn points(f: &Filter) -> Option<&'static str> { + Some(if switch(f, "roundness", Kind::Real).ok()?.unwrap_or(false) { "smooth" } else { "corner" }) +} + +/// The Tweak effect `f` (`Adobe Scribble and Tweak`): relative unless `absoluteness` says otherwise, +/// moving anchors and both handles unless the dictionary turns them off. +fn tweak(f: &Filter) -> Option { + let (absolute, h) = sized(f, false, "horz", "ahor")?; + let (_, v) = sized(f, false, "vert", "aver")?; + let on = |key: &str| switch(f, key, Kind::Bool).map(|v| v.unwrap_or(true)); + let (anchors, ins, outs) = (on("anch").ok()?, on("in").ok()?, on("out").ok()?); + Some(json!({"h": h, "v": v, "relative": !absolute, "anchors": anchors, "in": ins, "out": outs})) +} + +/// The Zig Zag effect `f`: absolute unless `absoluteness` says otherwise, 4 ridges and Corner points +/// when they are missing. +fn zig_zag(f: &Filter) -> Option { + let (absolute, size) = sized(f, true, "relAmount", "amount")?; + let ridges = real(f, "ridges", 4.0, 0.0..=100.0).filter(|r| r.fract() == 0.0)?; + Some(json!({"size": size, "relative": !absolute, "ridges": ridges as i64, "points": points(f)?})) +} + +/// Whether Tweak params `p` leave the outline as it is: its random outline isn't in the file, so any +/// other keeps the drawn look. +fn still(p: &serde_json::Value) -> bool { + (p["h"].as_f64() == Some(0.0) && p["v"].as_f64() == Some(0.0)) || ["anchors", "in", "out"].iter().all(|k| p[k] == json!(false)) +} + /// A Stylize effect's dictionary, read only as it is written: each setting a key of the kind it /// names, with a finite value in the effect's range. struct Settings<'a>(&'a Filter); @@ -763,8 +948,9 @@ fn path_effect(f: &Filter) -> Option { Some(Effect { id: id.into(), params, visible: f.visible }) } -/// The live effect filter `f` is, when it is one read here. -fn effect(f: &Filter) -> Option { +/// The live effect filter `f` is, when it is one read here; `under` is what it could change the +/// paint of. +fn effect(f: &Filter, under: Under) -> Option { if let Some(stylized) = stylize(f) { let (id, params) = stylized?; return Some(Effect { id: id.into(), params, visible: f.visible }); @@ -789,22 +975,14 @@ fn effect(f: &Filter) -> Option { ) } OFFSET | OUTLINE_TYPE | OUTLINE_STROKE => return path_effect(f), - // Art space has y up: a move up is a negative `moveV` in the document. - TRANSFORM => ( - "distort.transform", - json!({ - "scaleH": param("scaleH_Percent").unwrap_or(100.0), - "scaleV": param("scaleV_Percent").unwrap_or(100.0), - "moveH": param("moveH_Pts").unwrap_or(0.0), - "moveV": -param("moveV_Pts").unwrap_or(0.0), - "rotate": param("rotate_Degrees").unwrap_or(0.0), - "copies": param("numCopies").unwrap_or(0.0).clamp(0.0, 1000.0) as i64, - "reflectX": flag("reflectX"), - "reflectY": flag("reflectY"), - "reference": param("pinPoint").unwrap_or(4.0).clamp(0.0, 8.0) as i64, - "random": flag("randomize"), - }), - ), + TRANSFORM => ("distort.transform", transform(f, under)?), + FREE_DISTORT => ("distort.freeDistort", free_distort(f)?), + // Negative puckers (https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html). + PUCKER_BLOAT => ("distort.puckerBloat", json!({"amount": real(f, "d_factor", 0.0, -200.0..=200.0)?})), + TWEAK => ("distort.tweak", tweak(f).filter(still)?), + // A positive angle turns clockwise in both. + TWIST => ("distort.twist", json!({"angle": real(f, "angle", 0.0, -3600.0..=3600.0)?})), + ZIG_ZAG => ("distort.zigZag", zig_zag(f)?), GAUSSIAN => ("blur.gaussian", json!({"radius": param("blur").unwrap_or(5.0).clamp(0.0, 1000.0)})), // Its settings are in a Photoshop plug-in's binary block: the effect's defaults stand in. BAS_RELIEF => ("sketch.basRelief", json!({"detail": 13.0, "smoothness": 3.0, "lightDirection": "bottom"})), @@ -878,7 +1056,25 @@ impl Builder<'_> { let mut look = ItemLook { visible: f.visible, ..ItemLook::default() }; let mut painted: Option = None; let mut paints = 0; - for k in self.children(id) { + let kids = self.children(id); + // What this item's effects could change: its own paint, and its other effects' distances. + let mut under = Under { strokes: false, patterns: false }; + let mut effects = 0; + for &k in &kids { + let c = &filters[k]; + match c.name.as_str() { + _ if c.compound => {} + FILL | STROKE => match paint_item(c) { + Ok(Some(StyleItem::Stroke { .. })) => under.strokes = true, + Ok(Some(StyleItem::Pattern { .. })) => under.patterns = true, + _ => {} + }, + BLEND => {} + _ => effects += 1, + } + } + under.strokes |= effects > 1; + for k in kids { let c = &filters[k]; match c.name.as_str() { _ if c.compound => self.plain = false, @@ -897,7 +1093,7 @@ impl Builder<'_> { } // Convert to Shape on one fill or stroke isn't read yet: what it draws there is // the drawn look's. - _ => match effect(c) { + _ => match effect(c, under) { Some(e) if !e.id.starts_with("convertToShape.") => look.effects.push(e), _ => self.plain = false, }, @@ -955,7 +1151,7 @@ fn style(filters: &[Filter]) -> ArtStyle { st.transparency = Some((opacity, super::read::blend_mode(mode), isolate, knockout)); } } - _ => match effect(f) { + _ => match effect(f, ANYTHING) { Some(e) => st.effects.push(e), None => b.plain = false, }, diff --git a/crates/eps/src/import/ai/style/tests.rs b/crates/eps/src/import/ai/style/tests.rs index 65baacfca..444f5c4fd 100644 --- a/crates/eps/src/import/ai/style/tests.rs +++ b/crates/eps/src/import/ai/style/tests.rs @@ -593,6 +593,226 @@ fn offset_path_with_settings_not_read_keeps_the_drawn_look() { } } +/// The style `Fx`: effect `name` on the whole object, its dictionary holding `keys`. +fn distort_effect_style(name: &str, keys: &str) -> ArtStyle { + let data = format!("/KnownStyle :\n(Fx) /Name ,\n/ActiveStyle :\n{}", compound("Chain Style Filter", &[basic(name, keys)])); + art_styles(data.as_bytes()).remove("Fx").unwrap_or_default() +} + +/// The params of effect `name` with `keys`, `None` when the style keeps the drawn look. +fn read_effect(name: &str, keys: &str) -> Option { + let st = distort_effect_style(name, keys); + if !st.plain { + return None; + } + let [e] = st.effects.as_slice() else { panic!("{st:?}") }; + Some(e.params.clone()) +} + +/// A filter named `name` whose dictionary holds `entries`. +fn dict(name: &str, entries: &[(&str, Kind, f64)]) -> Filter { + let kinds = entries.iter().map(|(k, kind, _)| (k.to_string(), *kind)).collect(); + let params = entries.iter().map(|(k, _, v)| (k.to_string(), *v)).collect(); + Filter { name: name.into(), kinds, params, ..Filter::default() } +} + +fn close(a: &serde_json::Value, want: f64) -> bool { + a.as_f64().is_some_and(|a| (a - want).abs() < 1e-6) +} + +/// Every switch of the Transform effect, as files write them. +const SWITCHES: &str = "0 /Bool (scaleLines) ,\n0 /Bool (transformPatterns) ,\n"; + +#[test] +fn a_transform_reads_its_factors_and_radians() { + // GUARDS: the percent and degree entries (what the dialog shows) were read, and the options were not. + let p = read_effect( + "Adobe Transform", + "150 /Real (scaleH_Percent) ,\n1.4 /Real (scaleH_Factor) ,\n80 /Real (scaleV_Percent) ,\n1.2 /Real (scaleV_Factor) ,\n\ + 30 /Real (rotate_Degrees) ,\n0.471238898 /Real (rotate_Radians) ,\n-8 /Real (moveV_Pts) ,\n3 /Real (moveH_Pts) ,\n\ + 2 /Int (numCopies) ,\n0 /Int (pinPoint) ,\n1 /Bool (reflectX) ,\n0 /Bool (transformObjects) ,\n1 /Bool (transformPatterns) ,\n\ + 1 /Bool (scaleLines) ,\n0 /Bool (randomize) ,\n", + ) + .unwrap(); + assert!(close(&p["scaleH"], 140.0) && close(&p["scaleV"], 120.0) && close(&p["rotate"], 27.0), "{p}"); + assert_eq!((&p["moveH"], &p["moveV"], &p["copies"], &p["reference"]), (&json!(3.0), &json!(8.0), &json!(2), &json!(0))); + assert_eq!((&p["reflectX"], &p["reflectY"]), (&json!(true), &json!(false))); + assert_eq!((&p["objects"], &p["patterns"], &p["strokes"]), (&json!(false), &json!(true), &json!(true))); +} + +#[test] +fn a_transforms_percent_and_degrees_alone_change_nothing() { + let p = read_effect("Adobe Transform", &format!("150 /Real (scaleH_Percent) ,\n30 /Real (rotate_Degrees) ,\n{SWITCHES}")).unwrap(); + assert_eq!((&p["scaleH"], &p["scaleV"], &p["rotate"]), (&json!(100.0), &json!(100.0), &json!(0.0))); +} + +#[test] +fn a_transforms_reference_point_and_move_up() { + for pin in 0..9 { + let p = read_effect("Adobe Transform", &format!("0.5 /Real (scaleH_Factor) ,\n{pin} /Int (pinPoint) ,\n{SWITCHES}")).unwrap(); + assert_eq!(p["reference"], json!(pin)); + } + // A move up the page in the file is a move up in the document, whose y runs down. + let p = read_effect("Adobe Transform", &format!("10 /Real (moveV_Pts) ,\n{SWITCHES}")).unwrap(); + assert_eq!(p["moveV"], json!(-10.0)); + let p = read_effect("Adobe Transform", &format!("4 /Int (pinPoint) ,\n{SWITCHES}")).unwrap(); + assert_eq!((&p["objects"], &p["copies"], &p["scaleH"]), (&json!(true), &json!(0), &json!(100.0))); +} + +#[test] +fn a_transform_without_the_options_its_change_depends_on_keeps_the_drawn_look() { + // Missing Scale Strokes & Effects or Transform Patterns are the app's settings, not the file's. + let half = "0.5 /Real (scaleH_Factor) ,\n0.5 /Real (scaleV_Factor) ,\n"; + assert_eq!(read_effect("Adobe Transform", &format!("{half}0 /Bool (transformPatterns) ,\n")), None); + assert_eq!(read_effect("Adobe Transform", &format!("{half}0 /Bool (scaleLines) ,\n")), None); + // A move scales nothing; with Transform Patterns given, it reads. + let p = read_effect("Adobe Transform", "5 /Real (moveH_Pts) ,\n0 /Bool (transformPatterns) ,\n").unwrap(); + assert_eq!(p["strokes"], json!(false)); + // On a fill's own Transform, only that fill's paint counts: a solid fill moves the same either way. + let moved = basic("Adobe Transform", "5 /Real (moveH_Pts) ,\n"); + let item = |paint: &str| { + let item = compound("Chain Style Filter", &[fill(paint), moved.clone()]); + let data = + format!("/KnownStyle :\n(Fx) /Name ,\n/ActiveStyle :\n{}", compound("Chain Style Filter", &[compound("Stack Style Filter", &[item])])); + art_styles(data.as_bytes()).remove("Fx").unwrap_or_default().plain + }; + assert!(item("0 0 0 1 0 0 0 Xa")); + assert!(!item("(Dots) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p")); +} + +#[test] +fn a_random_transform_keeps_the_drawn_look() { + // The file holds no seed for its random share. + assert_eq!(read_effect("Adobe Transform", &format!("1 /Bool (randomize) ,\n{SWITCHES}")), None); +} + +/// Free Distort's dictionary: the source corners `src` and the destination `dst`, top left, top +/// right, bottom left, bottom right, y up. +fn corners(src: [[f64; 2]; 4], dst: [[f64; 2]; 4]) -> String { + let mut out = String::new(); + for (which, c) in [("src", src), ("dst", dst)] { + for (i, [h, v]) in c.into_iter().enumerate() { + out += &format!("{h} /Real ({which}{i}h) ,\n{v} /Real ({which}{i}v) ,\n"); + } + } + out +} + +fn unit_corners(p: &serde_json::Value) -> Vec<[f64; 2]> { + p["corners"].as_array().unwrap().iter().map(|c| [c[0].as_f64().unwrap(), c[1].as_f64().unwrap()]).collect() +} + +fn corners_near(a: &[[f64; 2]], b: &[[f64; 2]]) -> bool { + a.len() == b.len() && a.iter().zip(b).all(|(a, b)| (a[0] - b[0]).abs() < 1e-9 && (a[1] - b[1]).abs() < 1e-9) +} + +#[test] +fn free_distort_measures_its_corners_in_its_own_frame() { + // GUARDS: Free Distort wasn't read, and type with it came in as its drawn look. + let dst = [[180.0, 310.0], [320.0, 300.0], [210.0, 240.0], [250.0, 260.0]]; + // A frame wider than the object (its stroke's): the corners are fractions of that frame. + let p = read_effect("Adobe Free Distort", &corners([[195.0, 305.0], [305.0, 305.0], [195.0, 245.0], [305.0, 245.0]], dst)).unwrap(); + let want = [[-15.0 / 110.0, -5.0 / 60.0], [125.0 / 110.0, 5.0 / 60.0], [0.5, 0.75], [15.0 / 110.0, 65.0 / 60.0]]; + assert!(corners_near(&unit_corners(&p), &want), "{p}"); + // A unit frame: the same corners measured in it, far outside the unit box. + let p = read_effect("Adobe Free Distort", &corners([[0.0, 0.0], [1.0, 0.0], [0.0, -1.0], [1.0, -1.0]], dst)).unwrap(); + assert!(corners_near(&unit_corners(&p), &[[180.0, -310.0], [320.0, -300.0], [250.0, -260.0], [210.0, -240.0]]), "{p}"); + // No corners: no change. + let p = read_effect("Adobe Free Distort", "").unwrap(); + assert!(corners_near(&unit_corners(&p), &[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]), "{p}"); +} + +#[test] +fn free_distort_without_every_corner_or_an_upright_frame_keeps_the_drawn_look() { + let src = [[0.0, 0.0], [1.0, 0.0], [0.0, -1.0], [1.0, -1.0]]; + let dst = [[0.1, 0.0], [1.0, 0.1], [0.0, -1.0], [1.0, -1.2]]; + let all = corners(src, dst); + // The destination alone, one entry missing, a frame that is turned or of no height. + let dst_only: String = all.lines().filter(|l| l.contains("(dst")).map(|l| format!("{l}\n")).collect(); + let short = all.replace("-1.2 /Real (dst3v) ,\n", ""); + for keys in [ + dst_only, + short, + corners([[0.0, 0.0], [1.0, 0.5], [0.0, -1.0], [1.0, -1.0]], dst), + corners([[0.0, 0.0], [1.0, 0.0], [0.0, 0.0], [1.0, 0.0]], dst), + // A frame too wide for a number. + corners([[-1e308, 0.0], [1e308, 0.0], [-1e308, -1.0], [1e308, -1.0]], dst), + ] { + assert_eq!(read_effect("Adobe Free Distort", &keys), None, "{keys}"); + } +} + +#[test] +fn pucker_and_twist_keep_their_sense() { + // Negative is Pucker; a positive angle turns clockwise in the file and in the catalog. + assert_eq!(read_effect("Adobe Punk and Bloat", "-35 /Real (d_factor) ,\n"), Some(json!({"amount": -35.0}))); + assert_eq!(read_effect("Adobe Punk and Bloat", ""), Some(json!({"amount": 0.0}))); + assert_eq!(read_effect("Adobe Twirl", "45 /Real (angle) ,\n"), Some(json!({"angle": 45.0}))); + assert_eq!(read_effect("Adobe Twirl", ""), Some(json!({"angle": 0.0}))); + assert_eq!(read_effect("Adobe Punk and Bloat", "250 /Real (d_factor) ,\n"), None); + assert_eq!(read_effect("Adobe Twirl", "4000 /Real (angle) ,\n"), None); +} + +#[test] +fn roughen_keeps_the_drawn_look() { + // Its outline is random and the file holds no seed; at a size of 0 the effect still redraws the + // path's segments. + assert_eq!(read_effect("Adobe Roughen", "30 /Real (size) ,\n"), None); + assert_eq!(read_effect("Adobe Roughen", ""), None); +} + +#[test] +fn tweak_picks_its_amounts_by_its_mode_and_moves_every_point_unless_told() { + let amounts = [("horz", Kind::Real, 30.0), ("vert", Kind::Real, 20.0), ("ahor", Kind::Real, 2.0), ("aver", Kind::Real, 3.0)]; + let mut absolute = amounts.to_vec(); + absolute.push(("absoluteness", Kind::Real, 1.0)); + let want = |h: f64, v: f64, relative: bool| json!({"h": h, "v": v, "relative": relative, "anchors": true, "in": true, "out": true}); + assert_eq!(tweak(&dict("Adobe Scribble and Tweak", &amounts)), Some(want(30.0, 20.0, true))); + assert_eq!(tweak(&dict("Adobe Scribble and Tweak", &absolute)), Some(want(2.0, 3.0, false))); + let off = tweak(&dict("Adobe Scribble and Tweak", &[("in", Kind::Bool, 0.0)])).unwrap(); + assert_eq!((&off["anchors"], &off["in"], &off["out"]), (&json!(true), &json!(false), &json!(true))); + // Random too: it reads only when it moves nothing. + assert_eq!(read_effect("Adobe Scribble and Tweak", "30 /Real (horz) ,\n"), None); + assert!(read_effect("Adobe Scribble and Tweak", "").is_some()); + let still = "30 /Real (horz) ,\n0 /Bool (anch) ,\n0 /Bool (in) ,\n0 /Bool (out) ,\n"; + assert!(read_effect("Adobe Scribble and Tweak", still).is_some()); +} + +#[test] +fn zig_zag_is_absolute_with_four_ridges_and_corners_unless_told() { + let p = read_effect("Adobe Zigzag", "30 /Real (relAmount) ,\n2 /Real (amount) ,\n").unwrap(); + assert_eq!(p, json!({"size": 2.0, "relative": false, "ridges": 4, "points": "corner"})); + let p = read_effect( + "Adobe Zigzag", + "30 /Real (relAmount) ,\n2 /Real (amount) ,\n0 /Real (absoluteness) ,\n6 /Real (ridges) ,\n1 /Real (roundness) ,\n", + ) + .unwrap(); + assert_eq!(p, json!({"size": 30.0, "relative": true, "ridges": 6, "points": "smooth"})); + assert_eq!(read_effect("Adobe Zigzag", "2.5 /Real (ridges) ,\n"), None); +} + +#[test] +fn a_distort_entry_this_cant_read_keeps_the_drawn_look_and_unknown_ones_are_ignored() { + // Not a number, the wrong kind, a switch other than 0 or 1, a whole number that isn't. + for (name, keys) in [ + ("Adobe Twirl", "NaN /Real (angle) ,\n"), + ("Adobe Twirl", "45 /Int (angle) ,\n"), + ("Adobe Zigzag", "2 /Real (absoluteness) ,\n"), + ("Adobe Transform", "4 /Real (pinPoint) ,\n0 /Bool (scaleLines) ,\n0 /Bool (transformPatterns) ,\n"), + ("Adobe Transform", "2 /Bool (reflectX) ,\n0 /Bool (scaleLines) ,\n0 /Bool (transformPatterns) ,\n"), + ("Adobe Transform", "1e999 /Real (scaleH_Percent) ,\n0 /Bool (scaleLines) ,\n0 /Bool (transformPatterns) ,\n"), + ("Adobe Transform", "1001 /Int (numCopies) ,\n0 /Bool (scaleLines) ,\n0 /Bool (transformPatterns) ,\n"), + // In degrees, too large for a number. + ("Adobe Transform", "1e308 /Real (rotate_Radians) ,\n0 /Bool (scaleLines) ,\n0 /Bool (transformPatterns) ,\n"), + ("Adobe Free Distort", "(x) /Real (src0h) ,\n"), + ] { + assert_eq!(read_effect(name, keys), None, "{name}: {keys}"); + } + assert_eq!(read_effect("Adobe Twirl", "37 /Real (angle) ,\n9 /Real (futureKey) ,\n"), Some(json!({"angle": 37.0}))); + // The last of a repeated entry stands. + assert_eq!(read_effect("Adobe Twirl", "37 /Real (angle) ,\n12 /Real (angle) ,\n"), Some(json!({"angle": 12.0}))); +} + #[test] fn a_hidden_fill_comes_in_hidden() { let hidden = fill("0 0 0 1 0 0 0 Xa").replacen("1 /Visible", "0 /Visible", 1); diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index d39eb89aa..29a45ae19 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -3459,6 +3459,71 @@ fn type_with_a_stacked_art_style_comes_in_as_type_with_that_appearance() { check(&r.document, &r.warnings); } +/// An art style section with style `Bent`: a blue fill over the characters, and effect `name`, its +/// dictionary holding `keys`, on the whole object. +fn whole_object_style(name: &str, keys: &str) -> String { + let basic = |name: &str, body: &str| { + format!("/BasicFilter :\n({name}) 0 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n{body}; /Dict ;\n /Part ,\n") + }; + let compound = |name: &str, kids: &[String]| format!("/CompoundFilter :\n({name}) 0 0 /Filter ,\n1 /Visible ,\n{};\n /Part ,\n", kids.concat()); + let fill = basic("Fill Style Filter", "/FillStyle : 0 O\n0 0 0 0 0 0 1 Xa\n0 1 Xd\n/Def ;\n (FillStyle) ,\n"); + let stack = compound("Stack Style Filter", &[fill, basic("Conduit Filter", "")]); + let root = compound("Chain Style Filter", &[stack, basic(name, keys)]); + format!("%AI9_BeginArtStyles\n/KnownStyle :\n(Bent) /Name ,\n/ActiveStyle :\n{root}%AI9_EndArtStyles\n") +} + +#[test] +fn type_with_a_distort_effect_comes_in_as_type_with_it_live() { + // GUARDS: Free Distort, Pucker & Bloat, Twist, Zig Zag and Tweak weren't read, and type with them came in + // without its art style, the style's drawing beside it as paths. + use serde_json::json; + let doc = text_document(&[("Bang\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]); + let drawing = "0 0 1 0 k\n54 56 m\n100 56 L\n100 80 L\n54 80 L\nf"; + let look = format!("u\n/AI11Text :\n1 /FreeUndo ,\n0 /FrameIndex ,\n0 /StoryIndex ,\n;\n{drawing}\nU\n9 () XW\n"); + let own: String = text_object(0).lines().map(|l| format!("%_{l}\n")).collect(); + let open = |name: &str, keys: &str| { + let art = native_with_text(&(whole_object_style(name, keys) + &layer("Art", true, &format!("{look}{own}1 (Bent) XW\n"))), &doc); + let (d, _) = crate::ai_alone(&private(&art)).unwrap(); + let (mut texts, mut paths) = (vec![], 0); + d.walk(|n| match &n.kind { + NodeKind::Text(t) => texts.push((t.plain_text(), n.appearance.effects.clone())), + NodeKind::Path { .. } => paths += 1, + _ => {} + }); + (texts, paths) + }; + for (name, keys, id, param, want) in [ + ("Adobe Twirl", "45 /Real (angle) ,\n", "distort.twist", "angle", json!(45.0)), + ( + "Adobe Transform", + "5 /Real (moveH_Pts) ,\n-7 /Real (moveV_Pts) ,\n0 /Bool (transformPatterns) ,\n", + "distort.transform", + "moveV", + json!(7.0), + ), + ( + "Adobe Scribble and Tweak", + "30 /Real (horz) ,\n20 /Real (vert) ,\n0 /Bool (anch) ,\n0 /Bool (in) ,\n0 /Bool (out) ,\n", + "distort.tweak", + "h", + json!(30.0), + ), + ("Adobe Punk and Bloat", "-50 /Real (d_factor) ,\n", "distort.puckerBloat", "amount", json!(-50.0)), + ("Adobe Zigzag", "3 /Real (amount) ,\n1 /Real (absoluteness) ,\n7 /Real (ridges) ,\n", "distort.zigZag", "ridges", json!(7)), + ("Adobe Free Distort", "", "distort.freeDistort", "corners", json!([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]])), + ] { + let (texts, paths) = open(name, keys); + let [(text, effects)] = texts.as_slice() else { panic!("{name}: {texts:?}") }; + assert_eq!((text.as_str(), paths), ("Bang", 0), "{name}"); + let [e] = effects.as_slice() else { panic!("{name}: {effects:?}") }; + assert_eq!((e.id.as_str(), &e.params[param]), (id, &want), "{name}"); + } + // An angle that isn't a number: the drawing as paths, and no effect on the type. + let (texts, paths) = open("Adobe Twirl", "NaN /Real (angle) ,\n"); + assert_eq!(paths, 1, "{texts:?}"); + assert!(texts.iter().all(|(_, effects)| effects.is_empty()), "{texts:?}"); +} + #[test] fn type_with_convert_to_shape_comes_in_as_type_with_it_live() { // GUARDS: `Adobe Shape Effects` wasn't read, and type with it came in without its art style, diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 61b891886..450872215 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -121,7 +121,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri 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, 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), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), 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), guides, and every artboard where it is. 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 an Outer Glow or Drop Shadow comes in as itself with them; one with more, other effects, a brush or dashes aligned to corners comes in as its drawn look (a group named after it). 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, Transform (with copies), Drop Shadow, Outer Glow, Feather, Gaussian Blur and Bas Relief (its settings at their defaults), and a style with others keeps the type's 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 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); the page's drawing of it (often in pieces) is left out. 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 a graphic after the text object of a legacy wraparound group (`*w` … `*W`), 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, 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 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). 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). 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, 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), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), 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), guides, and every artboard where it is. 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 an Outer Glow or Drop Shadow comes in as itself with them; one with more, other effects, a brush or dashes aligned to corners comes in as its drawn look (a group named after it). 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, Transform (with copies), Drop Shadow, Outer Glow, Feather, Gaussian Blur and Bas Relief (its settings at their defaults), and a style with others keeps the type's 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 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); the page's drawing of it (often in pieces) is left out. 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. 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, 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). 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). 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, 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), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), 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), guides, and every artboard where it is. 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 an Outer Glow, Drop Shadow or Convert to Shape comes in as itself with them; one with more, other effects, a brush or dashes aligned to corners comes in as its drawn look (a group named after it). 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, Transform (with copies), Drop Shadow, Outer Glow, Feather, Gaussian Blur, Bas Relief (its settings at their defaults) and Convert to Shape, and a style with others 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 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); the page's drawing of it (often in pieces) is left out. 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. 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, 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 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). 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). 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, 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), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), 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), guides, and every artboard where it is. 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 or Scribble comes in as itself with them and their saved settings; one with more, other effects, 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), a brush or dashes aligned to corners comes in as its drawn look (a group named after it). 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, Transform (with copies), Drop Shadow, Inner Glow, Outer Glow, Feather, Round Corners, Scribble, Gaussian Blur and Bas Relief (its settings at their defaults), and a style with others keeps the type's 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 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); the page's drawing of it (often in pieces) is left out. 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. 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, 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 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). 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). 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, 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), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), 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), guides, and every artboard where it is. 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 or Scribble comes in as itself with them and their saved settings; one with more, other effects, 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), a brush or dashes aligned to corners comes in as its drawn look (a group named after it). 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, 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 and Bas Relief (its settings at their defaults), and a style with others, or with an effect setting that isn't read, keeps the type's 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 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); the page's drawing of it (often in pieces) is left out. 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. 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, 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 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). 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). 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, 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), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), 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), guides, and every artboard where it is. 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 an Outer Glow or Drop Shadow comes in as itself with them; one with more, other effects, a brush or dashes aligned to corners comes in as its drawn look (a group named after it). 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, Transform (with copies), Drop Shadow, Outer Glow, Feather, Gaussian Blur and Bas Relief (its settings at their defaults), and a style with others keeps the type's 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); the page's drawing of it (often in pieces) is left out. 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. 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, 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 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). 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). 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. ## Format by format