#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PageRotation {
Upright,
Ccw,
Cw,
}
impl PageRotation {
pub(crate) fn baseline_rebase_degrees(self) -> f32 {
match self {
PageRotation::Upright => 0.0,
PageRotation::Ccw => -90.0,
PageRotation::Cw => 90.0,
}
}
pub(crate) fn rotate_point(self, x: f32, y: f32) -> (f32, f32) {
match self {
PageRotation::Upright => (x, y),
PageRotation::Ccw => (y, -x),
PageRotation::Cw => (-y, x),
}
}
pub(crate) fn rotate_box(self, x: &mut f32, y: &mut f32, width: &mut f32, height: &mut f32) {
let (x0, x1) = (x.min(*x + *width), x.max(*x + *width));
let (y0, y1) = (y.min(*y + *height), y.max(*y + *height));
let (new_x, new_y, new_width, new_height) = match self {
PageRotation::Upright => (x0, y0, x1 - x0, y1 - y0),
PageRotation::Ccw => (y0, -x1, y1 - y0, x1 - x0),
PageRotation::Cw => (-y1, x0, y1 - y0, x1 - x0),
};
*x = new_x;
*y = new_y;
*width = new_width;
*height = new_height;
}
}
pub(crate) fn rise_adjusted(tm: &[f32; 6], rise: f32) -> [f32; 6] {
if rise == 0.0 {
return *tm;
}
[
tm[0],
tm[1],
tm[2],
tm[3],
tm[4] + rise * tm[2],
tm[5] + rise * tm[3],
]
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct RunGeometry {
pub(crate) x: f32,
pub(crate) y: f32,
pub(crate) width: f32,
pub(crate) height: f32,
pub(crate) rotation: f32,
pub(crate) advance_known: bool,
}
pub(crate) fn reading_direction(combined: &[f32; 6], font_size: f32) -> (f32, f32) {
if font_size < 0.0 {
(-combined[0], -combined[1])
} else {
(combined[0], combined[1])
}
}
pub(crate) fn scaled_run_geometry(
combined: &[f32; 6],
advance_ts: Option<f32>,
fallback_advance_ts: f32,
em: f32,
glyph_up_flipped: bool,
horizontal_scale: f32,
) -> RunGeometry {
let mut reflected = *combined;
if horizontal_scale < 0.0 {
reflected[0] = -reflected[0];
reflected[1] = -reflected[1];
}
run_geometry(
&reflected,
advance_ts.map(|advance| advance * horizontal_scale.abs()),
fallback_advance_ts * horizontal_scale.abs(),
em,
glyph_up_flipped,
)
}
pub(crate) fn run_geometry(
combined: &[f32; 6],
advance_ts: Option<f32>,
fallback_advance_ts: f32,
em: f32,
glyph_up_flipped: bool,
) -> RunGeometry {
let (x0, y0) = (combined[4], combined[5]);
let (advance, advance_known) = match advance_ts {
Some(advance) => (advance, true),
None => (fallback_advance_ts, false),
};
let reversed = em < 0.0;
let em = em.abs();
let (ax, ay) = (advance * combined[0], advance * combined[1]);
let axis_len = combined[0].hypot(combined[1]);
let (ux, uy) = if axis_len > f32::EPSILON {
let (px, py) = (-combined[1] / axis_len, combined[0] / axis_len);
let y_axis_perp = combined[2] * px + combined[3] * py;
let max_scale = axis_len.max(combined[2].hypot(combined[3]));
let em_perp = if y_axis_perp.abs() > f32::EPSILON && max_scale > f32::EPSILON {
em * y_axis_perp.abs() / max_scale
} else {
em
};
let y_axis_side = if y_axis_perp < 0.0 { -1.0 } else { 1.0 };
let side = if glyph_up_flipped != reversed {
-y_axis_side
} else {
y_axis_side
};
(px * em_perp * side, py * em_perp * side)
} else {
(
0.0,
if glyph_up_flipped != reversed {
-em
} else {
em
},
)
};
let (dir_x, dir_y) = if reversed {
(-combined[0], -combined[1])
} else {
(combined[0], combined[1])
};
let det = combined[0] * combined[3] - combined[1] * combined[2];
let rotation = if det < 0.0 {
let up_sign = if glyph_up_flipped != reversed {
-1.0
} else {
1.0
};
let (up_x, up_y) = (combined[2] * up_sign, combined[3] * up_sign);
baseline_rotation(up_y, -up_x)
} else {
baseline_rotation(dir_x, dir_y)
};
let xs = [x0, x0 + ax, x0 + ux, x0 + ax + ux];
let ys = [y0, y0 + ay, y0 + uy, y0 + ay + uy];
let x_min = xs.iter().copied().fold(f32::INFINITY, f32::min);
let x_max = xs.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let y_min = ys.iter().copied().fold(f32::INFINITY, f32::min);
let y_max = ys.iter().copied().fold(f32::NEG_INFINITY, f32::max);
RunGeometry {
x: x_min,
y: y_min,
width: x_max - x_min,
height: y_max - y_min,
rotation,
advance_known,
}
}
pub(crate) fn estimated_advance_ts(text: &str, font_size_ts: f32) -> f32 {
estimated_advance_for_glyphs(text.chars().count(), font_size_ts)
}
pub(crate) fn estimated_advance_for_glyphs(glyphs: usize, font_size_ts: f32) -> f32 {
glyphs as f32 * 0.5 * font_size_ts
}
pub(crate) fn baseline_rotation(a: f32, b: f32) -> f32 {
if a == 0.0 && b == 0.0 {
return 0.0;
}
let degrees = normalize_degrees(b.atan2(a).to_degrees());
let whole = degrees.round();
if (degrees - whole).abs() < 0.01 {
normalize_degrees(whole)
} else {
degrees
}
}
pub(crate) fn normalize_degrees(degrees: f32) -> f32 {
let wrapped = degrees.rem_euclid(360.0);
if wrapped >= 360.0 || wrapped == 0.0 {
0.0
} else {
wrapped
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn page_rotation_turns_boxes_and_points_consistently() {
let (mut x, mut y, mut w, mut h) = (188.0, 100.0, 12.0, 36.0);
PageRotation::Ccw.rotate_box(&mut x, &mut y, &mut w, &mut h);
assert_eq!((x, y, w, h), (100.0, -200.0, 36.0, 12.0));
assert_eq!(
PageRotation::Ccw.rotate_point(200.0, 100.0),
(100.0, -200.0)
);
let (mut x, mut y, mut w, mut h) = (580.0, 664.0, 10.0, 36.0);
PageRotation::Cw.rotate_box(&mut x, &mut y, &mut w, &mut h);
assert_eq!((x, y, w, h), (-700.0, 580.0, 36.0, 10.0));
assert_eq!(PageRotation::Cw.rotate_point(580.0, 700.0), (-700.0, 580.0));
let (mut x, mut y, mut w, mut h) = (1.0, 2.0, 3.0, 4.0);
PageRotation::Upright.rotate_box(&mut x, &mut y, &mut w, &mut h);
assert_eq!((x, y, w, h), (1.0, 2.0, 3.0, 4.0));
assert_eq!(PageRotation::Ccw.baseline_rebase_degrees(), -90.0);
assert_eq!(PageRotation::Cw.baseline_rebase_degrees(), 90.0);
}
#[test]
fn page_rotation_normalises_negative_extents() {
for rotation in [PageRotation::Ccw, PageRotation::Cw] {
let (mut x, mut y, mut w, mut h) = (100.0, 200.0, -20.0, -10.0);
rotation.rotate_box(&mut x, &mut y, &mut w, &mut h);
let (mut nx, mut ny, mut nw, mut nh) = (80.0, 190.0, 20.0, 10.0);
rotation.rotate_box(&mut nx, &mut ny, &mut nw, &mut nh);
assert_eq!((x, y, w, h), (nx, ny, nw, nh), "{rotation:?}");
assert!(w > 0.0 && h > 0.0);
}
let (mut x, mut y, mut w, mut h) = (100.0, 200.0, -20.0, -10.0);
PageRotation::Ccw.rotate_box(&mut x, &mut y, &mut w, &mut h);
assert_eq!((x, y, w, h), (190.0, -100.0, 10.0, 20.0));
let (mut x, mut y, mut w, mut h) = (100.0, 200.0, -20.0, -10.0);
PageRotation::Upright.rotate_box(&mut x, &mut y, &mut w, &mut h);
assert_eq!((x, y, w, h), (80.0, 190.0, 20.0, 10.0));
}
#[test]
fn run_geometry_without_advance_keeps_em_width_for_vertical_runs() {
let vertical = run_geometry(&[0.0, 1.0, -1.0, 0.0, 100.0, 100.0], None, 0.0, 10.0, false);
assert_eq!(
(vertical.x, vertical.y, vertical.width, vertical.height),
(90.0, 100.0, 10.0, 0.0)
);
assert!(!vertical.advance_known);
let upright = run_geometry(&[1.0, 0.0, 0.0, 1.0, 100.0, 100.0], None, 0.0, 10.0, false);
assert_eq!(
(upright.x, upright.y, upright.width, upright.height),
(100.0, 100.0, 0.0, 10.0)
);
assert!(!upright.advance_known);
let zero = run_geometry(
&[1.0, 0.0, 0.0, 1.0, 100.0, 100.0],
Some(0.0),
0.0,
10.0,
false,
);
assert!(zero.advance_known);
assert_eq!((zero.width, zero.height), (0.0, 10.0));
}
#[test]
fn run_geometry_lays_the_fallback_advance_along_the_run() {
let upright = run_geometry(&[1.0, 0.0, 0.0, 1.0, 100.0, 100.0], None, 20.0, 10.0, false);
assert!(!upright.advance_known);
assert_eq!(
(upright.x, upright.y, upright.width, upright.height),
(100.0, 100.0, 20.0, 10.0)
);
let flipped = run_geometry(
&[-1.0, 0.0, 0.0, 1.0, 100.0, 100.0],
None,
20.0,
10.0,
false,
);
assert_eq!((flipped.x, flipped.width), (80.0, 20.0));
assert!(!flipped.advance_known);
let down = run_geometry(
&[0.0, -1.0, 1.0, 0.0, 100.0, 100.0],
None,
20.0,
10.0,
false,
);
assert_eq!(
(down.y, down.height, down.x, down.width),
(80.0, 20.0, 100.0, 10.0)
);
let up = run_geometry(
&[0.0, 1.0, -1.0, 0.0, 100.0, 100.0],
None,
20.0,
10.0,
false,
);
assert_eq!((up.y, up.height, up.x, up.width), (100.0, 20.0, 90.0, 10.0));
assert_eq!(estimated_advance_ts("abcd", 12.0), 24.0);
}
#[test]
fn run_geometry_takes_the_em_height_from_the_y_axis() {
let stretched = run_geometry(
&[2.0, 0.0, 0.0, 1.0, 0.0, 0.0],
Some(10.0),
0.0,
24.0,
false,
);
assert_eq!(
(stretched.width, stretched.height, stretched.rotation),
(20.0, 12.0, 0.0)
);
let tall = run_geometry(
&[1.0, 0.0, 0.0, 2.0, 0.0, 0.0],
Some(10.0),
0.0,
24.0,
false,
);
assert_eq!((tall.width, tall.height), (10.0, 24.0));
let turned = run_geometry(
&[0.0, 2.0, -2.0, 0.0, 0.0, 0.0],
Some(10.0),
0.0,
24.0,
false,
);
assert_eq!((turned.width, turned.height), (24.0, 20.0));
}
#[test]
fn run_geometry_picks_the_baseline_side_from_the_matrix_and_font() {
let dvips = run_geometry(
&[1.0, 0.0, 0.0, -1.0, 100.0, 500.0],
Some(30.0),
0.0,
10.0,
true,
);
assert_eq!(
(dvips.x, dvips.y, dvips.width, dvips.height, dvips.rotation),
(100.0, 500.0, 30.0, 10.0, 0.0)
);
let hanging = run_geometry(
&[1.0, 0.0, 0.0, -1.0, 100.0, 500.0],
Some(30.0),
0.0,
10.0,
false,
);
assert_eq!((hanging.y, hanging.height), (490.0, 10.0));
let mirrored = run_geometry(
&[-1.0, 0.0, 0.0, 1.0, 300.0, 500.0],
Some(30.0),
0.0,
10.0,
false,
);
assert_eq!(
(
mirrored.x,
mirrored.y,
mirrored.width,
mirrored.height,
mirrored.rotation
),
(270.0, 500.0, 30.0, 10.0, 0.0)
);
let em = 10.0 * 0.34_f32.hypot(1.0);
let sheared = run_geometry(
&[1.0, 0.0, 0.34, 1.0, 100.0, 500.0],
Some(30.0),
0.0,
em,
false,
);
assert_eq!((sheared.x, sheared.y, sheared.width), (100.0, 500.0, 30.0));
assert!(
(sheared.height - 10.0).abs() < 1e-3,
"height = {}",
sheared.height
);
let ccw = run_geometry(
&[0.0, 1.0, -1.0, 0.0, 100.0, 100.0],
Some(30.0),
0.0,
10.0,
false,
);
assert_eq!((ccw.x, ccw.width), (90.0, 10.0));
let cw = run_geometry(
&[0.0, -1.0, 1.0, 0.0, 100.0, 100.0],
Some(30.0),
0.0,
10.0,
false,
);
assert_eq!((cw.x, cw.width), (100.0, 10.0));
}
#[test]
fn run_geometry_reports_glyph_orientation_for_reflected_matrices() {
let mirrored = run_geometry(
&[-1.0, 0.0, 0.0, 1.0, 300.0, 500.0],
Some(21.6),
0.0,
12.0,
false,
);
assert_eq!(mirrored.rotation, 0.0);
assert!((mirrored.x - 278.4).abs() < 1e-3 && mirrored.y == 500.0);
assert!((mirrored.width - 21.6).abs() < 1e-3 && mirrored.height == 12.0);
let dvips = run_geometry(
&[0.12, 0.0, 0.0, -0.12, 100.0, 700.0],
Some(300.0),
0.0,
12.0,
true,
);
assert_eq!(dvips.rotation, 0.0);
assert_eq!(dvips.y, 700.0);
let flipped = run_geometry(
&[1.0, 0.0, 0.0, -1.0, 100.0, 700.0],
Some(36.0),
0.0,
12.0,
false,
);
assert_eq!(flipped.rotation, 180.0);
assert_eq!((flipped.y, flipped.height), (688.0, 12.0));
let sideways = run_geometry(
&[0.0, 1.0, 1.0, 0.0, 100.0, 100.0],
Some(36.0),
0.0,
12.0,
false,
);
assert_eq!(sideways.rotation, 270.0);
}
#[test]
fn run_geometry_reads_the_turn_from_the_font_size_sign_not_the_advance() {
let turned = run_geometry(
&[1.0, 0.0, 0.0, 1.0, 100.0, 700.0],
Some(-36.0),
0.0,
-12.0,
false,
);
assert_eq!(turned.rotation, 180.0);
assert_eq!((turned.x, turned.width), (64.0, 36.0));
assert_eq!((turned.y, turned.height), (688.0, 12.0));
assert!(turned.advance_known);
let tight = run_geometry(
&[1.0, 0.0, 0.0, 1.0, 100.0, 700.0],
Some(-36.0),
0.0,
12.0,
false,
);
assert_eq!(tight.rotation, 0.0);
assert_eq!((tight.x, tight.width), (64.0, 36.0));
assert_eq!((tight.y, tight.height), (700.0, 12.0));
}
#[test]
fn baseline_rotation_reports_exact_cardinals_and_fractional_skews() {
assert_eq!(baseline_rotation(1.0, 0.0), 0.0);
assert_eq!(baseline_rotation(12.0, 0.0), 0.0);
assert_eq!(baseline_rotation(0.0, 1.0), 90.0);
assert_eq!(baseline_rotation(-1.0, 0.0), 180.0);
assert_eq!(baseline_rotation(0.0, -1.0), 270.0);
assert_eq!(baseline_rotation(0.0, 0.0), 0.0);
assert!((baseline_rotation(1.0, 1.0) - 45.0).abs() < 1e-3);
let skew = baseline_rotation(1.0, 0.01);
assert!(skew > 0.5 && skew < 0.6, "skew = {skew}");
assert_eq!(baseline_rotation(1.0, -1e-7), 0.0);
assert_eq!(normalize_degrees(-90.0), 270.0);
assert_eq!(normalize_degrees(360.0), 0.0);
}
}