Merge pull request #742 from jmverges/pdf-import-slanted-type

PDF import: slanted type keeps its slant
This commit is contained in:
BFlat 2026-10-09 18:40:19 +07:00 committed by GitHub
commit 257c8ec435
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 37 additions and 6 deletions

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 };

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;

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:?}");
}
}