diff --git a/crates/pdf/src/import_text.rs b/crates/pdf/src/import_text.rs index aee7de48..1b654a08 100644 --- a/crates/pdf/src/import_text.rs +++ b/crates/pdf/src/import_text.rs @@ -24,6 +24,9 @@ pub(crate) struct Placement { pub size: f64, /// Width of the em ÷ its height, in percent. pub h_scale: f64, + /// How far the em leans along the baseline per unit of its height (a text matrix with a + /// shear: faux italic, or type slanted as a whole); 0 when it stands square on the baseline. + pub slant: f64, } impl Placement { @@ -33,14 +36,23 @@ impl Placement { let origin = m * Point::ORIGIN; let ex = m * Point::new(1000.0, 0.0) - origin; let ey = m * Point::new(0.0, 1000.0) - origin; - let (w, size) = (ex.hypot(), ey.hypot()); + let w = ex.hypot(); // Upright in y-down document space: x to the right of up. let upright = ex.cross(ey) < 0.0; - (origin.is_finite() && w.is_finite() && size.is_finite() && w > 0.01 && size > 0.01 && size < 1e5 && upright).then(|| Self { + if !(w.is_finite() && w > 0.01) { + return None; + } + let dir = ex / w; + // The em's height is across the baseline; what's left of `ey` along it is a slant. + let up = Vec2::new(dir.y, -dir.x); + let size = ey.dot(up); + let slant = ey.dot(dir) / size; + (origin.is_finite() && size.is_finite() && slant.is_finite() && size > 0.01 && size < 1e5 && upright).then(|| Self { origin, - dir: ex / w, + dir, size, h_scale: w / size * 100.0, + slant, }) } @@ -211,6 +223,7 @@ impl TextLine { let gap = (at.origin - self.next).dot(self.last_dir); let on_line = opacity == self.opacity && at.dir.dot(self.at.dir) > 0.9995 + && (at.slant - self.at.slant).abs() < 1e-3 && (at.origin - self.at.origin).dot(self.at.up()).abs() < size * 0.15 && gap > -size * 0.3 && gap < size * 3.0; @@ -218,6 +231,7 @@ impl TextLine { // that one ends. let on_curve = !on_line && opacity == self.opacity + && (at.slant - self.at.slant).abs() < 1e-3 && at.dir.dot(self.last_dir) > CURVE_TURN_COS && at.dir.dot(self.last_dir) < 0.99999 && (at.origin - self.next).hypot() < size * 0.6; @@ -309,7 +323,8 @@ impl TextLine { to_logical(&mut t); } let angle = self.at.dir.atan2(); - t.xf = Affine::translate(self.at.origin.to_vec2()) * Affine::rotate(angle); + // The glyphs' tops lean along the baseline (+x), up being -y in the type's own space. + t.xf = Affine::translate(self.at.origin.to_vec2()) * Affine::rotate(angle) * Affine::skew(-self.at.slant, 0.0); let db = vectorcraft_text::FontDb::global(); if let Some(path) = path { t.kind = TextKind::OnPath { path: vectorcraft_geom::PathData::from_bezpath(&path), start: 0.0, end: None }; diff --git a/crates/pdf/src/tests_live_text.rs b/crates/pdf/src/tests_live_text.rs index cdbe985c..0cbf9ecc 100644 --- a/crates/pdf/src/tests_live_text.rs +++ b/crates/pdf/src/tests_live_text.rs @@ -265,7 +265,7 @@ fn a_single_upright_glyph_comes_in_as_vertical_type_and_takes_no_horizontal_glyp fill: Some(vectorcraft_color::Paint::solid(vectorcraft_color::Color::rgb(0.0, 0.0, 0.0))), stroke: None, }; - let at = |x: f64, y: f64| Placement { origin: Point::new(x, y), dir: Vec2::new(1.0, 0.0), size: 20.0, h_scale: 100.0 }; + let at = |x: f64, y: f64| Placement { origin: Point::new(x, y), dir: Vec2::new(1.0, 0.0), size: 20.0, h_scale: 100.0, slant: 0.0 }; let mut line = TextLine::new(at(100.0, 100.0), 1.0); assert!(line.push_upright(&look, at(100.0, 100.0), 1.0, Upright { top: Point::new(110.0, 82.0) }, "§")); assert!(!line.push(&look, at(130.0, 100.0), 1.0, 10.0, "a"), "a horizontal glyph starts another line"); @@ -288,7 +288,7 @@ fn letter_spaced_vertical_type_keeps_its_characters_together() { stroke: None, }; // Glyphs set down a column 1.5 em apart (tracking 500): no spaces come in between them. - let at = |y: f64| Placement { origin: Point::new(100.0, y), dir: Vec2::new(1.0, 0.0), size: 20.0, h_scale: 100.0 }; + let at = |y: f64| Placement { origin: Point::new(100.0, y), dir: Vec2::new(1.0, 0.0), size: 20.0, h_scale: 100.0, slant: 0.0 }; let mut line = TextLine::new(at(100.0), 1.0); for (i, c) in ["§", "§", "§"].iter().enumerate() { let y = 100.0 + 30.0 * i as f64; diff --git a/crates/pdf/src/tests_realtext.rs b/crates/pdf/src/tests_realtext.rs index d0e64afc..ef3f770d 100644 --- a/crates/pdf/src/tests_realtext.rs +++ b/crates/pdf/src/tests_realtext.rs @@ -274,3 +274,19 @@ fn an_embedded_fonts_own_cmap_names_its_glyphs() { } assert!(crate::import::font_chars(b"not a font").is_none()); } + +/// Type whose text matrix shears its glyphs (slanted as a whole, as Illustrator writes type +/// turned and skewed) reopens as type that still leans: drawn, it covers what its outlines do. +#[test] +fn slanted_type_reopens_as_type_that_still_leans() { + for (turn, lean) in [(0.0, 0.25), (-0.2, 0.25), (0.3, -0.15)] { + let mut t = TextObject::point(Point::new(80.0, 150.0), "LEAN", style(60.0)); + t.xf *= Affine::rotate(turn) * Affine::skew(lean, 0.0); + let d = doc(vec![t]); + let reopened = import_as(&pdf(&d, false).bytes, TextAs::Text); + assert_eq!(texts(&reopened), ["LEAN"], "{turn} {lean}: reopened as type"); + let (a, b) = (ink(&import_as(&pdf(&reopened, true).bytes, TextAs::Outlines)), ink(&import_as(&pdf(&d, true).bytes, TextAs::Outlines))); + let off = [a.x0 - b.x0, a.y0 - b.y0, a.x1 - b.x1, a.y1 - b.y1].iter().fold(0.0f64, |m, v| m.max(v.abs())); + assert!(off < 0.5, "{turn} {lean}: {a:?} vs {b:?}"); + } +}