spectre_pdf 1.0.0

Native Rust PDF extraction engine: text, markdown for RAG, AcroForm widgets, image decoding, and encrypted PDFs. Lazy parser, persistent Document handle, no C dependencies.
Documentation
//! Shared geometry types used by the positional-extraction surfaces.
//! PDF user space: points (1/72"), origin bottom-left. `f32` throughout —
//! enough precision for any page-sized coordinate (max ~14400 pt).

/// Axis-aligned rectangle in PDF user space (points, bottom-left origin).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rect {
    pub x0: f32,
    pub y0: f32,
    pub x1: f32,
    pub y1: f32,
}

impl Rect {
    pub const ZERO: Self = Self {
        x0: 0.0,
        y0: 0.0,
        x1: 0.0,
        y1: 0.0,
    };

    /// Normalizes so x0 <= x1 and y0 <= y1.
    pub fn new(x0: f32, y0: f32, x1: f32, y1: f32) -> Self {
        Self {
            x0: x0.min(x1),
            y0: y0.min(y1),
            x1: x0.max(x1),
            y1: y0.max(y1),
        }
    }

    pub fn width(&self) -> f32 {
        self.x1 - self.x0
    }

    pub fn height(&self) -> f32 {
        self.y1 - self.y0
    }

    pub fn area(&self) -> f32 {
        self.width() * self.height()
    }

    /// Bounding rect of `self` and `other`.
    pub fn union(self, other: Rect) -> Rect {
        Rect {
            x0: self.x0.min(other.x0),
            y0: self.y0.min(other.y0),
            x1: self.x1.max(other.x1),
            y1: self.y1.max(other.y1),
        }
    }

    pub fn contains_point(&self, x: f32, y: f32) -> bool {
        x >= self.x0 && x <= self.x1 && y >= self.y0 && y <= self.y1
    }

    pub fn intersects(&self, other: &Rect) -> bool {
        self.x0 <= other.x1 && self.x1 >= other.x0 && self.y0 <= other.y1 && self.y1 >= other.y0
    }

    /// Flip from PDF bottom-left origin to screen top-left.
    pub fn flip_y(self, page_height: f32) -> Rect {
        Rect {
            x0: self.x0,
            x1: self.x1,
            y0: page_height - self.y1,
            y1: page_height - self.y0,
        }
    }
}

impl From<Rect> for (f32, f32, f32, f32) {
    fn from(r: Rect) -> Self {
        (r.x0, r.y0, r.x1, r.y1)
    }
}

/// 2×3 affine transform in PDF `cm`/`Tm` order: `[a b c d e f]`, applied
/// as `x' = a·x + c·y + e`, `y' = b·x + d·y + f`.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Matrix {
    pub a: f32,
    pub b: f32,
    pub c: f32,
    pub d: f32,
    pub e: f32,
    pub f: f32,
}

impl Matrix {
    pub const IDENTITY: Self = Self {
        a: 1.0,
        b: 0.0,
        c: 0.0,
        d: 1.0,
        e: 0.0,
        f: 0.0,
    };

    pub fn new(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> Self {
        Self { a, b, c, d, e, f }
    }

    pub fn translation(tx: f32, ty: f32) -> Self {
        Self {
            a: 1.0,
            b: 0.0,
            c: 0.0,
            d: 1.0,
            e: tx,
            f: ty,
        }
    }

    /// Pre-multiplication per PDF spec: `cm m1 cm m2 ⇒ CTM := m2 · m1`.
    pub fn premultiply(self, other: Matrix) -> Matrix {
        Matrix {
            a: other.a * self.a + other.b * self.c,
            b: other.a * self.b + other.b * self.d,
            c: other.c * self.a + other.d * self.c,
            d: other.c * self.b + other.d * self.d,
            e: other.e * self.a + other.f * self.c + self.e,
            f: other.e * self.b + other.f * self.d + self.f,
        }
    }

    pub fn transform_point(&self, x: f32, y: f32) -> (f32, f32) {
        (
            self.a * x + self.c * y + self.e,
            self.b * x + self.d * y + self.f,
        )
    }

    /// Horizontal scale magnitude (translation-free).
    pub fn scale_x(&self) -> f32 {
        (self.a * self.a + self.b * self.b).sqrt()
    }

    pub fn scale_y(&self) -> f32 {
        (self.c * self.c + self.d * self.d).sqrt()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rect_normalizes_inverted_input() {
        let r = Rect::new(10.0, 20.0, 0.0, 5.0);
        assert_eq!(r.x0, 0.0);
        assert_eq!(r.x1, 10.0);
        assert_eq!(r.y0, 5.0);
        assert_eq!(r.y1, 20.0);
    }

    #[test]
    fn rect_union_grows_bbox() {
        let a = Rect::new(0.0, 0.0, 10.0, 10.0);
        let b = Rect::new(5.0, -5.0, 15.0, 5.0);
        let u = a.union(b);
        assert_eq!(u, Rect::new(0.0, -5.0, 15.0, 10.0));
    }

    #[test]
    fn matrix_identity_round_trip() {
        let m = Matrix::IDENTITY;
        assert_eq!(m.transform_point(3.0, 4.0), (3.0, 4.0));
    }

    #[test]
    fn matrix_translation_premultiply() {
        let t = Matrix::translation(10.0, 20.0);
        assert_eq!(t.transform_point(0.0, 0.0), (10.0, 20.0));
        let t2 = t.premultiply(Matrix::translation(1.0, 2.0));
        assert_eq!(t2.transform_point(0.0, 0.0), (11.0, 22.0));
    }
}