diff --git a/crates/engine/src/cmd/fileio/tests_aiimport.rs b/crates/engine/src/cmd/fileio/tests_aiimport.rs index 50d09b025..e0f6600d9 100644 --- a/crates/engine/src/cmd/fileio/tests_aiimport.rs +++ b/crates/engine/src/cmd/fileio/tests_aiimport.rs @@ -127,6 +127,45 @@ fn an_ai_unread_group_keeps_its_layers_and_live_neighbor() { assert!(result["warnings"].to_string().contains("`ZZ` (1)"), "{result}"); } +#[test] +fn a_differing_object_keeps_the_layers_with_a_note() { + let square = |cmyk: &str, x: f64| format!("{cmyk} k\n{}f\n", ai::rect(x, 40.0, x + 16.0, 56.0)); + let front = square("0 0 0 1", 90.0) + &square("0 0 1 0", 160.0); + let data = ai::editing_data(200.0, 100.0, &(ai::layer("Back", &square("0 0 1 0", 20.0)) + &ai::layer("Front", &front))); + let page = "0 0 1 0 k 20 40 16 16 re f 1 0 0 0 k 90 40 16 16 re f 0 0 1 0 k 160 40 16 16 re f"; + let mut session = Session::new(); + let result = open(&mut session, "differs.ai", &ai::ai(&compressed(data.as_bytes()), page), json!({})).unwrap(); + let doc = &session.doc().unwrap().doc; + assert_eq!(names(&doc.layers), ["Back", "Front"], "{result}"); + let front = doc.layers[1].children().unwrap(); + assert!(matches!(front[0].kind, NodeKind::Group { .. }) && matches!(front[1].kind, NodeKind::Path { .. }), "{result}"); + let replacement = &front[0].children().unwrap()[0]; + assert!( + matches!(replacement.appearance.items.first(), Some(vectorcraft_doc::AppearanceItem::Fill(fill)) + if fill.paint == vectorcraft_color::Paint::solid(vectorcraft_color::Color::cmyk(1.0, 0.0, 0.0, 0.0))), + "{result}" + ); + assert!( + result["warnings"].to_string().contains("objects that drew differently from the file's own page came in as its art: Front (1)"), + "{result}" + ); + // An export to the file's own path is refused unless it is given `acknowledgeLoss`. + let d = std::env::temp_dir().join(format!("vc-aidiffers-{}", std::process::id())); + std::fs::create_dir_all(&d).unwrap(); + let path = d.join("differs.ai"); + let file = ai::ai(&compressed(data.as_bytes()), page); + std::fs::write(&path, &file).unwrap(); + let path = path.to_string_lossy().to_string(); + let mut s = Session::new(); + s.execute("document.open", &json!({"path": path})).unwrap(); + let e = s.execute("document.export", &json!({"path": path})).unwrap_err().to_string(); + assert!(e.contains("left things out") && e.contains("objects that drew differently"), "{e}"); + assert_eq!(std::fs::read(&path).unwrap(), file); + s.execute("document.export", &json!({"path": path, "acknowledgeLoss": true})).unwrap(); + assert_ne!(std::fs::read(&path).unwrap(), file); + std::fs::remove_dir_all(d).ok(); +} + /// A palette of three swatches and a color group `Inks` holding a fourth. const INKS_PALETTE: &str = "%AI5_BeginPalette\n0 0 Pb\n0 0 0 0 1 1 1 Xa\n(White)\nPc\n0.04 0.3 0 0 81.96 21 -8 (Pink ink) 0 2 Xx\n(Pink ink)\nPc\n\ 0.06 0.98 1 0 (Red ink) 0 x\n(Red ink)\nPc\n1 (Inks) 1 Pg\n0.02 0.1 1 0 (Yellow ink) 0 x\n(Yellow ink)\nPc\nPB\n%AI5_EndPalette\n"; diff --git a/crates/eps/src/import/native/build.rs b/crates/eps/src/import/native/build.rs index f73121f23..07e88e540 100644 --- a/crates/eps/src/import/native/build.rs +++ b/crates/eps/src/import/native/build.rs @@ -1,8 +1,8 @@ -use super::compare::{MAX_DIFFERENCE, PageView, compare, difference}; +use super::compare::{MAX_DIFFERENCE, MAX_SOLID_DIFFERENCE, PageView, compare, difference}; use super::plan::{made_only, make_slots, plan, readable_all}; use super::slots::{ - ART_LEFT_OUT, Content, MAX_PAIRS, MAX_SLOTS, MAX_TEXT, OUTLINE_REACH, SlotInfo, TEXT_LEFT_OUT, TYPE_LEFT_OUT, collect_slots, extent, few, - fill_slots, letters, text_objects, words, + ART_LEFT_OUT, Content, DREW_DIFFERENTLY, MAX_PAIRS, MAX_SLOTS, MAX_TEXT, OUTLINE_REACH, SlotInfo, TEXT_LEFT_OUT, TYPE_LEFT_OUT, collect_slots, + extent, few, fill_slots, letters, text_objects, words, }; use super::{Affine, Arc, BTreeSet, Document, Node, NodeId, NodeKind, PathData, Rect, Scaling, Story, Structure, Texts, Unit, Vec2, ai}; @@ -341,10 +341,22 @@ pub(super) fn build_from( doc = other; notes.extend(unseen_note(unseen).into_iter().chain(note)); } - _ => { - let more = drawn_or_trusted(&frame, &mut view, &mut doc, &envelopes_drawn, ¬es, unseen, trust, why, reflow)?; - notes.extend(more); - } + other => match drawn_or_trusted(&frame, &mut view, &mut doc, &envelopes_drawn, ¬es, unseen, trust, why, reflow) { + Ok(more) => notes.extend(more), + // The objects that draw differently from the page come in as its art, from + // the placement that draws closer to it. + Err(why) => { + let other = other.map(|(_, other, unseen)| (other, unseen)); + let placements = closer_first(&frame, &mut view, (doc, unseen), other, reflow); + let Some((kept, unseen, kept_notes)) = placements.into_iter().find_map(|(base, unseen)| { + keep_differing(&base, &frame, &mut view, &threaded_frames, &shown, reflow).map(|(kept, n)| (kept, unseen, n)) + }) else { + return Err(why); + }; + doc = kept; + notes.extend(unseen_note(unseen).into_iter().chain(kept_notes)); + } + }, }, } } @@ -766,6 +778,135 @@ fn replace_unread( Ok(counts.into_iter().map(|(kind, count)| format!("{kind} ({count})")).collect()) } +/// Most objects of the layers that come in as the page's art where they draw differently from it. +const MAX_DIFFERING: usize = 64; + +/// Most pairs of a patch where the layers and the page differ and a top-level object that may hold +/// it, weighed to find the objects that come in as the page's art. +const MAX_PATCH_PAIRS: usize = 4_000_000; + +/// The layers as read and at the placed files' other placement (each with its count of text +/// objects the page doesn't draw), the one that draws closer to the page first. `reflow` as in +/// [`PageView::difference`]. +fn closer_first( + frame: &Frame<'_>, + view: &mut PageView, + doc: (Document, usize), + other: Option<(Document, usize)>, + reflow: bool, +) -> Vec<(Document, usize)> { + match other { + Some(other) if view.difference(&other.0, frame.rect, reflow).any < view.difference(&doc.0, frame.rect, reflow).any => vec![other, doc], + Some(other) => vec![doc, other], + None => vec![doc], + } +} + +/// `doc`, whose layers draw differently from the page (an object in another color, or one that only +/// one of them draws), with each of its top-level objects that holds such a difference replaced by the +/// page's art in its bounds, when that makes the two look alike → the document and its notes: the +/// comparison's, and how many objects were replaced on each layer (by name, in the layers' order). +/// `None` when only edges or type differ, a difference lies outside every object, more than half +/// of the objects (or more than [`MAX_DIFFERING`]) hold one, one of them holds type or a wrap +/// object, the page's art can't replace one of them ([`replace_unread`]), the page's type of a text +/// object that stays is copied into a replacement, or the two still differ. `shown` is the page's +/// type of each text object that shows. `reflow` as in [`PageView::difference`]. +fn keep_differing( + doc: &Document, + frame: &Frame<'_>, + view: &mut PageView, + threaded_frames: &BTreeSet, + shown: &[Vec>], + reflow: bool, +) -> Option<(Document, Vec)> { + /// The top-level objects that show and print (the outermost objects under a layer or + /// sublayer), with the name of that layer or sublayer as the Layers panel shows it and their + /// bounds. + fn top_level(nodes: &[Arc], layer: &str, out: &mut Vec<(String, NodeId, Rect)>) { + for n in nodes.iter().filter(|n| n.visible) { + match &n.kind { + NodeKind::Layer { printable: false, .. } => {} + NodeKind::Layer { .. } => { + let name = Some(n.display_name()).filter(|name| !name.is_empty()).unwrap_or_else(|| format!("<{}>", n.kind_label())); + top_level(n.children().map_or(&[], Vec::as_slice), &name, out); + } + NodeKind::Path { guide: true, .. } => {} + _ => { + if let Some(b) = n.visual_bounds().filter(|b| b.area().is_finite() && b.area() > 0.0) { + out.push((layer.to_string(), n.id, b)); + } + } + } + } + } + + // Where only spread edges or type differ, or only the art around the page, no object is + // replaced. + let (diff, regions) = view.solid_regions(doc, frame.rect, reflow)?; + if diff.solid <= MAX_SOLID_DIFFERENCE { + return None; + } + let mut objects = vec![]; + top_level(&doc.layers, "", &mut objects); + objects.retain(|(_, _, b)| !b.intersect(frame.rect).is_zero_area()); + if regions.len().saturating_mul(objects.len()) > MAX_PATCH_PAIRS { + return None; + } + // Each patch is the smallest object's that holds it. + let most = MAX_DIFFERING.min(objects.len() / 2); + let mut targets = BTreeSet::new(); + for region in regions { + let (_, id, _) = objects.iter().filter(|(_, _, b)| inside(region, *b)).min_by(|a, b| a.2.area().total_cmp(&b.2.area()))?; + targets.insert(*id); + if targets.len() > most { + return None; + } + } + if targets.is_empty() { + return None; + } + // A replaced wrap object keeps its wrap options, and the area type is wrapped around the page's + // art that replaces it. An object inside a replaced group can't keep them, so such a group isn't + // replaced. Neither is an object that holds type: the page's art that replaces it would hold + // type the page draws for other text objects too. + let mut wraps = vec![]; + for id in &targets { + let node = doc.node(*id)?; + let mut held = matches!(node.kind, NodeKind::Text(_)); + for child in node.children().into_iter().flatten() { + child.walk(&mut |n| held |= n.extras().wrap.is_some() || matches!(n.kind, NodeKind::Text(_))); + } + if held { + return None; + } + if let Some(wrap) = node.extras().wrap { + wraps.push((*id, wrap)); + } + } + let mut kept = doc.clone(); + let replace: Vec<(NodeId, Vec)> = targets.iter().map(|id| (*id, vec![])).collect(); + let mut copied = BTreeSet::new(); + replace_unread(&mut kept, frame, &replace, &[], threaded_frames, &mut copied).ok()?; + // A text object that stays draws its page type, so a copy of that type would draw it twice. + if shown.iter().flatten().any(|n| copied.contains(&n.id)) { + return None; + } + for (id, wrap) in wraps { + kept.node_mut(id)?.extras_mut().wrap = Some(wrap); + } + resolve_text_wrap(&mut kept).ok()?; + let note = compare(frame, view, &kept, reflow).ok()?; + let mut layers: Vec<(String, usize)> = vec![]; + for (layer, _, _) in objects.iter().filter(|(_, id, _)| targets.contains(id)) { + match layers.iter_mut().find(|(name, _)| name == layer) { + Some((_, count)) => *count += 1, + None => layers.push((layer.clone(), 1)), + } + } + let layers: Vec = layers.into_iter().map(|(layer, count)| format!("{layer} ({count})")).collect(); + Some((kept, note.into_iter().chain([format!("{DREW_DIFFERENTLY}: {}", layers.join(", "))]).collect())) +} + /// Copy page drawing IDs, including mask art outside the ordinary child tree. fn reid_page_art(doc: &mut Document, source: &Node) -> Node { let mut copy = doc.reid(source); diff --git a/crates/eps/src/import/native/compare.rs b/crates/eps/src/import/native/compare.rs index f382059b6..ac1718e1a 100644 --- a/crates/eps/src/import/native/compare.rs +++ b/crates/eps/src/import/native/compare.rs @@ -182,6 +182,12 @@ impl PageView { let printed = as_printed(b); let b = printed.as_ref().unwrap_or(b); let y = comparison_render(&mut self.layers, b, rb, self.scale, self.samples); + let areas = self.counted_type(b, rb, reflow); + compared(&self.page, &y, &areas, &self.restyled) + } + + /// Where [`Self::difference`] counts `b`'s and the page's pixels as type, in the page's pixels. + fn counted_type(&self, b: &Document, rb: Rect, reflow: bool) -> Vec { let mut areas = vec![]; type_areas(&b.layers, rb, self.scale, &mut areas); if reflow { @@ -194,7 +200,7 @@ impl PageView { } else { areas.extend(&self.page_type); } - compared(&self.page, &y, &areas, &self.restyled) + areas } /// The share (0 to 1) of the pixels where `b` on `rb` draws another colour than the page @@ -210,6 +216,52 @@ impl PageView { let differing = self.page.pixels.as_chunks::<4>().0.iter().zip(y.pixels.as_chunks::<4>().0).filter(|(p, q)| differ_in_colour(p, q)).count(); differing as f64 / (f64::from(y.width) * f64::from(y.height)).max(1.0) } + + /// How `b` on `rb` draws differently from the page ([`Self::difference`]), and the boxes (in + /// `b`'s terms) around each patch of the pixels counted in its `solid` share (they and all + /// their neighbors differ, away from type), neighbors touching corner to corner counting as one patch. `None` when the two + /// drawings aren't the same size. + pub(super) fn solid_regions(&mut self, b: &Document, rb: Rect, reflow: bool) -> Option<(Difference, Vec)> { + let printed = as_printed(b); + let b = printed.as_ref().unwrap_or(b); + let y = comparison_render(&mut self.layers, b, rb, self.scale, self.samples); + let areas = self.counted_type(b, rb, reflow); + let (diff, mut solid) = compared_with_solid(&self.page, &y, &areas, &self.restyled); + let (w, h) = (y.width as usize, y.height as usize); + if solid.len() != w * h { + return None; + } + let mut regions = vec![]; + let mut stack = vec![]; + for start in 0..solid.len() { + if !solid.get(start).copied().unwrap_or(false) { + continue; + } + // The patch's pixels are cleared as they are taken, so each is taken once. + if let Some(s) = solid.get_mut(start) { + *s = false; + } + stack.push(start); + let (mut i0, mut j0, mut i1, mut j1) = (start % w, start / w, start % w, start / w); + while let Some(k) = stack.pop() { + let (i, j) = (k % w, k / w); + (i0, j0, i1, j1) = (i0.min(i), j0.min(j), i1.max(i), j1.max(j)); + for n in j.saturating_sub(1)..=(j + 1).min(h - 1) { + for m in i.saturating_sub(1)..=(i + 1).min(w - 1) { + if let Some(s) = solid.get_mut(n * w + m).filter(|s| **s) { + *s = false; + stack.push(n * w + m); + } + } + } + } + let s = self.scale; + let at = |i: usize, j: usize| (rb.x0 + i as f64 / s, rb.y0 + j as f64 / s); + let ((x0, y0), (x1, y1)) = (at(i0, j0), at(i1 + 1, j1 + 1)); + regions.push(Rect::new(x0, y0, x1, y1)); + } + Some((diff, regions)) + } } /// How `a` on `ra` and `b` on `rb` (rectangles of the same size) draw differently ([`compared`]), @@ -233,8 +285,14 @@ pub(super) fn difference(a: &Document, ra: Rect, b: &Document, rb: Rect) -> Diff /// at all ([`differ`]): the page of an RGB document is written in the colours' CMYK equivalents, /// and what the comparison looks for is art that is missing or out of place. pub(super) fn compared(x: &vectorcraft_render::Rendered, y: &vectorcraft_render::Rendered, areas: &[Rect], skip: &[Rect]) -> Difference { + compared_with_solid(x, y, areas, skip).0 +} + +/// [`compared`], and which of the pixels (row by row) are counted in its `solid` share: none when +/// the drawings can't be compared. +fn compared_with_solid(x: &vectorcraft_render::Rendered, y: &vectorcraft_render::Rendered, areas: &[Rect], skip: &[Rect]) -> (Difference, Vec) { if x.width == 0 || x.height == 0 || x.width != y.width || x.height != y.height { - return Difference { any: 1.0, art: 1.0, solid: 1.0 }; + return (Difference { any: 1.0, art: 1.0, solid: 1.0 }, vec![]); } let (w, h) = (x.width as usize, x.height as usize); // Pixels in `skip` count as alike. @@ -245,12 +303,22 @@ pub(super) fn compared(x: &vectorcraft_render::Rendered, y: &vectorcraft_render: let typed = in_areas(areas, w, h); let art = differs.iter().zip(&typed).filter(|(d, t)| **d && !**t).count(); // A pixel on the region's edge has neighbours outside it, which don't count as differing. - let solid = (1..h.saturating_sub(1)) - .flat_map(|j| (1..w.saturating_sub(1)).map(move |i| (i, j))) - .filter(|&(i, j)| !typed.get(j * w + i).copied().unwrap_or(true) && (j - 1..=j + 1).all(|n| (i - 1..=i + 1).all(|m| at(m, n)))) - .count(); + let mut solid = vec![false; w * h]; + for (i, j) in (1..h.saturating_sub(1)).flat_map(|j| (1..w.saturating_sub(1)).map(move |i| (i, j))) { + if !typed.get(j * w + i).copied().unwrap_or(true) + && (j - 1..=j + 1).all(|n| (i - 1..=i + 1).all(|m| at(m, n))) + && let Some(s) = solid.get_mut(j * w + i) + { + *s = true; + } + } let area = (w as f64 * h as f64).max(1.0); - Difference { any: differs.iter().filter(|d| **d).count() as f64 / area, art: art as f64 / area, solid: solid as f64 / area } + let shares = Difference { + any: differs.iter().filter(|d| **d).count() as f64 / area, + art: art as f64 / area, + solid: solid.iter().filter(|s| **s).count() as f64 / area, + }; + (shares, solid) } /// Where the type that shows in `nodes` is, in pixels of `region` drawn at `scale` (with a margin diff --git a/crates/eps/src/import/native/slots.rs b/crates/eps/src/import/native/slots.rs index 1033effa9..cbb478752 100644 --- a/crates/eps/src/import/native/slots.rs +++ b/crates/eps/src/import/native/slots.rs @@ -18,6 +18,10 @@ pub(super) const PAGE_TYPE_UNCHECKED: &str = pub(super) const LAYERS_UNREAD: &str = "the file's layers weren't read"; +/// Objects of the layers replaced with the page's art where they drew differently from it, +/// followed by the layers they are on. +pub(super) const DREW_DIFFERENTLY: &str = "objects that drew differently from the file's own page came in as its art"; + /// Does this import note say that the file has something the document doesn't (hidden text, art or /// layers that could not be read, or the page's type that no text object takes or may not take)? /// Writing the document over the file would lose it for good. @@ -27,7 +31,9 @@ pub fn is_loss(note: &str) -> bool { || note.ends_with("they came in as the page's art") || note == PAGE_TYPE_LEFT_OUT || note == PAGE_TYPE_UNCHECKED - || [ART_LEFT_OUT, LAYERS_UNREAD, crate::import::graphics::TOO_MUCH, crate::import::graphics::FAR_AWAY].iter().any(|n| note.starts_with(n)) + || [ART_LEFT_OUT, LAYERS_UNREAD, DREW_DIFFERENTLY, crate::import::graphics::TOO_MUCH, crate::import::graphics::FAR_AWAY] + .iter() + .any(|n| note.starts_with(n)) } /// Most text objects in the layers, and most pieces of type in one place on the page, that are matched diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index daeea4826..6c409bd40 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -824,6 +824,215 @@ fn top_level_placed_file_without_page_art_falls_back() { assert!(notes.iter().any(|n| n.contains("page art wasn't found")), "{notes:?}"); } +const BLACK: &str = "0 0 0 1"; +const YELLOW: &str = "0 0 1 0"; +const PAGE_CYAN: [f32; 4] = [1.0, 0.0, 0.0, 0.0]; +const PAGE_YELLOW: [f32; 4] = [0.0, 0.0, 1.0, 0.0]; + +/// A square of 16 points at (`x`, 10) in the format's operators, painted in CMYK `cmyk`. +fn square_16(x: u32, cmyk: &str) -> String { + format!("{cmyk} k\n{x} 10 m\n{} 10 L\n{} 26 L\n{x} 26 L\nf", x + 16, x + 16) +} + +/// A 100 × 100 page with a square of 16 points at each (`x`, `y`, CMYK) of `squares`: at `y` 74 +/// where [`square_16`] puts the layers' squares. +fn page_16(squares: &[(f64, f64, [f32; 4])]) -> Document { + let mut d = Document::new(100.0, 100.0); + let layer = d.layers[0].id; + for (i, (x, y, [c, m, ye, k])) in squares.iter().enumerate() { + let path = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(*x, *y, x + 16.0, y + 16.0)); + let paint = vectorcraft_color::Paint::solid(vectorcraft_color::Color::cmyk(*c, *m, *ye, *k)); + let item = vectorcraft_doc::AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(paint)); + let n = Node::path(d.alloc_id(), path, vectorcraft_doc::Appearance { items: vec![item], ..Default::default() }); + d.insert(Some(layer), i, n).unwrap(); + } + d +} + +/// Three squares on two layers, the middle one black in the layers and cyan on the page. +fn middle_differs() -> (String, Document) { + let front = format!("{}\n{}", square_16(40, BLACK), square_16(76, YELLOW)); + let art = native(&(layer("Back", true, &square_16(4, YELLOW)) + &layer("Front", true, &front))); + (art, page_16(&[(4.0, 74.0, PAGE_YELLOW), (40.0, 74.0, PAGE_CYAN), (76.0, 74.0, PAGE_YELLOW)])) +} + +#[test] +fn an_object_that_draws_differently_comes_in_as_the_page_art() { + let (art, page) = middle_differs(); + let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0); + assert!(read, "{notes:?}"); + assert_eq!(names(&doc), ["Back", "Front"]); + assert_eq!(kinds(&doc.layers[0]), ["path"]); + assert_eq!(kinds(&doc.layers[1]), ["group[1]", "path"]); + replacement_is_cyan(&doc, 1, 0); + let note = "objects that drew differently from the file's own page came in as its art: Front (1)"; + assert!(notes.iter().any(|n| n == note && crate::is_loss(n)), "{notes:?}"); + assert!(!notes.iter().any(|n| n.contains("weren't read")), "{notes:?}"); +} + +#[test] +fn two_differing_objects_on_different_layers_are_both_replaced() { + let back = format!("{}\n{}", square_16(4, BLACK), square_16(28, YELLOW)); + let front = format!("{}\n{}", square_16(52, YELLOW), square_16(76, BLACK)); + let art = native(&(layer("Back", true, &back) + &layer("Front", true, &front))); + let page = page_16(&[(4.0, 74.0, PAGE_CYAN), (28.0, 74.0, PAGE_YELLOW), (52.0, 74.0, PAGE_YELLOW), (76.0, 74.0, PAGE_CYAN)]); + let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0); + assert!(read, "{notes:?}"); + assert_eq!(names(&doc), ["Back", "Front"]); + assert_eq!(kinds(&doc.layers[0]), ["group[1]", "path"]); + assert_eq!(kinds(&doc.layers[1]), ["path", "group[1]"]); + replacement_is_cyan(&doc, 0, 0); + replacement_is_cyan(&doc, 1, 1); + let note = "objects that drew differently from the file's own page came in as its art: Back (1), Front (1)"; + assert!(notes.iter().any(|n| n == note), "{notes:?}"); +} + +/// The text wrap options of a 2-point offset, for the object before them. +const WRAP_OPTIONS: &str = "%_/ArtDictionary :\n%_0 /Bool (AI11TextWrapInvert) ,\n%_2 /Real (AI11TextWrapOffset) ,\n%_;\n%_\n"; + +#[test] +fn a_differing_wrap_object_keeps_its_wrap_options() { + let front = format!("{}\n{WRAP_OPTIONS}{}", square_16(40, BLACK), square_16(76, YELLOW)); + let art = native(&(layer("Back", true, &square_16(4, YELLOW)) + &layer("Front", true, &front))); + let page = page_16(&[(4.0, 74.0, PAGE_YELLOW), (40.0, 74.0, PAGE_CYAN), (76.0, 74.0, PAGE_YELLOW)]); + let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0); + assert!(read, "{notes:?}"); + assert_eq!(kinds(&doc.layers[1]), ["group[1]", "path"]); + replacement_is_cyan(&doc, 1, 0); + let group = &doc.layers[1].children().unwrap()[0]; + assert_eq!(group.extras().wrap, Some(vectorcraft_doc::TextWrap { offset: 2.0, invert: false })); +} + +#[test] +fn a_differing_group_holding_a_wrap_object_still_falls_back() { + let group = os1_group(&format!("{}\n{WRAP_OPTIONS}", square_16(40, BLACK))); + let front = format!("{group}\n{}", square_16(76, YELLOW)); + let art = native(&(layer("Back", true, &square_16(4, YELLOW)) + &layer("Front", true, &front))); + let page = page_16(&[(4.0, 74.0, PAGE_YELLOW), (40.0, 74.0, PAGE_CYAN), (76.0, 74.0, PAGE_YELLOW)]); + let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0); + assert!(!read, "{notes:?}"); + assert_eq!(names(&doc), ["Layer 1"]); + assert!(!notes.iter().any(|n| n.starts_with("objects that drew")), "{notes:?}"); +} + +#[test] +fn a_page_that_differs_everywhere_still_comes_in_as_its_pdf_part() { + let front = format!("{}\n{}", square_16(40, BLACK), square_16(76, BLACK)); + let art = native(&(layer("Back", true, &square_16(4, BLACK)) + &layer("Front", true, &front))); + let page = page_16(&[(4.0, 74.0, PAGE_CYAN), (40.0, 74.0, PAGE_CYAN), (76.0, 74.0, PAGE_CYAN)]); + let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0); + assert!(!read, "{notes:?}"); + assert_eq!(names(&doc), ["Layer 1"]); + assert!(notes.iter().any(|n| n.contains("weren't read") && n.contains("differs from the page's")), "{notes:?}"); + assert!(!notes.iter().any(|n| n.starts_with("objects that drew")), "{notes:?}"); +} + +#[test] +fn a_page_object_outside_every_layer_object_still_falls_back() { + let front = format!("{}\n{}", square_16(40, YELLOW), square_16(76, YELLOW)); + let art = native(&(layer("Back", true, &square_16(4, YELLOW)) + &layer("Front", true, &front))); + let page = page_16(&[(4.0, 74.0, PAGE_YELLOW), (40.0, 74.0, PAGE_YELLOW), (76.0, 74.0, PAGE_YELLOW), (40.0, 20.0, PAGE_CYAN)]); + let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0); + assert!(!read, "{notes:?}"); + assert_eq!(names(&doc), ["Layer 1"]); + assert!(notes.iter().any(|n| n.contains("differs from the page's")), "{notes:?}"); +} + +#[test] +fn a_differing_object_with_live_art_inside_falls_back() { + // The group reaches over the square between its own two. + let group = os1_group(&format!("{}\n{}", square_16(4, BLACK), square_16(40, YELLOW))); + let front = format!("{group}\n{}\n{}", square_16(22, YELLOW), square_16(76, YELLOW)); + let art = native(&layer("Front", true, &front)); + let page = page_16(&[(4.0, 74.0, PAGE_CYAN), (22.0, 74.0, PAGE_YELLOW), (40.0, 74.0, PAGE_YELLOW), (76.0, 74.0, PAGE_YELLOW)]); + let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0); + assert!(!read, "{notes:?}"); + assert_eq!(names(&doc), ["Layer 1"]); + assert!(notes.iter().any(|n| n.contains("differs from the page's")), "{notes:?}"); +} + +#[test] +fn trusted_editing_data_keeps_a_differing_object() { + let (art, page) = middle_differs(); + let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, true, 1.0); + assert!(read, "{notes:?}"); + assert_eq!(names(&doc), ["Back", "Front"]); + assert_eq!(kinds(&doc.layers[1]), ["path", "path"]); + assert!(notes.iter().any(|n| n.ends_with("they come from its editing data all the same")), "{notes:?}"); + assert!(!notes.iter().any(|n| n.starts_with("objects that drew")), "{notes:?}"); +} + +#[test] +fn a_differing_object_on_an_unnamed_layer_is_counted_under_its_panel_name() { + let front = format!("{}\n{}", square_16(40, BLACK), square_16(76, YELLOW)); + let art = native(&(layer("Back", true, &square_16(4, YELLOW)) + &layer("", true, &front))); + let page = page_16(&[(4.0, 74.0, PAGE_YELLOW), (40.0, 74.0, PAGE_CYAN), (76.0, 74.0, PAGE_YELLOW)]); + let (_, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0); + assert!(read, "{notes:?}"); + let note = "objects that drew differently from the file's own page came in as its art: (1)"; + assert!(notes.iter().any(|n| n == note), "{notes:?}"); +} + +#[test] +fn at_most_64_differing_objects_come_in_as_the_page_art() { + // 130 squares of 4 points, the first `black` of them black in the layers and cyan on the page. + let grid = |black: usize| { + let at = |i: usize| (4 + 7 * (i % 13) as u32, 4 + 7 * (i / 13) as u32); + let art: String = (0..130) + .map(|i| { + let (x, y) = at(i); + if i < black { format!("{BLACK} k\n{}", square_path(x, y)) } else { square(x, y) } + }) + .collect::>() + .join("\n"); + let mut page = Document::new(100.0, 100.0); + let layer_id = page.layers[0].id; + for i in 0..130 { + let (x, y) = at(i); + let (x, y) = (f64::from(x), 96.0 - f64::from(y)); + let path = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(x, y, x + 4.0, y + 4.0)); + let color = + if i < black { vectorcraft_color::Color::cmyk(1.0, 0.0, 0.0, 0.0) } else { vectorcraft_color::Color::cmyk(0.0, 0.0, 1.0, 0.0) }; + let item = vectorcraft_doc::AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(vectorcraft_color::Paint::solid(color))); + let n = Node::path(page.alloc_id(), path, vectorcraft_doc::Appearance { items: vec![item], ..Default::default() }); + page.insert(Some(layer_id), i, n).unwrap(); + } + crate::layered_ai(&private(&native(&layer("Grid", true, &art))), page, vec![], false, false, 1.0) + }; + let (_, notes, read) = grid(64); + assert!(read, "{notes:?}"); + let note = "objects that drew differently from the file's own page came in as its art: Grid (64)"; + assert!(notes.iter().any(|n| n == note), "{notes:?}"); + let (doc, notes, read) = grid(65); + assert!(!read, "{notes:?}"); + assert_eq!(names(&doc), ["Layer 1"]); + assert!(!notes.iter().any(|n| n.starts_with("objects that drew")), "{notes:?}"); +} + +#[test] +fn a_differing_object_under_the_type_of_a_text_object_that_stays_falls_back() { + // A black rectangle under area type whose frame reaches past it; the page draws the rectangle + // cyan, with the type on it. + let big = format!("{BLACK} k\n10 2 m\n60 2 L\n60 40 L\n10 40 L\nf"); + let doc = document_of(&[area_frame(34.0, 26.0, 95.0, 38.0)], &[story("Hi\\r", &[0], "/0 0", "/1 8.0")]); + let art = native_with_text(&(layer("Art", true, &format!("{big}\n{}", frame_object(0, 0))) + &layer("Top", true, &square_16(76, YELLOW))), &doc); + let mut page = page_16(&[(76.0, 74.0, PAGE_YELLOW)]); + let layer_id = page.layers[0].id; + let path = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(10.0, 60.0, 60.0, 98.0)); + let paint = vectorcraft_color::Paint::solid(vectorcraft_color::Color::cmyk(1.0, 0.0, 0.0, 0.0)); + let item = vectorcraft_doc::AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(paint)); + let square = Node::path(page.alloc_id(), path, vectorcraft_doc::Appearance { items: vec![item], ..Default::default() }); + page.insert(Some(layer_id), 0, square).unwrap(); + let style = vectorcraft_doc::CharStyle { size: 8.0, ..Default::default() }; + let text = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(34.0, 69.0), "Hi", style); + let text = Node::new(page.alloc_id(), NodeKind::Text(Box::new(text))); + page.insert(Some(layer_id), 1, text).unwrap(); + let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0); + assert!(!read, "{notes:?}"); + assert_eq!(names(&doc), ["Layer 1"]); + assert!(!notes.iter().any(|n| n.starts_with("objects that drew")), "{notes:?}"); +} + #[test] fn unread_object_on_second_artboard_takes_second_page_art() { let boards = "%_/Document :\n%_/Dictionary :\n%_/Array :\n\ diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 3bda3f6b8..576b657ec 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -6026,6 +6026,7 @@ breakText Glyphs Глифи Gap 5 Разстояние 5 @msg {_1}: they came in as the page's art {_1}: те са внесени като изображението от страницата +@msg objects that drew differently from the file's own page came in as its art: {_1} обектите, изобразени различно от собствената страница на файла, са внесени като изображението от страницата: {_1} @msg You must choose a side pattern. Трябва да изберете страничен шаблон. @msg invalid pattern tile choice невалиден избор на плочка за шаблон diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 20744b85e..fa9904149 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -6060,6 +6060,7 @@ shape Open Otvoren @msg the paths have nothing to merge there putanje tu nemaju šta spojiti @msg the path is locked or hidden putanja je zaključana ili skrivena @msg {_1}: they came in as the page's art {_1}: uvezeni su kao ilustracija stranice +@msg objects that drew differently from the file's own page came in as its art: {_1} objekti koji su se iscrtali drugačije od vlastite stranice datoteke uvezeni su kao ilustracija stranice: {_1} @msg You must choose a side pattern. Morate odabrati bočni uzorak. @msg invalid pattern tile choice nevažeći izbor pločice uzorka diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 02ccc1939..0e21ff719 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -6158,6 +6158,7 @@ breakText Glyphs Glifs Gap 5 Separació 5 @msg {_1}: they came in as the page's art {_1}: han entrat com a art de la pàgina +@msg objects that drew differently from the file's own page came in as its art: {_1} els objectes que es dibuixaven diferent de la pàgina pròpia del fitxer han entrat com a art de la pàgina: {_1} @msg the JPEG 2000 file can't be read no es pot llegir el fitxer JPEG 2000 @msg the JPEG 2000 file has an unsupported color space or JPX content el fitxer JPEG 2000 té un espai de color o contingut JPX no compatible diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index aa81fbca3..bc3ccd20b 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6174,6 +6174,7 @@ breakText Glyphs Glyfy Gap 5 Mezera 5 @msg {_1}: they came in as the page's art {_1}: byly načteny jako kresba ze stránky +@msg objects that drew differently from the file's own page came in as its art: {_1} objekty, které se vykreslily jinak než vlastní stránka souboru, byly načteny jako kresba ze stránky: {_1} @msg You must choose a side pattern. Musíte zvolit boční vzorek. @msg invalid pattern tile choice neplatná volba dlaždice vzorku diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 8556ae44e..4a2c248dc 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -6042,6 +6042,7 @@ breakText Glyphs Glyffer Gap 5 Mellemrum 5 @msg {_1}: they came in as the page's art {_1}: de blev importeret som sidens grafik +@msg objects that drew differently from the file's own page came in as its art: {_1} objekter, der blev tegnet anderledes end filens egen side, blev importeret som sidens grafik: {_1} @msg You must choose a side pattern. Du skal vælge et sidemønster. @msg invalid pattern tile choice ugyldigt valg af mønsterflise diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 0e1bf7ece..c9955c72b 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5760,6 +5760,7 @@ breakText Glyphs Glyphen Gap 5 Lücke 5 @msg {_1}: they came in as the page's art {_1}: sie wurden als Seitengrafik importiert +@msg objects that drew differently from the file's own page came in as its art: {_1} Objekte, die anders als die eigene Seite der Datei gezeichnet wurden, wurden als Seitengrafik importiert: {_1} @msg You must choose a side pattern. Sie müssen ein Seitenmuster auswählen. @msg invalid pattern tile choice ungültige Auswahl einer Musterkachel diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 01471ba16..67b9a7317 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6126,6 +6126,7 @@ breakText Glyphs Γλύφοι Gap 5 Διάκενο 5 @msg {_1}: they came in as the page's art {_1}: εισήχθησαν ως γραφικά της σελίδας +@msg objects that drew differently from the file's own page came in as its art: {_1} τα αντικείμενα που σχεδιάστηκαν διαφορετικά από την ίδια τη σελίδα του αρχείου εισήχθησαν ως γραφικά της σελίδας: {_1} @msg You must choose a side pattern. Πρέπει να επιλέξετε πλευρικό μοτίβο. @msg invalid pattern tile choice μη έγκυρη επιλογή πλακιδίου μοτίβου diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index b68ba40d6..09ec906bc 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -6116,6 +6116,7 @@ breakText Glyphs Glifos Gap 5 Hueco 5 @msg {_1}: they came in as the page's art {_1}: se importaron como el arte de la página +@msg objects that drew differently from the file's own page came in as its art: {_1} los objetos que se dibujaban de forma distinta a la página del propio archivo se importaron como el arte de la página: {_1} @msg You must choose a side pattern. Debe elegir un motivo lateral. @msg invalid pattern tile choice selección de mosaico de motivo no válida diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index e44dac5b1..ad10d0875 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6204,6 +6204,7 @@ breakText Glyphs Merkit Gap 5 Väli 5 @msg {_1}: they came in as the page's art {_1}: ne tuotiin sivun piirroksena +@msg objects that drew differently from the file's own page came in as its art: {_1} objektit, jotka piirtyivät eri tavoin kuin tiedoston oma sivu, tuotiin sivun piirroksena: {_1} @msg You must choose a side pattern. Valitse sivukuvio. @msg invalid pattern tile choice virheellinen kuviolaatan valinta diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index e435b22b8..18dffaeb4 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6169,6 +6169,7 @@ breakText Glyphs Glyphes Gap 5 Écart 5 @msg {_1}: they came in as the page's art {_1} : ils ont été importés comme le dessin de la page +@msg objects that drew differently from the file's own page came in as its art: {_1} les objets dessinés différemment de la page du fichier lui-même ont été importés comme le dessin de la page : {_1} @msg You must choose a side pattern. Vous devez choisir un motif latéral. @msg invalid pattern tile choice choix de tuile de motif non valide diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 1b6037294..fb1f9c7bc 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6158,6 +6158,7 @@ breakText Glyphs Glifák Gap 5 Hézag 5 @msg {_1}: they came in as the page's art {_1}: az oldal rajzaként lettek importálva +@msg objects that drew differently from the file's own page came in as its art: {_1} a fájl saját oldalától eltérően rajzolt objektumok az oldal rajzaként lettek importálva: {_1} @msg You must choose a side pattern. Válasszon oldalsó mintát. @msg invalid pattern tile choice érvénytelen mintacsempe-választás diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 3e784fe33..abf75d965 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -6042,6 +6042,7 @@ breakText Glyphs Glif Gap 5 Jarak 5 @msg {_1}: they came in as the page's art {_1}: diimpor sebagai gambar halaman +@msg objects that drew differently from the file's own page came in as its art: {_1} objek yang tergambar berbeda dari halaman file itu sendiri diimpor sebagai gambar halaman: {_1} @msg You must choose a side pattern. Anda harus memilih pola sisi. @msg invalid pattern tile choice pilihan ubin pola tidak valid diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index f51eb524a..9d61d8c3e 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5794,6 +5794,7 @@ breakText Glyphs Glifi Gap 5 Spazio 5 @msg {_1}: they came in as the page's art {_1}: sono stati importati come elementi grafici della pagina +@msg objects that drew differently from the file's own page came in as its art: {_1} gli oggetti disegnati in modo diverso dalla pagina del file stesso sono stati importati come elementi grafici della pagina: {_1} @msg You must choose a side pattern. Devi scegliere un motivo laterale. @msg invalid pattern tile choice scelta della tessera motivo non valida diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 8630535a5..50241341d 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5859,6 +5859,7 @@ breakText Glyphs 字形 Gap 5 間隔 5 @msg {_1}: they came in as the page's art {_1}: ページのアートとして読み込みました +@msg objects that drew differently from the file's own page came in as its art: {_1} ファイル自身のページと描画が異なるオブジェクトをページのアートとして読み込みました: {_1} @msg You must choose a side pattern. 側面パターンを選択してください。 @msg invalid pattern tile choice パターンタイルの選択が無効です diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 6cf5cdb6b..112dfa657 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6162,6 +6162,7 @@ breakText Glyphs გლიფები Gap 5 შუალედი 5 @msg {_1}: they came in as the page's art {_1}: იმპორტირებულია გვერდის გამოსახულების სახით +@msg objects that drew differently from the file's own page came in as its art: {_1} ობიექტები, რომლებიც ფაილის საკუთარი გვერდისგან განსხვავებულად იხატებოდა, იმპორტირებულია გვერდის გამოსახულების სახით: {_1} @msg You must choose a side pattern. აირჩიეთ გვერდითი ნიმუში. @msg invalid pattern tile choice ნიმუშის ფილა არასწორად არის არჩეული diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 338be6dce..4ad48aa61 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5755,6 +5755,7 @@ breakText Glyphs 글리프 Gap 5 간격 5 @msg {_1}: they came in as the page's art {_1}: 페이지의 아트로 가져왔습니다 +@msg objects that drew differently from the file's own page came in as its art: {_1} 파일 자체 페이지와 다르게 그려진 오브젝트를 페이지의 아트로 가져왔습니다: {_1} @msg You must choose a side pattern. 측면 패턴을 선택해야 합니다. @msg invalid pattern tile choice 잘못된 패턴 타일 선택 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 263545d27..5096aa81c 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -6086,6 +6086,7 @@ breakText Glyphs Glyphs Gap 5 Tussenruimte 5 @msg {_1}: they came in as the page's art {_1}: ze zijn geïmporteerd als pagina-afbeelding +@msg objects that drew differently from the file's own page came in as its art: {_1} objecten die anders werden getekend dan de eigen pagina van het bestand, zijn geïmporteerd als pagina-afbeelding: {_1} @msg You must choose a side pattern. Kies een zijpatroon. @msg invalid pattern tile choice ongeldige keuze voor patroontegel diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 4cb23d6ac..d431ce0fb 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -6029,6 +6029,7 @@ breakText Glyphs Glify Gap 5 Odstęp 5 @msg {_1}: they came in as the page's art {_1}: zaimportowano je jako grafikę strony +@msg objects that drew differently from the file's own page came in as its art: {_1} obiekty rysowane inaczej niż własna strona pliku zaimportowano jako grafikę strony: {_1} @msg You must choose a side pattern. Musisz wybrać wzór boczny. @msg invalid pattern tile choice nieprawidłowy wybór kafelka wzoru diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index f53f0798e..b726d9ddb 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -6092,6 +6092,7 @@ breakText Glyphs Glifos Gap 5 Lacuna 5 @msg {_1}: they came in as the page's art {_1}: foram importados como arte da página +@msg objects that drew differently from the file's own page came in as its art: {_1} os objetos desenhados de forma diferente da própria página do arquivo foram importados como arte da página: {_1} @msg You must choose a side pattern. Você deve escolher um padrão lateral. @msg invalid pattern tile choice escolha inválida de ladrilho de padrão diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index c5a493ad8..0cb2785ac 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -6151,6 +6151,7 @@ breakText Glyphs Глифы Gap 5 Зазор 5 @msg {_1}: they came in as the page's art {_1}: импортированы как графика страницы +@msg objects that drew differently from the file's own page came in as its art: {_1} объекты, которые отрисовывались иначе, чем собственная страница файла, импортированы как графика страницы: {_1} @msg You must choose a side pattern. Выберите боковой узор. @msg invalid pattern tile choice недопустимый выбор плитки узора diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 77d5c41b6..4fdd6abf2 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6226,6 +6226,7 @@ breakText Glyphs Glyfer Gap 5 Mellanrum 5 @msg {_1}: they came in as the page's art {_1}: de importerades som sidans bild +@msg objects that drew differently from the file's own page came in as its art: {_1} objekt som ritades annorlunda än filens egen sida importerades som sidans bild: {_1} @msg You must choose a side pattern. Du måste välja ett sidomönster. @msg invalid pattern tile choice ogiltigt val av mönsterplatta diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index efbcf6934..ac0931554 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6131,6 +6131,7 @@ breakText Glyphs Glifler Gap 5 Boşluk 5 @msg {_1}: they came in as the page's art {_1}: sayfanın çizimi olarak içe aktarıldılar +@msg objects that drew differently from the file's own page came in as its art: {_1} dosyanın kendi sayfasından farklı çizilen nesneler sayfanın çizimi olarak içe aktarıldı: {_1} @msg You must choose a side pattern. Bir yan desen seçmelisiniz. @msg invalid pattern tile choice geçersiz desen döşemesi seçimi diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index b7d8962a9..a7febfd3a 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5774,6 +5774,7 @@ breakText Glyphs Гліфи Gap 5 Проміжок 5 @msg {_1}: they came in as the page's art {_1}: їх імпортовано як графіку сторінки +@msg objects that drew differently from the file's own page came in as its art: {_1} об’єкти, що малювалися інакше, ніж власна сторінка файлу, імпортовано як графіку сторінки: {_1} @msg You must choose a side pattern. Виберіть бічний візерунок. @msg invalid pattern tile choice недійсний вибір плитки візерунка diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 41a5d4c64..7c7d89b79 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5775,6 +5775,7 @@ breakText Glyphs Glyph Gap 5 Khoảng hở 5 @msg {_1}: they came in as the page's art {_1}: đã được nhập dưới dạng hình vẽ của trang +@msg objects that drew differently from the file's own page came in as its art: {_1} các đối tượng được vẽ khác với trang riêng của tệp đã được nhập dưới dạng hình vẽ của trang: {_1} @msg You must choose a side pattern. Bạn phải chọn mẫu bên. @msg invalid pattern tile choice lựa chọn ô mẫu không hợp lệ diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 8e383be64..2b70c03aa 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -6015,6 +6015,7 @@ breakText Glyphs 字形 Gap 5 间隔 5 @msg {_1}: they came in as the page's art {_1}:已作为页面图稿导入 +@msg objects that drew differently from the file's own page came in as its art: {_1} 与文件自身页面绘制不同的对象已作为页面图稿导入:{_1} @msg You must choose a side pattern. 必须选择侧边图案。 @msg invalid pattern tile choice 图案拼块选项无效 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index e401ee090..26e592e8f 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5812,6 +5812,7 @@ breakText Glyphs 字符 Gap 5 間隙 5 @msg {_1}: they came in as the page's art {_1}:已作為頁面圖稿匯入 +@msg objects that drew differently from the file's own page came in as its art: {_1} 與檔案本身頁面繪製不同的物件已作為頁面圖稿匯入:{_1} @msg You must choose a side pattern. 必須選擇側邊圖樣。 @msg invalid pattern tile choice 圖樣拼塊選項無效 diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 6f02703ca..be0bc3596 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -113,7 +113,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, 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), 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. 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 or Convert to Shape 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, 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, 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; 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), 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. 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 or Convert to Shape 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, 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, 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). 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). 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