From 04806a98bf52b7bb48d3b172c00d8e54956a619a Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 15 Oct 2026 20:29:32 -0700 Subject: [PATCH] .ai import: text on a layer that doesn't print stays on that layer A `.ai` opened from its editing data lost a text object on a layer that shows and has Print off when its page drew no type for it. prune_unseen weighs each text object made from the text document that the page's type doesn't match against the page, and the layers are drawn for that comparison as they print (as_printed), with such layers hidden. Taking one of these objects out never changed the comparison, so each was removed with the note "text objects that the file keeps but its page doesn't draw ... are left out". unmark now carries a print flag next to the shown flag, and a layer with printable off clears it for everything it holds. Only made text objects that show and print are weighed against the page; the others stay on their layer. Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/eps/src/import/native.rs | 21 +++++++++++---------- crates/eps/src/import/tests_native.rs | 18 ++++++++++++++++++ docs/mcp.md | 3 ++- docs/target-app-parity.md | 1 + 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/crates/eps/src/import/native.rs b/crates/eps/src/import/native.rs index c7f193c2b..465cec773 100644 --- a/crates/eps/src/import/native.rs +++ b/crates/eps/src/import/native.rs @@ -1005,7 +1005,7 @@ fn build(data: &[u8], visible: Option<&Document>, page: Page, outlined: bool, tr Err(why) => return Err(why), } } - None => unmark(&mut doc.layers, true, &mut vec![]), + None => unmark(&mut doc.layers, true, true, &mut vec![]), } doc.limit_depth_noting(&mut notes); if small.is_some() { @@ -1047,12 +1047,13 @@ const UNSEEN_TEXT: &str = "text objects that the file keeps but its page doesn't /// 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)>) { +/// Clear the marks in `nodes` and say which marked objects show and print, by id. +fn unmark(nodes: &mut [Arc], shown: bool, prints: bool, found: &mut Vec<(NodeId, Option)>) { for n in nodes { let shown = shown && n.visible; + let prints = prints && !matches!(n.kind, NodeKind::Layer { printable: false, .. }); if let Some(name) = n.name.as_deref().and_then(|m| m.strip_prefix(MADE)) { - if shown { + if shown && prints { found.push((n.id, n.visual_bounds())); } let name = (!name.is_empty()).then(|| name.to_string()); @@ -1060,7 +1061,7 @@ fn unmark(nodes: &mut [Arc], shown: bool, found: &mut Vec<(NodeId, Option< } else if !matches!(n.kind, NodeKind::Compound { .. }) && let Some(c) = Arc::make_mut(n).children_mut() { - unmark(c, shown, found); + unmark(c, shown, prints, found); } } } @@ -1080,13 +1081,13 @@ fn take_out(nodes: &mut Vec>, id: NodeId, hide: bool) -> bool { 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. +/// The text objects made from the file's text document that show and print 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(frame: &Frame<'_>, view: &mut PageView, doc: &mut Document) -> usize { let mut found = vec![]; - unmark(&mut doc.layers, true, &mut found); + unmark(&mut doc.layers, true, true, &mut found); let rect = frame.rect; if found.is_empty() || found.len() > MAX_WEIGHED { return 0; diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index 84217c4b2..b7f9a67fe 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -628,6 +628,24 @@ fn a_shown_text_object_on_the_page_that_the_page_doesnt_draw_is_left_out() { assert!(r.warnings.iter().any(|w| w.contains("doesn't draw")), "{:?}", r.warnings); } +/// A text object on a layer that shows and doesn't print, which the page doesn't draw, isn't taken +/// out: it comes in from the text document, on its layer. +#[test] +fn a_shown_text_object_on_a_layer_that_doesnt_print_stays_on_its_layer() { + let doc = text_document(&[("Hi there\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]); + // The layer's options with its fourth flag, printing, off. + let tips = layer("Tips", true, &text_object(0)).replace("1 1 1 1 0 0 1 0 79", "1 1 1 0 0 0 1 0 79"); + let art = native_with_text(&(layer("Art", true, &square(10, 10)) + &tips), &doc); + let (d, w, read) = crate::layered_ai(&private(&art), pdf_part(10.0, 86.0), vec![], false, false, 1.0); + assert!(read, "{w:?}"); + assert_eq!(names(&d), ["Art", "Tips"], "{w:?}"); + let tips = &d.layers[1]; + assert!(tips.visible && matches!(tips.kind, NodeKind::Layer { printable: false, .. })); + let texts: Vec = text_of(tips).iter().map(|t| t.plain_text()).collect(); + assert_eq!(texts, ["Hi there"], "{w:?}"); + assert!(!w.iter().any(|w| w.contains("doesn't draw")), "{w:?}"); +} + #[test] fn pieces_of_two_lines_the_page_draws_alternately_go_each_to_its_own_text() { // "Hello" is drawn as "He" and "llo", with "abcd" ("ab", "cd") drawn between them. diff --git a/docs/mcp.md b/docs/mcp.md index f9e0d2886..2581f7744 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -546,7 +546,8 @@ strokes, effects or a brush comes in as its drawn look (a group named after it). document, whole and editable: point type, area type (a story in several frames as threaded type) and type on a path, with its fonts, size, leading, tracking, scaling, baseline shift, fill and stroke (grey, RGB or CMYK), alignment, indents and paragraph spacing. Where the text document can't say, type that shows is the page's, in its text object's -place. A file whose editing data has symbols, +place. A text object on a layer that shows and doesn't print keeps the type the text document makes for it, on that +layer, even where the page doesn't draw it. A file whose editing data has symbols, pattern fills, placed files or anything else the reader doesn't read on a layer that shows, or whose layers look different from its page, opens as before: an EPS as its printed page, a `.ai` file from its PDF content (where plain groups open ungrouped), with a warning saying why. Layers look different when they draw an object the page doesn't (or diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 1eae3e6f3..280a2b6bd 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -578,6 +578,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur SVG import's style cascade, used for hidden objects, non-scaling strokes and type, tests each element only against the `