From 618b1cf94f8b118322c02588e5156305bb67c489 Mon Sep 17 00:00:00 2001 From: Matthew Farrow Date: Thu, 8 Oct 2026 21:38:33 -0400 Subject: [PATCH] EPS import: a commented copy of a text object that has a plain one isn't another object Some files write a text object twice, once plainly and once as comments (and others only as comments). Reading both made a second copy of the type, built from the text document, over the page's own (wrong size and place). The commented copy is now skipped when its story has a plain text object. Co-Authored-By: Claude Sonnet 5.5 --- crates/eps/src/import/native.rs | 33 ++++++++++++++++++++++++--- crates/eps/src/import/tests_native.rs | 12 ++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/crates/eps/src/import/native.rs b/crates/eps/src/import/native.rs index c2c8da574..863ea989b 100644 --- a/crates/eps/src/import/native.rs +++ b/crates/eps/src/import/native.rs @@ -266,6 +266,16 @@ fn unzstd(packed: &[u8]) -> Option> { (data.len() as u64 != MAX_DECODED).then_some(data) } +/// The stories that text objects written plainly (not as comments) name. +fn plain_stories(data: &[u8]) -> BTreeSet { + data.split(|b| matches!(b, b'\r' | b'\n')) + .filter_map(|raw| { + let line = String::from_utf8_lossy(raw); + line.trim().strip_suffix("/StoryIndex ,").and_then(|n| n.trim().parse().ok()) + }) + .collect() +} + /// The layers of the editing copy `data`. fn parse(data: &[u8]) -> Option { let (mut bbox, mut hires, mut artboard, mut template) = (None, None, None, None); @@ -274,6 +284,10 @@ fn parse(data: &[u8]) -> Option { let mut dict_depth = 0usize; // In a text object's dictionary: the story it names, once the dictionary says. let mut text_story: Option> = None; + // Whether that text object is written as comments, which the file does for some of its stories + // twice (once plain, once commented) and for others only commented. + let mut text_commented = false; + let plain = plain_stories(data); let mut count = 0usize; for raw in data.split(|b| matches!(b, b'\r' | b'\n')) { let line = String::from_utf8_lossy(raw); @@ -284,8 +298,18 @@ fn parse(data: &[u8]) -> Option { // A text object's dictionary may be written as comments (`%_`), which a reader that doesn't // know text objects skips: what that reader skips is read here, for the text object only. let line = match line.strip_prefix("%_") { - Some(rest) if dict_depth > 0 || rest.starts_with("/AI11Text") && rest.ends_with(':') => rest.trim(), - _ => line, + Some(rest) if dict_depth > 0 || rest.starts_with("/AI11Text") && rest.ends_with(':') => { + if dict_depth == 0 { + text_commented = true; + } + rest.trim() + } + _ => { + if dict_depth == 0 { + text_commented = false; + } + line + } }; if line.starts_with('%') { match line { @@ -331,7 +355,10 @@ fn parse(data: &[u8]) -> Option { if dict_depth == 0 && let Some(story) = text_story.take() { - layer.code.push_str(&format!("{} __txt\n", story.map_or(-1, i64::from))); + // A commented copy of a story that has a plain text object is not another object. + if !(text_commented && story.is_some_and(|n| plain.contains(&n))) { + layer.code.push_str(&format!("{} __txt\n", story.map_or(-1, i64::from))); + } } } else if line.ends_with(':') { dict_depth += 1; diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index 414d51aeb..3eb6f5714 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -436,3 +436,15 @@ fn type_of_the_page_that_no_story_places_stays_with_the_nearest_text_object() { }); assert!(all.contains(&"Extra".to_string()), "{all:?} {:?}", r.warnings); } + +#[test] +fn a_commented_copy_of_a_story_that_has_a_plain_text_object_is_not_another_object() { + // The file writes some stories twice (plain, then as comments) and others only as comments. + let doc = text_document(&[("One\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0), ("Two\r", 0, (8300.0, 8180.0), (0.0, 0.0), 0.0)]); + let commented = |story| -> String { text_object(story).lines().map(|l| format!("%_{l}\n")).collect() }; + let body = text_object(0) + &commented(0) + &commented(1); + let art = native_with_text(&layer("Spare", false, &body), &doc); + let r = import(&eps(&page_square(10, 10), &art)).unwrap(); + let texts: Vec = text_of(&r.document.layers[0]).iter().map(|t| t.plain_text()).collect(); + assert_eq!(texts, ["One", "Two"], "{:?}", r.warnings); +}