fix(eps-import): read layers whose type is in the legacy format beside a page with text

An EPS saved for Illustrator 10 keeps its type in the editing data as
legacy text objects (`To` ... `TO`), with no `/AI11Text` slot for the
page's type to go in, so a file whose page had text came in as its page.
Those text objects are now the layers' own: the page's type isn't put
anywhere, and the comparison with the page decides as before. Type the
editing data keeps only as outlines stays outlines, with a note counting
the page's text objects that are.
This commit is contained in:
Don Omondi
2026-10-31 03:34:40 +07:00
committed by bflatastic
parent 9de15d6599
commit bfe509bb38
3 changed files with 104 additions and 10 deletions
+75 -9
View File
@@ -2,7 +2,7 @@ use super::compare::{MAX_DIFFERENCE, PageView, compare};
use super::plan::plan;
use super::slots::{
ART_LEFT_OUT, Content, MAX_PAIRS, MAX_SLOTS, MAX_TEXT, OUTLINE_REACH, TEXT_LEFT_OUT, TYPE_LEFT_OUT, collect_slots, extent, few, fill_slots,
text_objects,
letters, text_objects, words,
};
use super::{Affine, Arc, BTreeSet, Document, Node, NodeId, NodeKind, PathData, Rect, Scaling, Story, Structure, Texts, Unit, Vec2, ai};
@@ -178,14 +178,9 @@ pub(super) fn build_from(
if outlined && infos.iter().any(|i| i.shown) {
return Err("its type is to come in as outlines, which only its page has".into());
}
if infos.iter().all(|i| !i.shown) && !shown.is_empty() {
if !in_placed_files(&shown, &doc, &placed_elsewhere) {
return Err("its page has text, and its layers have no text objects to put it in".into());
}
// The page's type is all inside placed files: it is theirs, which their previews show, and
// needs no text object.
shown.clear();
}
// The page's type that needs no text object of the layers: the layers' own legacy text, or
// placed files that show it.
let as_outlines = page_type_elsewhere(infos.is_empty(), infos.iter().all(|i| !i.shown), &mut shown, &mut hidden, &doc, &placed_elsewhere)?;
// The page's type is compared with every text object of the layers: that has its limits.
if infos.len() > MAX_SLOTS || infos.len().saturating_mul(shown.len()) > MAX_PAIRS {
return Err("it has too many text objects".into());
@@ -233,6 +228,9 @@ pub(super) fn build_from(
if short > 0 {
notes.push(format!("{short} {PAGE_TYPE_WITHOUT_OUTLINES}"));
}
if as_outlines > 0 {
notes.push(format!("{as_outlines} {PAGE_TYPE_AS_OUTLINES}"));
}
if !hidden_unread.is_empty() {
notes.push(format!("{ART_LEFT_OUT} ({}), left out of them", few(hidden_unread)));
}
@@ -304,6 +302,71 @@ fn elsewhere(doc: &Document, placed: &[(NodeId, Affine)]) -> Option<Document> {
Some(doc)
}
/// The page's type that needs no text object of the layers, taken out of `shown` and `hidden` (the
/// page's text objects that show and that don't): how many of them the layers have as outlines.
/// Type in the legacy format (`To` … `TO`, as files saved for Illustrator 10 and earlier keep it)
/// is read as text objects of the layers' own, with no slot (`no_slots`) for the page's type; the
/// page's type that their words don't hold is outlines on the layers, and the comparison with the
/// page says whether they draw it. Without such text and with no slot that shows (`none_shown`),
/// the page's type must all lie inside placed files, which show it.
fn page_type_elsewhere(
no_slots: bool,
none_shown: bool,
shown: &mut Vec<Vec<Arc<Node>>>,
hidden: &mut Vec<Vec<Arc<Node>>>,
doc: &Document,
placed: &[(NodeId, Affine)],
) -> Result<usize, String> {
let mut own = vec![];
own_text(&doc.layers, &mut own);
let mut as_outlines = 0;
if no_slots && !own.is_empty() {
as_outlines = shown.iter().filter(|o| !in_own_text(o, &own)).count();
shown.clear();
hidden.clear();
}
if none_shown && !shown.is_empty() && own.is_empty() {
if !in_placed_files(shown, doc, placed) {
return Err("its page has text, and its layers have no text objects to put it in".into());
}
// The page's type is all inside placed files: it is theirs, which their previews show, and
// needs no text object.
shown.clear();
}
Ok(as_outlines)
}
/// Is the page's text object `object` the layers' own text (`own`, from [`own_text`]): one with its
/// words next to it? The rest of the page's type is what the editing data keeps as outlines (what
/// the legacy format can't hold), and the comparison with the page says whether the layers draw it.
fn in_own_text(object: &[Arc<Node>], own: &[(Rect, String)]) -> bool {
let Some(r) = extent(object) else { return false };
let near: String =
own.iter().filter(|(b, _)| !b.inflate(OUTLINE_REACH, OUTLINE_REACH).intersect(r).is_zero_area()).map(|(_, l)| l.as_str()).collect();
let page_words: Vec<String> = object
.iter()
.filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some(words(&t.plain_text().to_lowercase())) } else { None })
.flatten()
.collect();
!near.is_empty() && page_words.iter().all(|w| near.contains(w.as_str()))
}
/// The text objects of their own (not slots for the page's type) that show in `nodes`: their
/// bounds and their letters, in lower case.
fn own_text(nodes: &[Arc<Node>], out: &mut Vec<(Rect, String)>) {
for n in nodes.iter().filter(|n| n.visible) {
match &n.kind {
NodeKind::Text(t) => out.extend(n.visual_bounds().map(|b| (b, letters(&t.plain_text().to_lowercase())))),
NodeKind::Compound { .. } => {}
_ => {
if let Some(c) = n.children() {
own_text(c, out);
}
}
}
}
}
/// Has `r` the size of `page`'s first artboard (in points)?
fn same_size(r: Option<Rect>, page: &Document) -> bool {
let (Some(r), Some(p)) = (r, page.artboards.first().map(|a| a.rect)) else { return false };
@@ -378,6 +441,9 @@ const UNSEEN_TEXT: &str = "text objects that the file keeps but its page doesn't
/// left out.
const PAGE_TYPE_WITHOUT_OUTLINES: &str = "text objects hold their type as the page draws it, without the characters the page has only as outlines";
/// The page's text objects that the layers have as outlines, as the editing data keeps them.
const PAGE_TYPE_AS_OUTLINES: &str = "text objects of the page are outlines on the layers, as the file's editing data keeps them";
/// 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;
+27
View File
@@ -192,6 +192,33 @@ fn text_and_its_strokes_come_from_the_page_into_the_layer_of_its_text_object() {
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
}
/// A point type object in the legacy format (`To` … `TO`), as an EPS saved for Illustrator 10
/// keeps its type in the editing data: no `/AI11Text` for the page's type to go in.
fn legacy_text(x: u32, y: u32, text: &str) -> String {
format!("0 To\n1 0 0 1 {x} {y} 0 Tp\nTP\n0 Tr\n0 0 0 1 k\n/_Helvetica 12 Tf\n({text}) Tx\nTO")
}
#[test]
fn type_in_the_legacy_format_is_the_layers_own_and_the_page_has_text() {
let page = format!("{}\n0 0 0 1 setcmykcolor /Helvetica findfont 12 scalefont setfont 10 50 moveto (Hi) show", page_square(10, 10));
let art = native(&(layer("Art", true, &square(10, 10)) + &layer("Words", true, &legacy_text(10, 50, "Hi"))));
let r = import(&eps(&page, &art)).unwrap();
assert_eq!(names(&r.document), ["Art", "Words"], "{:?}", r.warnings);
let texts = text_of(&r.document.layers[1]);
assert_eq!(texts.len(), 1, "{:?}", r.warnings);
assert_eq!(texts[0].plain_text(), "Hi");
assert!(!r.warnings.iter().any(|w| w.contains("left out") || w.contains("weren't read")), "{:?}", r.warnings);
// Type the editing data keeps as outlines (here a stand-in, a small square where the page has
// more type) comes in as they are, and the import says so.
let page = format!("{page}\n/Helvetica findfont 4 scalefont setfont 60 20 moveto (o) show");
let outlines = format!("{}\n0 0 0 1 k\n60.5 20 m\n62.5 20 L\n62.5 22 L\n60.5 22 L\nf", legacy_text(10, 50, "Hi"));
let art = native(&(layer("Art", true, &square(10, 10)) + &layer("Words", true, &outlines)));
let r = import(&eps(&page, &art)).unwrap();
assert_eq!(names(&r.document), ["Art", "Words"], "{:?}", r.warnings);
assert_eq!(kinds(&r.document.layers[1]), ["text", "path"], "{:?}", r.warnings);
assert!(r.warnings.iter().any(|w| w.starts_with("1 text objects of the page are outlines on the layers")), "{:?}", r.warnings);
}
#[test]
fn a_damaged_editing_copy_leaves_the_page() {
// Not ASCII85, ASCII85 of something that isn't Zstandard, Zstandard of something that isn't art.