diff --git a/crates/eps/src/ai_editing.rs b/crates/eps/src/ai_editing.rs index d87a00396..9f3c24fe3 100644 --- a/crates/eps/src/ai_editing.rs +++ b/crates/eps/src/ai_editing.rs @@ -36,7 +36,7 @@ //! the first artboard's top-left corner. On the canvas ([`CANVAS_SIZE`] points square, y down) the //! artboards together are centered; each artboard's `RulerOrigin` is its top-left corner there, and //! `%AI3_TemplateBox` holds the canvas's center in art space. What the form above can't hold is written as plain filled and stroked paths, each such -//! loss a warning: type as its outlines, gradients as their average color, pattern fills as their +//! loss a warning: type as its outlines and its inline graphics' art, gradients as their average color, pattern fills as their //! tiles cut to the shape, clipping groups and clipping layers as their art cut to the clipping //! path (the clipping path's fill cut too, its stroke not), images as their outlines filled with //! their average color, symbols and live objects as their art, strokes a plain line can't draw as diff --git a/crates/eps/src/ai_editing/tests.rs b/crates/eps/src/ai_editing/tests.rs index 696014fb9..a3acb5d2f 100644 --- a/crates/eps/src/ai_editing/tests.rs +++ b/crates/eps/src/ai_editing/tests.rs @@ -617,6 +617,40 @@ fn type_is_written_as_create_outlines_makes_it() { assert_eq!(rounded(group), rounded(&outlined)); } +#[test] +fn inline_graphics_in_type_are_written_as_their_art() { + let mut doc = Document::new(200.0, 100.0); + let layer = doc.layers[0].id; + let mark = Node::path( + doc.alloc_id(), + shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), + Appearance::basic(solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0), + ); + doc.symbols.push(Symbol { name: "Mark".into(), art: Arc::new(mark) }); + let style = vectorcraft_doc::CharStyle { size: 18.0, fill: solid(Color::BLACK), ..vectorcraft_doc::CharStyle::default() }; + let mut t = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(10.0, 70.0), "A", style.clone()); + t.runs.push(vectorcraft_doc::TextRun::inline(vectorcraft_doc::InlineArt::new("Mark"), style)); + let mut text = Node::new(doc.alloc_id(), NodeKind::Text(Box::new(t))); + // A stroke of the object's own above the Characters row: drawn over the inline graphic. + let red = solid(Color::rgb(1.0, 0.0, 0.0)); + text.appearance = + Appearance { items: vec![AppearanceItem::Stroke(StrokeLayer::new(red.clone(), 3.0))], contents_index: Some(0), ..Appearance::default() } + .into(); + doc.insert(Some(layer), usize::MAX, text).unwrap(); + let back = read_back(&doc); + let mut all = vec![]; + paths(back.layers[0].children().unwrap(), &mut all); + let has = |p: &Node, c: &Paint| p.appearance.items.iter().any(|i| paint(i.paint()) == paint(c)); + let blue = solid(Color::rgb(0.0, 0.0, 1.0)); + let art: Vec<&Node> = all.iter().filter(|p| has(p, &blue)).collect(); + assert_eq!(art.len(), 1, "the inline graphic is written"); + // To the right of the letter, 18 pt high as the run's size makes it. + let b = art[0].visual_bounds().unwrap(); + assert!(b.x0 > 10.0 && (b.height() - 18.0).abs() < 0.01, "{b:?}"); + let at = |c: &Paint| all.iter().position(|p| has(p, c)).unwrap(); + assert!(at(&blue) < at(&red), "the stroke above the characters is over the inline graphic"); +} + #[test] fn a_stained_symbol_instance_is_written_in_its_stained_colors() { let mut doc = Document::new(200.0, 100.0); diff --git a/crates/eps/src/ai_editing/writer.rs b/crates/eps/src/ai_editing/writer.rs index 24c0d57b8..58947715c 100644 --- a/crates/eps/src/ai_editing/writer.rs +++ b/crates/eps/src/ai_editing/writer.rs @@ -507,19 +507,55 @@ impl<'a> Writer<'a> { /// Type `n` as a group of what Type › Create Outlines makes of it /// ([`vectorcraft_effects::outline_text`]): each run's glyphs with their underline and - /// strikethrough, its color glyphs, and the object's own fills and strokes. + /// strikethrough, its color glyphs, and the object's own fills and strokes; then its inline + /// graphics as their symbols' art where the layout places them, as the PDF page draws them. fn text(&mut self, n: &Node) { self.warn(TYPE); self.line("u"); - if let Some(outlined) = vectorcraft_effects::outline_text(n) { - let state = self.state; - for c in outlined.children().map(Vec::as_slice).unwrap_or_default() { - self.node(c); - } - if let Some((locked, hidden)) = state { - self.state(locked, hidden); + let doc = self.doc; + let resolved = match &n.kind { + NodeKind::Text(t) => Some(doc.inline_resolved(t).into_owned()), + _ => None, + }; + let node = match &resolved { + Some(t) => Node { kind: NodeKind::Text(Box::new(t.clone())), ..n.clone() }, + None => n.clone(), + }; + let state = self.state; + let outlined = vectorcraft_effects::outline_text(&node); + let pieces = outlined.as_ref().and_then(|o| o.children()).map(Vec::as_slice).unwrap_or_default(); + // The object's fills and strokes above the Characters row, when they paint, are the last + // piece: the inline graphics go under it, as the PDF page draws them. + let above = !node.appearance.split_contents().1.is_empty() && { + let bare = Node { appearance: Default::default(), ..node.clone() }; + let glyphs = vectorcraft_effects::outline_text(&bare).and_then(|o| o.children().map(Vec::len)).unwrap_or_default(); + pieces.len() > glyphs + }; + let (under, over) = pieces.split_at(pieces.len() - usize::from(above)); + for c in under { + self.node(c); + } + if let Some(t) = resolved.as_ref().filter(|_| self.nest < MAX_NEST) { + let layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t); + for ig in &layout.inlines { + let Some(art) = t.runs.get(ig.run).and_then(|r| r.inline.as_ref()) else { continue }; + let Some(sym) = doc.symbols.iter().find(|x| x.name == art.symbol) else { continue }; + self.warn(LIVE); + let mut art_node = (*sym.art).clone(); + // Strokes scale with the art, as on the canvas. + art_node.transform(t.xf * ig.xf * doc.symbol_natural_xf(&art.symbol), true); + (art_node.visible, art_node.locked, art_node.name) = (true, false, None); + self.nest += 1; + self.node(&art_node); + self.nest -= 1; } } + for c in over { + self.node(c); + } + if let Some((locked, hidden)) = state { + self.state(locked, hidden); + } self.line("U"); }