PDF import: slanted type keeps its slant

A text matrix that shears the glyphs (type turned and slanted as a whole)
came in as point type turned by the baseline's angle only: the glyphs stood
square on the baseline, so the type drew somewhere else (60 pt type leaning
by 0.25 came back about 10 pt off its outlines). The placement now keeps the
slant (the em's lean along the baseline per unit of its height, the height
measured across the baseline), lines only join glyphs with the same slant,
and the type object's transform shears by it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Juan Manuel Verges SolanasandClaude Opus 5.5 committed 2026-10-09 11:18:03 +00:00
1 parent 133ad87541
commit ddd1c8fade
3 files changed
+37 -6

No files matched your search

+19 -4
View File
@@ -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 };
+2 -2
View File
@@ -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;
+16
View File
@@ -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:?}");
}
}