mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user