diff --git a/crates/pdf/src/import.rs b/crates/pdf/src/import.rs index 4af87211..70877028 100644 --- a/crates/pdf/src/import.rs +++ b/crates/pdf/src/import.rs @@ -1241,11 +1241,48 @@ impl<'p> Builder<'p> { } fn flush_text(&mut self) { - let Some((t, opacity, facts)) = self.text.take().and_then(TextLine::finish) else { return }; - let mut n = Node::new(self.id(), NodeKind::Text(Box::new(t))); - n.opacity = opacity; - self.lines.insert(n.id, facts); - self.push_node(n); + let Some(mut line) = self.text.take() else { return }; + let held = std::mem::take(&mut line.held); + if let Some((t, opacity, facts)) = line.finish() { + let mut n = Node::new(self.id(), NodeKind::Text(Box::new(t))); + n.opacity = opacity; + self.lines.insert(n.id, facts); + self.push_node(n); + } + // The strokes of its glyphs' outlines, right after it ([`import_lines`] makes them its stroke). + for n in held { + self.push_node(n); + } + } + + /// A stroke of the outline of the glyph just drawn, in the glyph's own paint, while every glyph + /// before it on the open line was stroked so too, is held with the line instead of ending it + /// (an editor writes type on a path glyph by glyph, each followed by the stroke added to the + /// type). `false`: it isn't one. + fn hold_glyph_stroke(&mut self, path: &BezPath, transform: Affine, paint: &hayro_interpret::Paint<'_>, draw_mode: &PathDrawMode) -> bool { + let PathDrawMode::Stroke(props) = draw_mode else { return false }; + if self.mask.is_some() || self.blend != BlendMode::Normal || matches!(paint, hayro_interpret::Paint::Pattern(_)) { + return false; + } + let (p, opacity) = self.paint(paint, true); + let Some(line) = &self.text else { return false }; + let Some(ink) = line.ink.last() else { return false }; + if line.held.len() + 1 != line.ink.len() || opacity < 0.999 || !line.last_fill().is_some_and(|f| *f == p) { + return false; + } + let mut bp = path.clone(); + bp.apply_affine(transform); + let b = bp.bounding_box(); + let tol = 0.01 * (ink.width() + ink.height()) + 0.01; + if ![(b.x0, ink.x0), (b.y0, ink.y0), (b.x1, ink.x1), (b.y1, ink.y1)].iter().all(|(a, c)| (a - c).abs() <= tol) { + return false; + } + let st = stroke_layer(p, opacity, props, mean_scale(transform)); + let n = Node::path(self.id(), PathData::from_bezpath(&bp), Appearance { items: vec![AppearanceItem::Stroke(st)], ..Default::default() }); + if let Some(line) = &mut self.text { + line.held.push(n); + } + true } /// Convert a hayro paint to a VectorCraft paint and opacity (`stroke`: for a stroke). @@ -1667,6 +1704,10 @@ impl<'a> Device<'a> for Builder<'_> { fn draw_path(&mut self, path: &BezPath, transform: Affine, paint: &hayro_interpret::Paint<'a>, draw_mode: &PathDrawMode) { self.flush_glyphs(); + if self.hold_glyph_stroke(path, transform, paint, draw_mode) { + self.pending = false; + return; + } self.flush_text(); self.pending = false; let overprint = self.path_overprint(); diff --git a/crates/pdf/src/import_text.rs b/crates/pdf/src/import_text.rs index 1ccab798..04d25c0b 100644 --- a/crates/pdf/src/import_text.rs +++ b/crates/pdf/src/import_text.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; use kurbo::{Affine, BezPath, Point, Rect, Vec2}; use vectorcraft_color::Paint; -use vectorcraft_doc::{CharStyle, ParaDirection, TextKind, TextObject, TextRun}; +use vectorcraft_doc::{CharStyle, Node, ParaDirection, TextKind, TextObject, TextRun}; use vectorcraft_text::TextLayout; /// One glyph's placement: its baseline origin, advance direction, size and horizontal scale. @@ -144,6 +144,9 @@ pub(crate) struct TextLine { gaps: Vec<(usize, f64)>, /// Each glyph's advance in the file's font (points), with [`Self::baseline`]. advances: Vec, + /// The strokes of its glyphs' outlines drawn between them, one per glyph with ink so far + /// (written right after the line). + pub held: Vec, } /// What a finished line of type tells beyond its text object (see [`crate::import_lines`]). @@ -177,9 +180,15 @@ impl TextLine { ink: vec![], gaps: vec![], advances: vec![], + held: vec![], } } + /// The paint the last glyph is filled with. + pub fn last_fill(&self) -> Option<&Paint> { + self.runs.last()?.0.fill.as_ref() + } + /// Add glyph `text` set vertically (`upright`) if it continues this column (or makes this /// line's single glyph a column): below the last one, in the same column. `false`: it starts /// another line. diff --git a/crates/pdf/src/tests_import_lines.rs b/crates/pdf/src/tests_import_lines.rs index ea2ea3ad..fddb4622 100644 --- a/crates/pdf/src/tests_import_lines.rs +++ b/crates/pdf/src/tests_import_lines.rs @@ -153,6 +153,25 @@ fn a_stroke_on_the_type_of_a_moved_ai_page_is_still_the_types_stroke() { assert!(matches!(art[1].appearance.items.as_slice(), [AppearanceItem::Stroke(_)]), "{:?}", art[1].appearance); } +#[test] +fn glyphs_each_followed_by_their_stroked_outline_stay_one_object() { + // Type on a path is written glyph by glyph: each glyph, then its outline stroked in the same + // colour (a shadow under the type). + let glyph = |c: char, x: f64, stroke: &str| format!("BT 0 0 1 rg /F1 24 Tf {x} 50 Td ({c}) Tj ET {stroke} 0.5 w {}", outline(c, x)); + let i_at = 10.0 + 0.722 * 24.0; + let d = import(&page(&(glyph('H', 10.0, "0 0 1 RG") + &glyph('i', i_at, "0 0 1 RG")))).unwrap(); + let art = d.layers[0].children().unwrap(); + assert_eq!(art.len(), 1, "one object: {art:?}"); + let NodeKind::Text(t) = &art[0].kind else { panic!("{:?}", art[0].kind) }; + assert_eq!(t.plain_text(), "Hi"); + let [AppearanceItem::Stroke(st)] = art[0].appearance.items.as_slice() else { panic!("{:?}", art[0].appearance) }; + assert_eq!(st.paint.color(), Some(Color::rgb(0.0, 0.0, 1.0))); + // A stroke of another colour drawn over each glyph in turn would also cover the next one's + // neighbour as one object's stroke: the glyphs stay as the file draws them. + let d = import(&page(&(glyph('H', 10.0, "1 1 1 RG") + &glyph('i', i_at, "1 1 1 RG")))).unwrap(); + assert_eq!(texts(&d).len(), 2); +} + /// A 300 × 200 pt page with `lines` of (x, y from the top, size, text) in Source Sans 3. fn source_sans(lines: &[(f32, f32, f32, &str)]) -> Vec { crate::tests_live_text::text_pdf(lines, false)