oxipdf-ir 0.1.0

Intermediate representation types for the oxipdf PDF engine
Documentation
//! 2D transform properties: translate, rotate, scale.

use crate::units::Pt;

/// A 2D affine transform applied to an element.
///
/// Transforms are applied in order: translate → rotate → scale,
/// relative to the `transform_origin`.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Transform {
    /// Horizontal translation.
    pub translate_x: Pt,
    /// Vertical translation.
    pub translate_y: Pt,
    /// Rotation in degrees (clockwise).
    pub rotate_degrees: f64,
    /// Horizontal scale factor (1.0 = no scale).
    pub scale_x: f64,
    /// Vertical scale factor (1.0 = no scale).
    pub scale_y: f64,
}

impl Default for Transform {
    fn default() -> Self {
        Self {
            translate_x: Pt::ZERO,
            translate_y: Pt::ZERO,
            rotate_degrees: 0.0,
            scale_x: 1.0,
            scale_y: 1.0,
        }
    }
}

impl Transform {
    /// Compute the 6-element PDF CTM matrix [a, b, c, d, e, f].
    ///
    /// The matrix represents: translate(e,f) · rotate(θ) · scale(sx,sy).
    #[must_use]
    pub fn to_pdf_matrix(&self) -> [f32; 6] {
        let theta = self.rotate_degrees.to_radians();
        let cos_t = theta.cos() as f32;
        let sin_t = theta.sin() as f32;
        let sx = self.scale_x as f32;
        let sy = self.scale_y as f32;

        [
            cos_t * sx,
            sin_t * sx,
            -sin_t * sy,
            cos_t * sy,
            self.translate_x.get() as f32,
            self.translate_y.get() as f32,
        ]
    }

    /// Returns true if this transform has no effect.
    #[must_use]
    pub fn is_identity(&self) -> bool {
        self.translate_x.get() == 0.0
            && self.translate_y.get() == 0.0
            && self.rotate_degrees == 0.0
            && (self.scale_x - 1.0).abs() < 1e-6
            && (self.scale_y - 1.0).abs() < 1e-6
    }
}

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

    #[test]
    fn identity_transform() {
        let t = Transform::default();
        assert!(t.is_identity());
        let m = t.to_pdf_matrix();
        assert!((m[0] - 1.0).abs() < 0.001); // a = 1
        assert!((m[3] - 1.0).abs() < 0.001); // d = 1
    }

    #[test]
    fn translate_only() {
        let t = Transform {
            translate_x: Pt::new(10.0),
            translate_y: Pt::new(20.0),
            ..Transform::default()
        };
        assert!(!t.is_identity());
        let m = t.to_pdf_matrix();
        assert!((m[4] - 10.0).abs() < 0.001);
        assert!((m[5] - 20.0).abs() < 0.001);
    }

    #[test]
    fn scale_only() {
        let t = Transform {
            scale_x: 2.0,
            scale_y: 0.5,
            ..Transform::default()
        };
        let m = t.to_pdf_matrix();
        assert!((m[0] - 2.0).abs() < 0.001);
        assert!((m[3] - 0.5).abs() < 0.001);
    }
}