EPS import: leave out text objects that the page doesn't draw

Type turned to outlines leaves its text object in the file, which the app that
wrote it doesn't draw (it paints every shown text object on the page). A text
object made from the file's text document that shows on the page's area where the
page has no type, and without which the layers look no worse against the page, is
left out, with a note.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Matthew Farrow
2026-10-09 03:49:49 -04:00
co-authored by Claude Sonnet 5.5
parent 94dfa20fa0
commit 2f0ede9c44
2 changed files with 101 additions and 1 deletions
+83 -1
View File
@@ -656,6 +656,7 @@ fn fill_slots(doc: &mut Document, nodes: &mut Vec<Arc<Node>>, 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<Node>], shown: bool, found: &mut Vec<(NodeId, Option<Rect>)>) {
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<Arc<Node>>, 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<Option<String>, 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<String>) -> (
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))
});
+18
View File
@@ -454,3 +454,21 @@ fn a_commented_copy_of_a_story_that_has_a_plain_text_object_is_not_another_objec
let texts: Vec<String> = 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);
}