diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index f12e5953b..d12a569e3 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -1412,17 +1412,33 @@ impl<'a> Reader<'a> { return Ok(()); } let Some(f) = self.frames.pop() else { return Ok(()) }; - let Kind::Obj(mut o) = f.kind else { return Ok(()) }; - obj::close(&mut o, &mut self.stack, f.base); - self.obj_hidden = f.hidden; + let Frame { kind, children, base, hidden, gs, after, .. } = f; + let Kind::Obj(mut o) = kind else { return Ok(()) }; + obj::close(&mut o, &mut self.stack, base); + self.obj_hidden = hidden; // Art made inside a dictionary (type's paths) isn't kept. - self.gs = f.gs; - self.after_object = f.after; + self.gs = gs; + self.after_object = after; if matches!(self.frames.last(), Some(Frame { kind: Kind::Obj(_), .. })) { return self.push(V::Obj(Rc::new(*o))); } match o.ty.as_str() { - "ArtDictionary" => self.art_dictionary(&o), + "ArtDictionary" => { + let envelope = o.get("AIDeformOptionsKey").is_some(); + self.art_dictionary(&o); + if envelope { + let mut slots = vec![]; + for child in &children { + envelope_text_slots(child, &mut slots); + } + for mut slot in slots { + // The editing data keeps the source text inside the envelope's options; + // its visible result is the outlined art the PDF page carries. + slot.visible = false; + self.add(slot, false)?; + } + } + } "Document" => self.document(&o), "AI11Text" => self.text_slot(&o)?, "SymbolInstance" => self.unreadable("symbols"), @@ -2336,6 +2352,17 @@ impl<'a> Reader<'a> { } } +/// The source text inside an envelope options dictionary, carried through its containing metadata. +fn envelope_text_slots(node: &Node, out: &mut Vec) { + if slot_of(node.name.as_deref()).is_some() { + out.push(node.clone()); + } else if let Some(children) = node.children() { + for child in children { + envelope_text_slots(child, out); + } + } +} + /// The text objects written twice, plainly and on `%_` lines: the commented copy (`hidden`) of a /// story that also has a plain one goes. fn drop_commented_copies(layers: &mut [Arc], hidden: &HashSet) { diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index c0cd26c31..0e74cffa7 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -1332,3 +1332,23 @@ fn raster_print_page_recovers_legacy_mesh_vector_mask_and_editable_curve() { let r = import(wrong.as_bytes()).unwrap(); assert!(r.warnings.iter().any(|w| w.contains("for a 100 × 100 pt page, not the file's 120 × 100 pt page")), "{:?}", r.warnings); } + +#[test] +fn envelope_options_keep_the_source_text_hidden_under_its_outlined_appearance() { + let doc = text_document(&[("CHONKYCATS\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]); + let slot: String = text_object(0).lines().map(|line| format!("%_{line}\n")).collect(); + let envelope = + format!("%_/ArtDictionary :\n%_1 /Int (AIDeformOptionsKey) ,\n%_198 /Real (AIDeformArtHeightKey) ,\n%_X=\n%_0 Ae\n%_u\n{slot}%_U\n%_;\n"); + let art = native_with_text(&layer("Art", true, &format!("{}\n{envelope}", square(10, 10))), &doc); + let r = import(&eps(&page_square(10, 10), &art)).unwrap(); + assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings); + assert_eq!(kinds(&r.document.layers[0]), ["path", "text"], "{:?}", r.warnings); + let mut texts = vec![]; + r.document.walk(|n| { + if let NodeKind::Text(t) = &n.kind { + texts.push((t.plain_text(), n.visible)); + } + }); + assert_eq!(texts, [("CHONKYCATS".to_string(), false)], "{:?}", r.warnings); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); +}