use pdfrum_common::kurbo::{Affine, BezPath, PathEl, Point, Vec2};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct SynthGlyph {
pub skew: i32,
pub vertical: bool,
pub embolden: f64,
}
impl SynthGlyph {
pub const NONE: Self = Self {
skew: 0,
vertical: false,
embolden: 0.0,
};
#[must_use]
pub fn apply(self, path: BezPath) -> BezPath {
let sheared = if self.skew == 0 {
path
} else {
shear(self.skew, self.vertical) * path
};
if self.embolden == 0.0 {
sheared
} else {
embolden(&sheared, self.embolden)
}
}
}
#[must_use]
pub(crate) fn shear(skew: i32, vertical: bool) -> Affine {
let s = f64::from(skew) / 100.0;
if vertical {
Affine::new([1.0, s, 0.0, 1.0, 0.0, 0.0])
} else {
Affine::new([1.0, 0.0, -s, 1.0, 0.0, 0.0])
}
}
#[must_use]
pub(crate) fn embolden(path: &BezPath, strength: f64) -> BezPath {
let strength = strength / 2.0;
if strength == 0.0 || !strength.is_finite() {
return path.clone();
}
let Some(truetype) = orientation_is_truetype(path) else {
return path.clone();
};
let mut out = BezPath::new();
for contour in contours(path) {
let moved = embolden_contour(&contour.points, strength, truetype);
emit(&mut out, &contour.els, &moved);
}
out
}
struct Contour {
els: Vec<PathEl>,
points: Vec<Point>,
}
fn contours(path: &BezPath) -> Vec<Contour> {
let mut out: Vec<Contour> = Vec::new();
for el in path.elements() {
if matches!(el, PathEl::MoveTo(_)) || out.is_empty() {
out.push(Contour {
els: Vec::new(),
points: Vec::new(),
});
}
let Some(last) = out.last_mut() else {
continue;
};
last.els.push(*el);
match *el {
PathEl::MoveTo(p) | PathEl::LineTo(p) => last.points.push(p),
PathEl::QuadTo(a, b) => last.points.extend([a, b]),
PathEl::CurveTo(a, b, c) => last.points.extend([a, b, c]),
PathEl::ClosePath => {}
}
}
out
}
fn emit(out: &mut BezPath, els: &[PathEl], points: &[Point]) {
let mut n = 0;
let next = |n: &mut usize| {
let p = points.get(*n).copied().unwrap_or_default();
*n += 1;
p
};
for el in els {
match el {
PathEl::MoveTo(_) => {
let p = next(&mut n);
out.push(PathEl::MoveTo(p));
}
PathEl::LineTo(_) => {
let p = next(&mut n);
out.push(PathEl::LineTo(p));
}
PathEl::QuadTo(..) => {
let (a, b) = (next(&mut n), next(&mut n));
out.push(PathEl::QuadTo(a, b));
}
PathEl::CurveTo(..) => {
let (a, b, c) = (next(&mut n), next(&mut n), next(&mut n));
out.push(PathEl::CurveTo(a, b, c));
}
PathEl::ClosePath => out.push(PathEl::ClosePath),
}
}
}
fn orientation_is_truetype(path: &BezPath) -> Option<bool> {
let mut area = 0.0;
for contour in contours(path) {
let pts = &contour.points;
let Some(&last) = pts.last() else { continue };
let mut prev = last;
for &p in pts {
area += (p.y - prev.y) * (p.x + prev.x);
prev = p;
}
}
if area == 0.0 {
return None;
}
Some(area < 0.0)
}
fn embolden_contour(points: &[Point], strength: f64, truetype: bool) -> Vec<Point> {
let mut pts = points.to_vec();
let count = pts.len();
if count == 0 {
return pts;
}
let last = count - 1;
let mut edge_in = Vec2::ZERO;
let mut l_in = 0.0_f64;
let mut anchor = Vec2::ZERO;
let mut l_anchor = 0.0_f64;
let mut hold = last;
let mut scan = 0_usize;
let mut first_moved: Option<usize> = None;
while scan != hold && Some(hold) != first_moved {
let (edge_out, l_out) = if Some(scan) == first_moved {
(anchor, l_anchor)
} else {
let Some((&head, &tail)) = pts.get(scan).zip(pts.get(hold)) else {
break;
};
let edge = head - tail;
let len = edge.hypot();
if len == 0.0 {
scan = if scan < last { scan + 1 } else { 0 };
continue;
}
(edge / len, len)
};
if l_in == 0.0 {
hold = scan;
} else {
if first_moved.is_none() {
first_moved = Some(hold);
anchor = edge_in;
l_anchor = l_in;
}
let dot = edge_in.dot(edge_out);
let mut shift = if dot > -0.9375 {
let dot = dot + 1.0;
let mut bisector = Vec2::new(edge_in.y + edge_out.y, edge_in.x + edge_out.x);
if truetype {
bisector.x = -bisector.x;
} else {
bisector.y = -bisector.y;
}
let cross = edge_out.x * edge_in.y - edge_out.y * edge_in.x;
let cross_signed = if truetype { -cross } else { cross };
let shorter = l_in.min(l_out);
if strength * cross_signed <= shorter * dot {
bisector * (strength / dot)
} else {
bisector * (shorter / cross_signed)
}
} else {
Vec2::ZERO
};
shift += Vec2::new(strength, strength);
while hold != scan {
if let Some(p) = pts.get_mut(hold) {
*p += shift;
}
hold = if hold < last { hold + 1 } else { 0 };
}
}
edge_in = edge_out;
l_in = l_out;
scan = if scan < last { scan + 1 } else { 0 };
}
pts
}
#[cfg(test)]
mod tests {
use super::*;
use pdfrum_common::kurbo::Shape;
fn square(size: f64) -> BezPath {
let mut p = BezPath::new();
p.move_to((0.0, 0.0));
p.line_to((size, 0.0));
p.line_to((size, size));
p.line_to((0.0, size));
p.close_path();
p
}
#[test]
fn a_negative_skew_pushes_the_top_of_a_glyph_to_the_right() {
let m = shear(-21, false);
let p = m * Point::new(0.0, 100.0);
assert!((p.x - 21.0).abs() < 1e-9, "{p:?}");
assert!((p.y - 100.0).abs() < 1e-9);
let base = m * Point::new(50.0, 0.0);
assert!((base.x - 50.0).abs() < 1e-9 && base.y.abs() < 1e-9);
}
#[test]
fn a_vertical_skew_displaces_y_from_x_instead() {
let m = shear(-21, true);
let p = m * Point::new(100.0, 0.0);
assert!((p.x - 100.0).abs() < 1e-9);
assert!((p.y + 21.0).abs() < 1e-9, "{p:?}");
let on_axis = m * Point::new(0.0, 50.0);
assert!(on_axis.x.abs() < 1e-9 && (on_axis.y - 50.0).abs() < 1e-9);
}
#[test]
fn a_zero_skew_is_the_identity() {
assert_eq!(shear(0, false), Affine::IDENTITY);
assert_eq!(shear(0, true), Affine::IDENTITY);
}
#[test]
fn emboldening_grows_a_contour_s_area() {
let before = square(100.0);
let after = embolden(&before, 20.0);
assert!(
after.area().abs() > before.area().abs(),
"{} vs {}",
after.area().abs(),
before.area().abs()
);
}
#[test]
fn a_zero_strength_leaves_an_outline_alone() {
let before = square(100.0);
assert_eq!(
format!("{:?}", embolden(&before, 0.0)),
format!("{before:?}")
);
assert_eq!(
format!("{:?}", embolden(&before, f64::NAN)),
format!("{before:?}")
);
}
#[test]
fn emboldening_preserves_the_element_shape_of_a_path() {
let mut p = BezPath::new();
p.move_to((0.0, 0.0));
p.curve_to((10.0, 30.0), (60.0, 30.0), (70.0, 0.0));
p.quad_to((35.0, -20.0), (0.0, 0.0));
p.close_path();
let after = embolden(&p, 8.0);
let kind = |b: &BezPath| {
b.elements()
.iter()
.map(|e| match e {
PathEl::MoveTo(_) => 'M',
PathEl::LineTo(_) => 'L',
PathEl::QuadTo(..) => 'Q',
PathEl::CurveTo(..) => 'C',
PathEl::ClosePath => 'Z',
})
.collect::<String>()
};
assert_eq!(kind(&after), kind(&p));
assert_ne!(format!("{after:?}"), format!("{p:?}"));
}
#[test]
fn emboldening_drifts_the_outline_away_from_the_origin() {
let before = square(100.0);
let after = embolden(&before, 20.0);
assert!(after.bounding_box().x1 > before.bounding_box().x1);
assert!(after.bounding_box().y1 > before.bounding_box().y1);
}
#[test]
fn a_degenerate_outline_survives_emboldening() {
let mut p = BezPath::new();
p.move_to((5.0, 5.0));
p.line_to((5.0, 5.0));
p.line_to((5.0, 5.0));
p.close_path();
let after = embolden(&p, 10.0);
assert_eq!(after.elements().len(), p.elements().len());
assert!(BezPath::new().elements().is_empty());
assert_eq!(embolden(&BezPath::new(), 10.0).elements().len(), 0);
}
}