oxidize_pdf/
coordinate_system.rs

1//! Coordinate system management for PDF rendering
2//!
3//! This module provides a flexible and extensible coordinate system framework
4//! for PDF generation, supporting multiple coordinate conventions while maintaining
5//! PDF standard compliance.
6//!
7//! # Coordinate Systems
8//!
9//! ## PDF Standard (Default)
10//! - Origin (0,0) at bottom-left corner
11//! - Y-axis increases upward (mathematical convention)
12//! - X-axis increases rightward
13//! - Units in points (1/72 inch)
14//!
15//! ## Screen Space
16//! - Origin (0,0) at top-left corner
17//! - Y-axis increases downward (screen convention)
18//! - X-axis increases rightward
19//! - Useful for developers familiar with web/screen graphics
20//!
21//! ## Custom Transform
22//! - User-defined transformation matrix
23//! - Allows arbitrary coordinate systems
24//! - Full control over scaling, rotation, and translation
25
26use crate::geometry::Point;
27
28/// Coordinate system types supported for rendering
29#[derive(Debug, Clone, Copy, PartialEq)]
30pub enum CoordinateSystem {
31    /// PDF standard: origin (0,0) at bottom-left, Y increases upward
32    /// This is the native PDF coordinate system per ISO 32000-1:2008
33    PdfStandard,
34
35    /// Screen-like: origin (0,0) at top-left, Y increases downward
36    /// Familiar to web developers and screen graphics programmers
37    ScreenSpace,
38
39    /// Custom transformation matrix for advanced use cases
40    Custom(TransformMatrix),
41}
42
43/// 2D transformation matrix in homogeneous coordinates
44///
45/// Represents a 3x3 matrix in the form:
46/// ```text
47/// [a c e]   [x]   [ax + cy + e]
48/// [b d f] × [y] = [bx + dy + f]
49/// [0 0 1]   [1]   [    1      ]
50/// ```
51///
52/// Common transformations:
53/// - Identity: `a=1, b=0, c=0, d=1, e=0, f=0`
54/// - Translation: `a=1, b=0, c=0, d=1, e=tx, f=ty`
55/// - Scale: `a=sx, b=0, c=0, d=sy, e=0, f=0`
56/// - Y-flip: `a=1, b=0, c=0, d=-1, e=0, f=page_height`
57#[derive(Debug, Clone, Copy, PartialEq)]
58pub struct TransformMatrix {
59    /// Scale/rotation X component
60    pub a: f64,
61    /// Skew Y component  
62    pub b: f64,
63    /// Skew X component
64    pub c: f64,
65    /// Scale/rotation Y component
66    pub d: f64,
67    /// Translation X component
68    pub e: f64,
69    /// Translation Y component
70    pub f: f64,
71}
72
73impl Default for CoordinateSystem {
74    fn default() -> Self {
75        Self::PdfStandard
76    }
77}
78
79impl TransformMatrix {
80    /// Identity transformation (no change)
81    pub const IDENTITY: Self = Self {
82        a: 1.0,
83        b: 0.0,
84        c: 0.0,
85        d: 1.0,
86        e: 0.0,
87        f: 0.0,
88    };
89
90    /// Create a new transformation matrix
91    pub fn new(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Self {
92        Self { a, b, c, d, e, f }
93    }
94
95    /// Create translation matrix
96    pub fn translate(tx: f64, ty: f64) -> Self {
97        Self {
98            a: 1.0,
99            b: 0.0,
100            c: 0.0,
101            d: 1.0,
102            e: tx,
103            f: ty,
104        }
105    }
106
107    /// Create scaling matrix
108    pub fn scale(sx: f64, sy: f64) -> Self {
109        Self {
110            a: sx,
111            b: 0.0,
112            c: 0.0,
113            d: sy,
114            e: 0.0,
115            f: 0.0,
116        }
117    }
118
119    /// Create rotation matrix (angle in radians)
120    pub fn rotate(angle: f64) -> Self {
121        let cos = angle.cos();
122        let sin = angle.sin();
123        Self {
124            a: cos,
125            b: sin,
126            c: -sin,
127            d: cos,
128            e: 0.0,
129            f: 0.0,
130        }
131    }
132
133    /// Create Y-axis flip transformation for given page height
134    pub fn flip_y(page_height: f64) -> Self {
135        Self {
136            a: 1.0,
137            b: 0.0,
138            c: 0.0,
139            d: -1.0,
140            e: 0.0,
141            f: page_height,
142        }
143    }
144
145    /// Matrix multiplication: self * other
146    pub fn multiply(&self, other: &TransformMatrix) -> Self {
147        Self {
148            a: self.a * other.a + self.c * other.b,
149            b: self.b * other.a + self.d * other.b,
150            c: self.a * other.c + self.c * other.d,
151            d: self.b * other.c + self.d * other.d,
152            e: self.a * other.e + self.c * other.f + self.e,
153            f: self.b * other.e + self.d * other.f + self.f,
154        }
155    }
156
157    /// Transform a point using this matrix
158    pub fn transform_point(&self, point: Point) -> Point {
159        Point::new(
160            self.a * point.x + self.c * point.y + self.e,
161            self.b * point.x + self.d * point.y + self.f,
162        )
163    }
164
165    /// Convert to PDF CTM (Current Transformation Matrix) string
166    pub fn to_pdf_ctm(&self) -> String {
167        format!(
168            "{:.6} {:.6} {:.6} {:.6} {:.6} {:.6} cm",
169            self.a, self.b, self.c, self.d, self.e, self.f
170        )
171    }
172}
173
174impl CoordinateSystem {
175    /// Get transformation matrix to convert from this system to PDF standard
176    pub fn to_pdf_standard_matrix(&self, page_height: f64) -> TransformMatrix {
177        match *self {
178            Self::PdfStandard => TransformMatrix::IDENTITY,
179            Self::ScreenSpace => TransformMatrix::flip_y(page_height),
180            Self::Custom(matrix) => matrix,
181        }
182    }
183
184    /// Convert a point from this coordinate system to PDF standard
185    pub fn to_pdf_standard(&self, point: Point, page_height: f64) -> Point {
186        let matrix = self.to_pdf_standard_matrix(page_height);
187        matrix.transform_point(point)
188    }
189
190    /// Convert a Y coordinate specifically (common operation)
191    pub fn y_to_pdf_standard(&self, y: f64, page_height: f64) -> f64 {
192        match *self {
193            Self::PdfStandard => y,
194            Self::ScreenSpace => page_height - y,
195            Self::Custom(matrix) => {
196                // For custom matrices, we need to transform a point
197                let transformed = matrix.transform_point(Point::new(0.0, y));
198                transformed.y
199            }
200        }
201    }
202
203    /// Check if this coordinate system grows upward (like PDF standard)
204    pub fn grows_upward(&self) -> bool {
205        match *self {
206            Self::PdfStandard => true,
207            Self::ScreenSpace => false,
208            Self::Custom(matrix) => matrix.d > 0.0, // Positive Y scaling
209        }
210    }
211}
212
213/// Rendering context that maintains coordinate system state
214#[derive(Debug)]
215pub struct RenderContext {
216    /// Active coordinate system
217    pub coordinate_system: CoordinateSystem,
218    /// Page dimensions
219    pub page_width: f64,
220    pub page_height: f64,
221    /// Current transformation matrix
222    pub current_transform: TransformMatrix,
223}
224
225impl RenderContext {
226    /// Create a new render context
227    pub fn new(coordinate_system: CoordinateSystem, page_width: f64, page_height: f64) -> Self {
228        let current_transform = coordinate_system.to_pdf_standard_matrix(page_height);
229
230        Self {
231            coordinate_system,
232            page_width,
233            page_height,
234            current_transform,
235        }
236    }
237
238    /// Transform a point to PDF standard coordinates
239    pub fn to_pdf_standard(&self, point: Point) -> Point {
240        self.coordinate_system
241            .to_pdf_standard(point, self.page_height)
242    }
243
244    /// Transform Y coordinate to PDF standard
245    pub fn y_to_pdf(&self, y: f64) -> f64 {
246        self.coordinate_system
247            .y_to_pdf_standard(y, self.page_height)
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::geometry::Point;
255
256    #[test]
257    fn test_transform_matrix_identity() {
258        let identity = TransformMatrix::IDENTITY;
259        let point = Point::new(10.0, 20.0);
260        let transformed = identity.transform_point(point);
261
262        assert_eq!(transformed, point);
263    }
264
265    #[test]
266    fn test_transform_matrix_translate() {
267        let translate = TransformMatrix::translate(5.0, 10.0);
268        let point = Point::new(1.0, 2.0);
269        let transformed = translate.transform_point(point);
270
271        assert_eq!(transformed, Point::new(6.0, 12.0));
272    }
273
274    #[test]
275    fn test_transform_matrix_scale() {
276        let scale = TransformMatrix::scale(2.0, 3.0);
277        let point = Point::new(4.0, 5.0);
278        let transformed = scale.transform_point(point);
279
280        assert_eq!(transformed, Point::new(8.0, 15.0));
281    }
282
283    #[test]
284    fn test_coordinate_system_pdf_standard() {
285        let coord_system = CoordinateSystem::PdfStandard;
286        let page_height = 842.0;
287        let point = Point::new(100.0, 200.0);
288
289        let pdf_point = coord_system.to_pdf_standard(point, page_height);
290        assert_eq!(pdf_point, point); // Should be unchanged
291    }
292
293    #[test]
294    fn test_coordinate_system_screen_space() {
295        let coord_system = CoordinateSystem::ScreenSpace;
296        let page_height = 842.0;
297        let point = Point::new(100.0, 200.0);
298
299        let pdf_point = coord_system.to_pdf_standard(point, page_height);
300        assert_eq!(pdf_point, Point::new(100.0, 642.0)); // Y flipped
301    }
302
303    #[test]
304    fn test_y_flip_matrix() {
305        let page_height = 800.0;
306        let flip = TransformMatrix::flip_y(page_height);
307
308        // Top of page (screen coords) -> bottom of page (PDF coords)
309        let top_screen = Point::new(0.0, 0.0);
310        let top_pdf = flip.transform_point(top_screen);
311        assert_eq!(top_pdf, Point::new(0.0, 800.0));
312
313        // Bottom of page (screen coords) -> top of page (PDF coords)
314        let bottom_screen = Point::new(0.0, 800.0);
315        let bottom_pdf = flip.transform_point(bottom_screen);
316        assert_eq!(bottom_pdf, Point::new(0.0, 0.0));
317    }
318
319    #[test]
320    fn test_render_context() {
321        let context = RenderContext::new(CoordinateSystem::ScreenSpace, 595.0, 842.0);
322
323        let screen_point = Point::new(100.0, 100.0);
324        let pdf_point = context.to_pdf_standard(screen_point);
325
326        assert_eq!(pdf_point, Point::new(100.0, 742.0));
327    }
328}