diff --git a/crates/eps/src/import/native.rs b/crates/eps/src/import/native.rs index e6b8edeca..7045404b4 100644 --- a/crates/eps/src/import/native.rs +++ b/crates/eps/src/import/native.rs @@ -656,6 +656,7 @@ fn fill_slots(doc: &mut Document, nodes: &mut Vec>, content: &mut std: Some(Content::Made(mut text)) => { text.id = doc.alloc_id(); text.visible = visible; + text.name = Some(MADE.into()); Some(*text) } Some(Content::Merged) => { @@ -1044,6 +1045,79 @@ fn build(data: &[u8], visible: &Document, base: Document, page: Page) -> Result< Ok(Built { out, layers }) } +/// Marks (in a node's name, until [`prune_unseen`] has looked) the text objects made from the file's +/// text document. +const MADE: &str = "\u{1}made"; +/// How many such objects are weighed against the page (each takes a render of the page's size). +const MAX_WEIGHED: usize = 12; +/// The text objects the file keeps that the page doesn't draw. +const UNSEEN_TEXT: &str = "text objects that the file keeps but its page doesn't draw (what is left of type turned to outlines) are left out"; +/// How much more of the page (0 to 1) the layers may differ without a text object and still look no worse. +const UNSEEN_MARGIN: f64 = 5e-5; + +/// Clear the marks in `nodes` and say which marked objects show, by id. +fn unmark(nodes: &mut [Arc], shown: bool, found: &mut Vec<(NodeId, Option)>) { + for n in nodes { + let shown = shown && n.visible; + if n.name.as_deref() == Some(MADE) { + if shown { + found.push((n.id, n.visual_bounds())); + } + Arc::make_mut(n).name = None; + } else if !matches!(n.kind, NodeKind::Compound { .. }) + && let Some(c) = Arc::make_mut(n).children_mut() + { + unmark(c, shown, found); + } + } +} + +/// Take object `id` out of `nodes` (`hide`: leave it, hidden); whether it was there. +fn take_out(nodes: &mut Vec>, id: NodeId, hide: bool) -> bool { + if let Some(i) = nodes.iter().position(|n| n.id == id) { + if hide { + if let Some(n) = nodes.get_mut(i) { + Arc::make_mut(n).visible = false; + } + } else { + nodes.remove(i); + } + return true; + } + nodes.iter_mut().any(|n| !matches!(n.kind, NodeKind::Compound { .. }) && Arc::make_mut(n).children_mut().is_some_and(|c| take_out(c, id, hide))) +} + +/// The text objects made from the file's text document that show on the page's area, where the page +/// has no type to match them, and that the layers look no worse without, are not drawn by the app +/// that wrote the file (which paints every shown text object; it keeps the type of what was turned +/// to outlines): they are taken out. How many. +fn prune_unseen(page: &Document, doc: &mut Document) -> usize { + let mut found = vec![]; + unmark(&mut doc.layers, true, &mut found); + let Some(rect) = page.artboards.first().map(|a| a.rect) else { return 0 }; + if found.is_empty() || found.len() > MAX_WEIGHED { + return 0; + } + let mut with = difference(page, doc, rect); + let mut removed = 0; + for (id, bounds) in found { + if !bounds.is_some_and(|b| !b.intersect(rect).is_zero_area()) { + continue; + } + let mut without = doc.clone(); + if !take_out(&mut without.layers, id, true) { + continue; + } + let d = difference(page, &without, rect); + if d - with <= UNSEEN_MARGIN { + take_out(&mut doc.layers, id, false); + with = d; + removed += 1; + } + } + removed +} + /// Do the layers' art and the page's own look alike? An error says they don't; `Ok` has the note /// to give when they differ a little. fn compare(page: &Document, layered: &Document) -> Result, String> { @@ -1070,6 +1144,10 @@ pub(super) fn layered(ps: &[u8], visible: Imported) -> Imported { let base = Document::new(page.width(), page.height()); let result = build(&data, &visible.document, base, Page::Art).and_then(|built| { let mut done = finish_with(built.out, built.layers); + let unseen = prune_unseen(&visible.document, &mut done.document); + if unseen > 0 { + done.warnings.push(format!("{unseen} {UNSEEN_TEXT}")); + } let note = compare(&visible.document, &done.document)?; done.warnings.extend(note); Ok(done) @@ -1100,7 +1178,11 @@ pub fn layered_ai(private: &[u8], visible: Document, warnings: Vec) -> ( let Some(data) = decode_ai(private) else { return (visible, warnings) }; let result = build(&data, &visible, visible.clone(), Page::Artboard).and_then(|built| { let mut notes = built.out.warnings.clone(); - let done = finish_layers(built.out, built.layers); + let mut done = finish_layers(built.out, built.layers); + let unseen = prune_unseen(&visible, &mut done); + if unseen > 0 { + notes.push(format!("{unseen} {UNSEEN_TEXT}")); + } notes.extend(compare(&visible, &done)?); Ok((done, notes)) }); diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index 93a00a5a0..e1d2708b4 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -454,3 +454,21 @@ fn a_commented_copy_of_a_story_that_has_a_plain_text_object_is_not_another_objec let texts: Vec = text_of(&r.document.layers[0]).iter().map(|t| t.plain_text()).collect(); assert_eq!(texts, ["One", "Two"], "{:?}", r.warnings); } + +#[test] +fn a_shown_text_object_on_the_page_that_the_page_doesnt_draw_is_left_out() { + // Story 1 is at (58.5, 61.5) on the 100 × 100 page, where the page paints no type: the file + // keeps it but the app that wrote it doesn't draw it (type turned to outlines leaves one). + let doc = text_document(&[("Hi there\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0), ("Ghost\r", 0, (8200.0, 8200.0), (0.0, 0.0), 0.0)]); + let art = native_with_text(&layer("Art", true, &(text_object(0) + &text_object(1))), &doc); + let page = "0 0 0 1 setcmykcolor /Helvetica findfont 14 scalefont setfont 58.5 61.5 moveto (Hi there) show"; + let r = import(&eps(page, &art)).unwrap(); + let mut all = vec![]; + r.document.walk(|n| { + if let NodeKind::Text(t) = &n.kind { + all.push(t.plain_text()); + } + }); + assert_eq!(all, ["Hi there"], "{:?}", r.warnings); + assert!(r.warnings.iter().any(|w| w.contains("doesn't draw")), "{:?}", r.warnings); +}