use kurbo::{Affine, BezPath, PathEl, Point, Rect};
pub const MAX_POS: f64 = 32000.0;
#[must_use]
#[expect(
clippy::many_single_char_names,
reason = "a..d are the affine matrix coefficients, named as in the PDF `cm` operands"
)]
pub fn is_available_matrix(m: Affine) -> bool {
let [a, b, c, d, _, _] = m.as_coeffs();
if a == 0.0 || d == 0.0 {
return b != 0.0 && c != 0.0;
}
if b == 0.0 || c == 0.0 {
return a != 0.0 && d != 0.0;
}
true
}
#[must_use]
pub fn hard_clip(path: &BezPath) -> BezPath {
let clamp = |p: Point| Point::new(p.x.clamp(-MAX_POS, MAX_POS), p.y.clamp(-MAX_POS, MAX_POS));
let mut out = BezPath::new();
for el in path.elements() {
match *el {
PathEl::MoveTo(p) => out.move_to(clamp(p)),
PathEl::LineTo(p) => out.line_to(clamp(p)),
PathEl::QuadTo(a, b) => out.quad_to(clamp(a), clamp(b)),
PathEl::CurveTo(a, b, c) => out.curve_to(clamp(a), clamp(b), clamp(c)),
PathEl::ClosePath => out.close_path(),
}
}
out
}
#[must_use]
pub fn transform_hard_clip(matrix: Affine, path: &BezPath) -> BezPath {
let clamp = |p: Point| {
let p = matrix * p;
Point::new(p.x.clamp(-MAX_POS, MAX_POS), p.y.clamp(-MAX_POS, MAX_POS))
};
let mut out = BezPath::with_capacity(path.elements().len());
for el in path.elements() {
match *el {
PathEl::MoveTo(p) => out.move_to(clamp(p)),
PathEl::LineTo(p) => out.line_to(clamp(p)),
PathEl::QuadTo(a, b) => out.quad_to(clamp(a), clamp(b)),
PathEl::CurveTo(a, b, c) => out.curve_to(clamp(a), clamp(b), clamp(c)),
PathEl::ClosePath => out.close_path(),
}
}
out
}
#[must_use]
pub fn nudge_degenerate_subpaths(path: &BezPath, user: &BezPath) -> BezPath {
let els = path.elements();
let user_els = user.elements();
if els.len() != user_els.len() {
return path.clone();
}
let mut out = BezPath::with_capacity(els.len());
let mut skip_close = false;
for (i, el) in els.iter().enumerate() {
match *el {
PathEl::LineTo(p) if should_nudge(els, user_els, i) => {
out.line_to(Point::new(p.x + 1.0, p.y));
skip_close = matches!(els.get(i + 1), Some(PathEl::ClosePath));
}
PathEl::MoveTo(p) => out.move_to(p),
PathEl::LineTo(p) => out.line_to(p),
PathEl::QuadTo(a, b) => out.quad_to(a, b),
PathEl::CurveTo(a, b, c) => out.curve_to(a, b, c),
PathEl::ClosePath if skip_close => skip_close = false,
PathEl::ClosePath => out.close_path(),
}
}
out
}
fn should_nudge(els: &[PathEl], user: &[PathEl], i: usize) -> bool {
if !matches!(
(i > 0).then(|| els.get(i - 1)).flatten(),
Some(PathEl::MoveTo(_))
) {
return false;
}
let next_ends_subpath = match els.get(i + 1) {
None | Some(PathEl::MoveTo(_) | PathEl::ClosePath) => true,
Some(_) => false,
};
if !next_ends_subpath {
return false;
}
if matches!(els.get(i + 1), Some(PathEl::ClosePath))
&& !matches!(els.get(i + 2), None | Some(PathEl::MoveTo(_)))
{
return false;
}
let (Some(PathEl::MoveTo(um)), Some(PathEl::LineTo(ul))) =
((i > 0).then(|| user.get(i - 1)).flatten(), user.get(i))
else {
return false;
};
um == ul
}
#[derive(Debug, Clone, Copy)]
struct Points {
buf: [Point; Points::CAP],
len: usize,
}
impl Points {
const CAP: usize = 33;
fn new() -> Self {
Self {
buf: [Point::ZERO; Self::CAP],
len: 0,
}
}
fn push(&mut self, p: Point) -> Option<()> {
*self.buf.get_mut(self.len)? = p;
self.len += 1;
Some(())
}
fn as_slice(&self) -> &[Point] {
self.buf.get(..self.len).unwrap_or(&[])
}
}
fn rect_candidate_points(path: &BezPath) -> Option<Points> {
let mut points = Points::new();
let mut closed = false;
for el in path.elements() {
match *el {
PathEl::MoveTo(p) => {
if points.len != 0 {
return None; }
points.push(p)?;
}
PathEl::LineTo(p) => {
if points.len == 0 || closed {
return None;
}
points.push(p)?;
}
PathEl::ClosePath => closed = true,
PathEl::QuadTo(..) | PathEl::CurveTo(..) => return None,
}
if points.len > 32 {
return None; }
}
let slice = points.as_slice();
if closed && points.len >= 4 && slice.first() != slice.last() {
let first = *slice.first()?;
points.push(first)?;
}
Some(points)
}
fn normalize_points(points: &[Point]) -> Option<Points> {
let mut out = Points::new();
if points.len() <= 5 {
for &p in points {
out.push(p)?;
}
return Some(out);
}
if points.first() != points.last() {
return None;
}
out.push(*points.first()?)?;
for (i, p) in points.iter().enumerate().skip(1) {
if out.len + (points.len() - i) == 5 {
for &q in points.get(i..)? {
out.push(q)?;
}
break;
}
if out.as_slice().last() == Some(p) {
continue; }
out.push(*p)?;
if out.len > 5 {
return None;
}
}
(out.len == 5).then_some(out)
}
#[expect(
clippy::float_cmp,
reason = "upstream's `XYBothNotEqual` is bit-exact inequality: a rectangle \
corner is a corner only when the coordinates are literally the \
same value, and a tolerance would classify near-rectangles into \
the never-antialiased fast path PDFium sends through the \
general one"
)]
fn xy_both_differ(a: Point, b: Point) -> bool {
a.x != b.x && a.y != b.y
}
fn is_rect_pre_transform(points: &[Point]) -> bool {
if points.len() != 5 && points.len() != 4 {
return false;
}
let (Some(&p0), Some(&p1), Some(&p2), Some(&p3)) =
(points.first(), points.get(1), points.get(2), points.get(3))
else {
return false;
};
if points.len() == 5 && points.get(4) != Some(&p0) {
return false;
}
p0 != p2 && p1 != p3
}
#[must_use]
pub fn path_rect(path: &BezPath, matrix: Affine) -> Option<Rect> {
let candidate = rect_candidate_points(path)?;
let points = normalize_points(candidate.as_slice())?;
if !is_rect_pre_transform(points.as_slice()) {
return None;
}
let mut transformed = Points::new();
for &p in points.as_slice() {
transformed.push(matrix * p)?;
}
let transformed = transformed.as_slice();
for i in 1..transformed.len() {
let (Some(&cur), Some(&prev)) = (transformed.get(i), transformed.get(i - 1)) else {
return None;
};
if xy_both_differ(cur, prev) {
return None;
}
}
let (Some(&p0), Some(&p2), Some(&p3)) =
(transformed.first(), transformed.get(2), transformed.get(3))
else {
return None;
};
if xy_both_differ(p0, p3) {
return None;
}
Some(Rect::from_points(p0, p2))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IntRect {
pub left: i32,
pub top: i32,
pub right: i32,
pub bottom: i32,
}
impl IntRect {
#[must_use]
pub fn is_valid(self) -> bool {
self.right > self.left && self.bottom > self.top
}
#[must_use]
pub fn width(self) -> i32 {
self.right.saturating_sub(self.left)
}
#[must_use]
pub fn height(self) -> i32 {
self.bottom.saturating_sub(self.top)
}
#[must_use]
pub fn to_rect(self) -> Rect {
Rect::new(
f64::from(self.left),
f64::from(self.top),
f64::from(self.right),
f64::from(self.bottom),
)
}
#[must_use]
pub fn intersect(self, other: Self) -> Self {
Self {
left: self.left.max(other.left),
top: self.top.max(other.top),
right: self.right.min(other.right),
bottom: self.bottom.min(other.bottom),
}
}
}
#[must_use]
#[expect(
clippy::cast_possible_truncation,
reason = "the clamp bounds every finite coordinate to +/-i32::MAX/2 first, \
so the narrowing is exact; a NaN survives the clamp and Rust's \
saturating float-to-int cast turns it into 0, an empty rect"
)]
pub fn outer_rect(r: Rect) -> IntRect {
let clamp = |v: f64| v.clamp(f64::from(i32::MIN) / 2.0, f64::from(i32::MAX) / 2.0);
IntRect {
left: clamp(r.x0.floor()) as i32,
top: clamp(r.y0.floor()) as i32,
right: clamp(r.x1.ceil()) as i32,
bottom: clamp(r.y1.ceil()) as i32,
}
}
#[must_use]
#[expect(
clippy::cast_possible_truncation,
reason = "the two `ceil() as i32` narrowings are upstream's `(int)ceil(..)` \
on a rect the caller has already hard-clipped to +/-32000; Rust's \
saturating cast makes an out-of-range or NaN width 0 or i32::MAX, \
both of which the `< 1` promotion and the checked adds below \
handle without wrapping"
)]
pub fn snap_rect(rect_f: Rect) -> Option<IntRect> {
let mut rect_i = outer_rect(rect_f);
if !rect_i.is_valid() {
return None;
}
let mut width = (rect_f.x1 - rect_f.x0).ceil() as i32;
if width < 1 {
width = 1;
if rect_i.left == rect_i.right {
rect_i.right = rect_i.right.checked_add(1)?;
}
}
let mut height = (rect_f.y1 - rect_f.y0).ceil() as i32;
if height < 1 {
height = 1;
if rect_i.top == rect_i.bottom {
rect_i.bottom = rect_i.bottom.checked_add(1)?;
}
}
if rect_i.width() >= width.checked_add(1)? {
if rect_f.x0 - f64::from(rect_i.left) > f64::from(rect_i.right) - rect_f.x1 {
rect_i.left = rect_i.left.checked_add(1)?;
} else {
rect_i.right = rect_i.right.checked_sub(1)?;
}
}
if rect_i.height() >= height.checked_add(1)? {
if rect_f.y0 - f64::from(rect_i.top) > f64::from(rect_i.bottom) - rect_f.y1 {
rect_i.top = rect_i.top.checked_add(1)?;
} else {
rect_i.bottom = rect_i.bottom.checked_sub(1)?;
}
}
Some(rect_i)
}
#[cfg(test)]
mod tests {
use super::*;
use kurbo::Shape;
fn rect_path(x0: f64, y0: f64, x1: f64, y1: f64) -> BezPath {
let mut p = BezPath::new();
p.move_to((x0, y0));
p.line_to((x1, y0));
p.line_to((x1, y1));
p.line_to((x0, y1));
p.close_path();
p
}
#[test]
fn the_rect_candidate_buffer_rejects_rather_than_panicking() {
let mut long = BezPath::new();
long.move_to((0.0, 0.0));
for i in 1..100 {
long.line_to((f64::from(i), f64::from(i % 7)));
}
assert_eq!(path_rect(&long, Affine::IDENTITY), None);
let mut at_bound = BezPath::new();
at_bound.move_to((0.0, 0.0));
for i in 1..33 {
at_bound.line_to((f64::from(i), 0.0));
}
assert_eq!(path_rect(&at_bound, Affine::IDENTITY), None);
let mut padded = BezPath::new();
padded.move_to((0.0, 0.0));
for corner in [(10.0, 0.0), (10.0, 5.0), (0.0, 5.0)] {
for _ in 0..10 {
padded.line_to(corner);
}
}
padded.line_to((0.0, 5.0));
padded.close_path();
assert_eq!(
padded.elements().len() - 1,
32,
"the candidate must sit exactly on the guard for this to bite"
);
assert_eq!(
path_rect(&padded, Affine::IDENTITY),
Some(Rect::new(0.0, 0.0, 10.0, 5.0)),
"a rectangle written with repeated points must still be recognised"
);
assert!(
path_rect(&rect_path(0.0, 0.0, 10.0, 5.0), Affine::IDENTITY).is_some(),
"a closed rectangle must still be recognised"
);
const { assert!(Points::CAP == 33) };
}
#[test]
fn is_available_matrix_is_not_a_determinant_test() {
assert!(is_available_matrix(Affine::new([
1.0, 1.0, 1.0, 1.0, 0.0, 0.0
])));
assert!(is_available_matrix(Affine::IDENTITY));
assert!(is_available_matrix(Affine::new([
0.0, 1.0, -1.0, 0.0, 0.0, 0.0
])));
assert!(!is_available_matrix(Affine::new([
0.0, 0.0, 1.0, 1.0, 0.0, 0.0
])));
assert!(!is_available_matrix(Affine::new([
1.0, 0.0, 0.0, 0.0, 0.0, 0.0
])));
assert!(!is_available_matrix(Affine::new([0.0; 6])));
}
#[test]
fn path_rect_accepts_four_and_five_point_rects() {
let r = path_rect(&rect_path(1.0, 2.0, 5.0, 8.0), Affine::IDENTITY);
assert_eq!(r, Some(Rect::new(1.0, 2.0, 5.0, 8.0)));
}
#[test]
fn path_rect_rejects_a_45_degree_rotation_but_accepts_90() {
let p = rect_path(0.0, 0.0, 10.0, 4.0);
let quarter = Affine::new([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]);
assert_eq!(
path_rect(&p, quarter),
Some(Rect::new(-4.0, 0.0, 0.0, 10.0))
);
let eighth = Affine::rotate(std::f64::consts::FRAC_PI_4);
assert!(path_rect(&p, eighth).is_none(), "45 degrees is not a rect");
}
#[test]
fn path_rect_rejects_curves_and_degenerate_diagonals() {
let mut curved = BezPath::new();
curved.move_to((0.0, 0.0));
curved.curve_to((1.0, 0.0), (2.0, 0.0), (3.0, 0.0));
curved.close_path();
assert!(path_rect(&curved, Affine::IDENTITY).is_none());
let mut line = BezPath::new();
line.move_to((0.0, 0.0));
line.line_to((5.0, 0.0));
line.line_to((0.0, 0.0));
line.line_to((5.0, 0.0));
line.close_path();
assert!(path_rect(&line, Affine::IDENTITY).is_none());
}
#[test]
fn path_rect_normalizes_six_plus_points() {
let mut p = BezPath::new();
p.move_to((0.0, 0.0));
p.line_to((0.0, 0.0)); p.line_to((4.0, 0.0));
p.line_to((4.0, 3.0));
p.line_to((0.0, 3.0));
p.close_path();
assert_eq!(
path_rect(&p, Affine::IDENTITY),
Some(Rect::new(0.0, 0.0, 4.0, 3.0))
);
}
#[test]
fn rect_snap_promotes_sub_pixel_extents() {
let snapped = snap_rect(Rect::new(2.2, 5.0, 2.4, 9.0)).expect("snaps");
assert_eq!(snapped.width(), 1);
assert_eq!(snapped.left, 2);
assert_eq!(snapped.right, 3);
}
#[test]
fn rect_snap_rejects_an_exactly_zero_axis_before_promoting() {
assert_eq!(snap_rect(Rect::new(3.0, 1.0, 3.0, 4.0)), None);
assert_eq!(snap_rect(Rect::new(1.0, 2.0, 4.0, 2.0)), None);
}
#[test]
fn rect_snap_shrinks_the_wider_side() {
let snapped = snap_rect(Rect::new(1.4, 0.0, 4.6, 2.0)).expect("snaps");
assert_eq!((snapped.left, snapped.right), (1, 5));
let snapped = snap_rect(Rect::new(1.9, 0.0, 4.1, 2.0)).expect("snaps");
assert_eq!((snapped.left, snapped.right), (1, 4));
let snapped = snap_rect(Rect::new(1.95, 0.0, 5.95, 2.0)).expect("snaps");
assert_eq!((snapped.left, snapped.right), (2, 6));
}
#[test]
fn rect_snap_tie_goes_to_right_and_bottom() {
let snapped = snap_rect(Rect::new(1.5, 2.5, 4.5, 6.5)).expect("snaps");
assert_eq!((snapped.left, snapped.right), (1, 4));
assert_eq!((snapped.top, snapped.bottom), (2, 6));
}
#[test]
fn rect_snap_leaves_integer_rects_alone() {
let snapped = snap_rect(Rect::new(2.0, 3.0, 7.0, 11.0)).expect("snaps");
assert_eq!(
snapped,
IntRect {
left: 2,
top: 3,
right: 7,
bottom: 11
}
);
}
#[test]
#[expect(
clippy::float_cmp,
reason = "the clamp writes MAX_POS and copies 5.0/7.0 through verbatim; \
exact equality is what pins that no rounding crept in"
)]
fn hard_clip_clamps_rather_than_clips() {
let mut p = BezPath::new();
p.move_to((-99_999.0, 5.0));
p.line_to((99_999.0, 7.0));
let clipped = hard_clip(&p);
let bbox = clipped.bounding_box();
assert_eq!(bbox.x0, -MAX_POS);
assert_eq!(bbox.x1, MAX_POS);
assert_eq!(bbox.y0, 5.0);
assert_eq!(bbox.y1, 7.0);
}
fn shape(p: &BezPath) -> Vec<(&'static str, Option<(f64, f64)>)> {
p.elements()
.iter()
.map(|el| match *el {
PathEl::MoveTo(q) => ("m", Some((q.x, q.y))),
PathEl::LineTo(q) => ("l", Some((q.x, q.y))),
PathEl::QuadTo(_, q) => ("q", Some((q.x, q.y))),
PathEl::CurveTo(_, _, q) => ("c", Some((q.x, q.y))),
PathEl::ClosePath => ("h", None),
})
.collect()
}
#[test]
fn a_degenerate_open_subpath_is_nudged_one_pixel_right() {
let mut user = BezPath::new();
user.move_to((50.0, 40.0));
user.line_to((50.0, 40.0));
let device = Affine::scale(2.0) * user.clone();
let out = nudge_degenerate_subpaths(&device, &user);
assert_eq!(
shape(&out),
vec![("m", Some((100.0, 80.0))), ("l", Some((101.0, 80.0)))],
"one device pixel, not one user unit"
);
}
#[test]
fn a_degenerate_closed_subpath_is_nudged_and_loses_its_close() {
let mut user = BezPath::new();
user.move_to((50.0, 40.0));
user.line_to((50.0, 40.0));
user.close_path();
let out = nudge_degenerate_subpaths(&user, &user);
assert_eq!(
shape(&out),
vec![("m", Some((50.0, 40.0))), ("l", Some((51.0, 40.0)))]
);
}
#[test]
fn three_identical_points_are_not_nudged() {
let mut user = BezPath::new();
user.move_to((40.0, 140.0));
user.line_to((40.0, 140.0));
user.line_to((40.0, 140.0));
assert_eq!(
shape(&nudge_degenerate_subpaths(&user, &user)),
shape(&user)
);
}
#[test]
fn a_real_segment_is_left_alone() {
let mut user = BezPath::new();
user.move_to((10.0, 10.0));
user.line_to((20.0, 10.0));
assert_eq!(
shape(&nudge_degenerate_subpaths(&user, &user)),
shape(&user)
);
let mut two = BezPath::new();
two.move_to((10.0, 10.0));
two.line_to((20.0, 10.0));
two.line_to((20.0, 10.0));
assert_eq!(shape(&nudge_degenerate_subpaths(&two, &two)), shape(&two));
}
#[test]
fn every_degenerate_subpath_of_a_multi_subpath_path_is_nudged() {
let mut user = BezPath::new();
user.move_to((1.0, 1.0));
user.line_to((1.0, 1.0));
user.move_to((5.0, 5.0));
user.line_to((5.0, 5.0));
assert_eq!(
shape(&nudge_degenerate_subpaths(&user, &user)),
vec![
("m", Some((1.0, 1.0))),
("l", Some((2.0, 1.0))),
("m", Some((5.0, 5.0))),
("l", Some((6.0, 5.0))),
]
);
}
#[test]
fn outer_rect_rounds_outward() {
assert_eq!(
outer_rect(Rect::new(1.2, 2.8, 3.1, 4.0)),
IntRect {
left: 1,
top: 2,
right: 4,
bottom: 4
}
);
}
}