Skip to main content

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/// Where `[x]`, `[y]`, and `[1]` represent the input vector.
53///
54/// Common transformations:
55/// - Identity: `a=1, b=0, c=0, d=1, e=0, f=0`
56/// - Translation: `a=1, b=0, c=0, d=1, e=tx, f=ty`
57/// - Scale: `a=sx, b=0, c=0, d=sy, e=0, f=0`
58/// - Y-flip: `a=1, b=0, c=0, d=-1, e=0, f=page_height`
59#[derive(Debug, Clone, Copy, PartialEq)]
60pub struct TransformMatrix {
61    /// Scale/rotation X component
62    pub a: f64,
63    /// Skew Y component  
64    pub b: f64,
65    /// Skew X component
66    pub c: f64,
67    /// Scale/rotation Y component
68    pub d: f64,
69    /// Translation X component
70    pub e: f64,
71    /// Translation Y component
72    pub f: f64,
73}
74
75impl Default for CoordinateSystem {
76    fn default() -> Self {
77        Self::PdfStandard
78    }
79}
80
81impl TransformMatrix {
82    /// Identity transformation (no change)
83    pub const IDENTITY: Self = Self {
84        a: 1.0,
85        b: 0.0,
86        c: 0.0,
87        d: 1.0,
88        e: 0.0,
89        f: 0.0,
90    };
91
92    /// Create a new transformation matrix
93    pub fn new(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Self {
94        Self { a, b, c, d, e, f }
95    }
96
97    /// Create translation matrix
98    pub fn translate(tx: f64, ty: f64) -> Self {
99        Self {
100            a: 1.0,
101            b: 0.0,
102            c: 0.0,
103            d: 1.0,
104            e: tx,
105            f: ty,
106        }
107    }
108
109    /// Create scaling matrix
110    pub fn scale(sx: f64, sy: f64) -> Self {
111        Self {
112            a: sx,
113            b: 0.0,
114            c: 0.0,
115            d: sy,
116            e: 0.0,
117            f: 0.0,
118        }
119    }
120
121    /// Create rotation matrix (angle in radians)
122    pub fn rotate(angle: f64) -> Self {
123        let cos = angle.cos();
124        let sin = angle.sin();
125        Self {
126            a: cos,
127            b: sin,
128            c: -sin,
129            d: cos,
130            e: 0.0,
131            f: 0.0,
132        }
133    }
134
135    /// Create Y-axis flip transformation for given page height
136    pub fn flip_y(page_height: f64) -> Self {
137        Self {
138            a: 1.0,
139            b: 0.0,
140            c: 0.0,
141            d: -1.0,
142            e: 0.0,
143            f: page_height,
144        }
145    }
146
147    /// Matrix multiplication: self * other
148    pub fn multiply(&self, other: &TransformMatrix) -> Self {
149        Self {
150            a: self.a * other.a + self.c * other.b,
151            b: self.b * other.a + self.d * other.b,
152            c: self.a * other.c + self.c * other.d,
153            d: self.b * other.c + self.d * other.d,
154            e: self.a * other.e + self.c * other.f + self.e,
155            f: self.b * other.e + self.d * other.f + self.f,
156        }
157    }
158
159    /// Transform a point using this matrix
160    pub fn transform_point(&self, point: Point) -> Point {
161        Point::new(
162            self.a * point.x + self.c * point.y + self.e,
163            self.b * point.x + self.d * point.y + self.f,
164        )
165    }
166
167    /// Convert to PDF CTM (Current Transformation Matrix) string
168    pub fn to_pdf_ctm(&self) -> String {
169        format!(
170            "{:.6} {:.6} {:.6} {:.6} {:.6} {:.6} cm",
171            self.a, self.b, self.c, self.d, self.e, self.f
172        )
173    }
174}
175
176impl CoordinateSystem {
177    /// Get transformation matrix to convert from this system to PDF standard
178    pub fn to_pdf_standard_matrix(&self, page_height: f64) -> TransformMatrix {
179        match *self {
180            Self::PdfStandard => TransformMatrix::IDENTITY,
181            Self::ScreenSpace => TransformMatrix::flip_y(page_height),
182            Self::Custom(matrix) => matrix,
183        }
184    }
185
186    /// Convert a point from this coordinate system to PDF standard
187    pub fn to_pdf_standard(&self, point: Point, page_height: f64) -> Point {
188        let matrix = self.to_pdf_standard_matrix(page_height);
189        matrix.transform_point(point)
190    }
191
192    /// Convert a Y coordinate specifically (common operation)
193    pub fn y_to_pdf_standard(&self, y: f64, page_height: f64) -> f64 {
194        match *self {
195            Self::PdfStandard => y,
196            Self::ScreenSpace => page_height - y,
197            Self::Custom(matrix) => {
198                // For custom matrices, we need to transform a point
199                let transformed = matrix.transform_point(Point::new(0.0, y));
200                transformed.y
201            }
202        }
203    }
204
205    /// Check if this coordinate system grows upward (like PDF standard)
206    pub fn grows_upward(&self) -> bool {
207        match *self {
208            Self::PdfStandard => true,
209            Self::ScreenSpace => false,
210            Self::Custom(matrix) => matrix.d > 0.0, // Positive Y scaling
211        }
212    }
213}
214
215/// Rendering context that maintains coordinate system state
216#[derive(Debug)]
217pub struct RenderContext {
218    /// Active coordinate system
219    pub coordinate_system: CoordinateSystem,
220    /// Page dimensions
221    pub page_width: f64,
222    pub page_height: f64,
223    /// Current transformation matrix
224    pub current_transform: TransformMatrix,
225}
226
227impl RenderContext {
228    /// Create a new render context
229    pub fn new(coordinate_system: CoordinateSystem, page_width: f64, page_height: f64) -> Self {
230        let current_transform = coordinate_system.to_pdf_standard_matrix(page_height);
231
232        Self {
233            coordinate_system,
234            page_width,
235            page_height,
236            current_transform,
237        }
238    }
239
240    /// Transform a point to PDF standard coordinates
241    pub fn to_pdf_standard(&self, point: Point) -> Point {
242        self.coordinate_system
243            .to_pdf_standard(point, self.page_height)
244    }
245
246    /// Transform Y coordinate to PDF standard
247    pub fn y_to_pdf(&self, y: f64) -> f64 {
248        self.coordinate_system
249            .y_to_pdf_standard(y, self.page_height)
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use crate::geometry::Point;
257
258    #[test]
259    fn test_transform_matrix_identity() {
260        let identity = TransformMatrix::IDENTITY;
261        let point = Point::new(10.0, 20.0);
262        let transformed = identity.transform_point(point);
263
264        assert_eq!(transformed, point);
265    }
266
267    #[test]
268    fn test_transform_matrix_translate() {
269        let translate = TransformMatrix::translate(5.0, 10.0);
270        let point = Point::new(1.0, 2.0);
271        let transformed = translate.transform_point(point);
272
273        assert_eq!(transformed, Point::new(6.0, 12.0));
274    }
275
276    #[test]
277    fn test_transform_matrix_scale() {
278        let scale = TransformMatrix::scale(2.0, 3.0);
279        let point = Point::new(4.0, 5.0);
280        let transformed = scale.transform_point(point);
281
282        assert_eq!(transformed, Point::new(8.0, 15.0));
283    }
284
285    #[test]
286    fn test_coordinate_system_pdf_standard() {
287        let coord_system = CoordinateSystem::PdfStandard;
288        let page_height = 842.0;
289        let point = Point::new(100.0, 200.0);
290
291        let pdf_point = coord_system.to_pdf_standard(point, page_height);
292        assert_eq!(pdf_point, point); // Should be unchanged
293    }
294
295    #[test]
296    fn test_coordinate_system_screen_space() {
297        let coord_system = CoordinateSystem::ScreenSpace;
298        let page_height = 842.0;
299        let point = Point::new(100.0, 200.0);
300
301        let pdf_point = coord_system.to_pdf_standard(point, page_height);
302        assert_eq!(pdf_point, Point::new(100.0, 642.0)); // Y flipped
303    }
304
305    #[test]
306    fn test_y_flip_matrix() {
307        let page_height = 800.0;
308        let flip = TransformMatrix::flip_y(page_height);
309
310        // Top of page (screen coords) -> bottom of page (PDF coords)
311        let top_screen = Point::new(0.0, 0.0);
312        let top_pdf = flip.transform_point(top_screen);
313        assert_eq!(top_pdf, Point::new(0.0, 800.0));
314
315        // Bottom of page (screen coords) -> top of page (PDF coords)
316        let bottom_screen = Point::new(0.0, 800.0);
317        let bottom_pdf = flip.transform_point(bottom_screen);
318        assert_eq!(bottom_pdf, Point::new(0.0, 0.0));
319    }
320
321    #[test]
322    fn test_render_context() {
323        let context = RenderContext::new(CoordinateSystem::ScreenSpace, 595.0, 842.0);
324
325        let screen_point = Point::new(100.0, 100.0);
326        let pdf_point = context.to_pdf_standard(screen_point);
327
328        assert_eq!(pdf_point, Point::new(100.0, 742.0));
329    }
330
331    // =============================================================================
332    // RIGOROUS TESTS FOR TransformMatrix
333    // =============================================================================
334
335    #[test]
336    fn test_transform_matrix_rotate_90_degrees() {
337        let rotate = TransformMatrix::rotate(std::f64::consts::FRAC_PI_2); // 90 degrees
338        let point = Point::new(1.0, 0.0);
339        let transformed = rotate.transform_point(point);
340
341        // (1,0) rotated 90° should be (0,1)
342        assert!((transformed.x - 0.0).abs() < 1e-10, "X should be ~0");
343        assert!((transformed.y - 1.0).abs() < 1e-10, "Y should be ~1");
344    }
345
346    #[test]
347    fn test_transform_matrix_rotate_180_degrees() {
348        let rotate = TransformMatrix::rotate(std::f64::consts::PI); // 180 degrees
349        let point = Point::new(1.0, 0.0);
350        let transformed = rotate.transform_point(point);
351
352        // (1,0) rotated 180° should be (-1,0)
353        assert!((transformed.x - (-1.0)).abs() < 1e-10, "X should be ~-1");
354        assert!((transformed.y - 0.0).abs() < 1e-10, "Y should be ~0");
355    }
356
357    #[test]
358    fn test_transform_matrix_rotate_270_degrees() {
359        let rotate = TransformMatrix::rotate(3.0 * std::f64::consts::FRAC_PI_2); // 270 degrees
360        let point = Point::new(1.0, 0.0);
361        let transformed = rotate.transform_point(point);
362
363        // (1,0) rotated 270° should be (0,-1)
364        assert!((transformed.x - 0.0).abs() < 1e-10, "X should be ~0");
365        assert!((transformed.y - (-1.0)).abs() < 1e-10, "Y should be ~-1");
366    }
367
368    #[test]
369    fn test_transform_matrix_multiply_identity() {
370        let matrix = TransformMatrix::new(2.0, 3.0, 4.0, 5.0, 6.0, 7.0);
371        let result = matrix.multiply(&TransformMatrix::IDENTITY);
372
373        // Multiplying by identity should return the same matrix
374        assert_eq!(result.a, 2.0);
375        assert_eq!(result.b, 3.0);
376        assert_eq!(result.c, 4.0);
377        assert_eq!(result.d, 5.0);
378        assert_eq!(result.e, 6.0);
379        assert_eq!(result.f, 7.0);
380    }
381
382    #[test]
383    fn test_transform_matrix_multiply_translate_then_scale() {
384        let translate = TransformMatrix::translate(10.0, 20.0);
385        let scale = TransformMatrix::scale(2.0, 3.0);
386
387        // Translate then scale: scale is applied first, then translate
388        let combined = translate.multiply(&scale);
389        let point = Point::new(5.0, 5.0);
390        let transformed = combined.transform_point(point);
391
392        // Point (5,5) scaled by (2,3) = (10,15), then translated by (10,20) = (20,35)
393        assert_eq!(transformed.x, 20.0);
394        assert_eq!(transformed.y, 35.0);
395    }
396
397    #[test]
398    fn test_transform_matrix_multiply_scale_then_translate() {
399        let scale = TransformMatrix::scale(2.0, 3.0);
400        let translate = TransformMatrix::translate(10.0, 20.0);
401
402        // Scale then translate: translate is applied first, then scale
403        let combined = scale.multiply(&translate);
404        let point = Point::new(5.0, 5.0);
405        let transformed = combined.transform_point(point);
406
407        // Point (5,5) translated by (10,20) = (15,25), then scaled by (2,3) = (30,75)
408        assert_eq!(transformed.x, 30.0);
409        assert_eq!(transformed.y, 75.0);
410    }
411
412    #[test]
413    fn test_transform_matrix_to_pdf_ctm() {
414        let matrix = TransformMatrix::new(1.5, 0.5, -0.5, 2.0, 10.0, 20.0);
415        let ctm = matrix.to_pdf_ctm();
416
417        assert_eq!(
418            ctm,
419            "1.500000 0.500000 -0.500000 2.000000 10.000000 20.000000 cm"
420        );
421    }
422
423    #[test]
424    fn test_transform_matrix_to_pdf_ctm_with_precision() {
425        let matrix = TransformMatrix::new(
426            0.123456789,
427            0.987654321,
428            -0.111111111,
429            0.222222222,
430            100.123456,
431            200.987654,
432        );
433        let ctm = matrix.to_pdf_ctm();
434
435        // Should round to 6 decimal places
436        assert!(ctm.contains("0.123457")); // Rounded up from 0.123456789
437        assert!(ctm.contains("0.987654")); // Rounded down from 0.987654321
438        assert!(ctm.contains("-0.111111"));
439        assert!(ctm.contains("0.222222"));
440        assert!(ctm.contains("100.123456"));
441        assert!(ctm.contains("200.987654"));
442        assert!(ctm.ends_with(" cm"));
443    }
444
445    #[test]
446    fn test_transform_matrix_flip_y_zero_height() {
447        let flip = TransformMatrix::flip_y(0.0);
448        let point = Point::new(100.0, 50.0);
449        let transformed = flip.transform_point(point);
450
451        // With page_height=0, flip should map (100,50) to (100,-50)
452        assert_eq!(transformed.x, 100.0);
453        assert_eq!(transformed.y, -50.0);
454    }
455
456    #[test]
457    fn test_transform_matrix_flip_y_negative_height() {
458        let flip = TransformMatrix::flip_y(-100.0);
459        let point = Point::new(50.0, 25.0);
460        let transformed = flip.transform_point(point);
461
462        // With page_height=-100, flip should map (50,25) to (50,-125)
463        assert_eq!(transformed.x, 50.0);
464        assert_eq!(transformed.y, -125.0);
465    }
466
467    #[test]
468    fn test_transform_matrix_scale_zero() {
469        let scale = TransformMatrix::scale(0.0, 0.0);
470        let point = Point::new(100.0, 200.0);
471        let transformed = scale.transform_point(point);
472
473        // Scaling by zero should collapse point to origin
474        assert_eq!(transformed.x, 0.0);
475        assert_eq!(transformed.y, 0.0);
476    }
477
478    #[test]
479    fn test_transform_matrix_scale_negative() {
480        let scale = TransformMatrix::scale(-1.0, -2.0);
481        let point = Point::new(10.0, 20.0);
482        let transformed = scale.transform_point(point);
483
484        // Negative scaling should flip and scale
485        assert_eq!(transformed.x, -10.0);
486        assert_eq!(transformed.y, -40.0);
487    }
488
489    #[test]
490    fn test_transform_matrix_translate_zero() {
491        let translate = TransformMatrix::translate(0.0, 0.0);
492        let point = Point::new(50.0, 75.0);
493        let transformed = translate.transform_point(point);
494
495        // Zero translation should not change point
496        assert_eq!(transformed, point);
497    }
498
499    #[test]
500    fn test_transform_matrix_translate_negative() {
501        let translate = TransformMatrix::translate(-10.0, -20.0);
502        let point = Point::new(100.0, 200.0);
503        let transformed = translate.transform_point(point);
504
505        assert_eq!(transformed.x, 90.0);
506        assert_eq!(transformed.y, 180.0);
507    }
508
509    // =============================================================================
510    // RIGOROUS TESTS FOR CoordinateSystem
511    // =============================================================================
512
513    #[test]
514    fn test_coordinate_system_default() {
515        let default_cs = CoordinateSystem::default();
516        assert!(
517            matches!(default_cs, CoordinateSystem::PdfStandard),
518            "Default should be PdfStandard"
519        );
520    }
521
522    #[test]
523    fn test_coordinate_system_pdf_standard_identity() {
524        let cs = CoordinateSystem::PdfStandard;
525        let matrix = cs.to_pdf_standard_matrix(500.0);
526
527        // PdfStandard should return identity matrix
528        assert_eq!(matrix.a, 1.0);
529        assert_eq!(matrix.b, 0.0);
530        assert_eq!(matrix.c, 0.0);
531        assert_eq!(matrix.d, 1.0);
532        assert_eq!(matrix.e, 0.0);
533        assert_eq!(matrix.f, 0.0);
534    }
535
536    #[test]
537    fn test_coordinate_system_screen_space_flip() {
538        let cs = CoordinateSystem::ScreenSpace;
539        let page_height = 600.0;
540        let matrix = cs.to_pdf_standard_matrix(page_height);
541
542        // ScreenSpace should flip Y-axis
543        assert_eq!(matrix.a, 1.0);
544        assert_eq!(matrix.b, 0.0);
545        assert_eq!(matrix.c, 0.0);
546        assert_eq!(matrix.d, -1.0);
547        assert_eq!(matrix.e, 0.0);
548        assert_eq!(matrix.f, page_height);
549    }
550
551    #[test]
552    fn test_coordinate_system_custom_matrix() {
553        let custom_matrix = TransformMatrix::new(2.0, 0.0, 0.0, 2.0, 50.0, 100.0);
554        let cs = CoordinateSystem::Custom(custom_matrix);
555
556        let retrieved_matrix = cs.to_pdf_standard_matrix(500.0);
557
558        // Custom should return the exact matrix provided
559        assert_eq!(retrieved_matrix.a, 2.0);
560        assert_eq!(retrieved_matrix.b, 0.0);
561        assert_eq!(retrieved_matrix.c, 0.0);
562        assert_eq!(retrieved_matrix.d, 2.0);
563        assert_eq!(retrieved_matrix.e, 50.0);
564        assert_eq!(retrieved_matrix.f, 100.0);
565    }
566
567    #[test]
568    fn test_coordinate_system_y_to_pdf_standard_pdf_standard() {
569        let cs = CoordinateSystem::PdfStandard;
570        let y = 200.0;
571        let page_height = 842.0;
572
573        let pdf_y = cs.y_to_pdf_standard(y, page_height);
574
575        // PdfStandard should not change Y
576        assert_eq!(pdf_y, 200.0);
577    }
578
579    #[test]
580    fn test_coordinate_system_y_to_pdf_standard_screen_space() {
581        let cs = CoordinateSystem::ScreenSpace;
582        let y = 200.0;
583        let page_height = 842.0;
584
585        let pdf_y = cs.y_to_pdf_standard(y, page_height);
586
587        // ScreenSpace should flip Y: 842 - 200 = 642
588        assert_eq!(pdf_y, 642.0);
589    }
590
591    #[test]
592    fn test_coordinate_system_y_to_pdf_standard_custom() {
593        let custom_matrix = TransformMatrix::new(1.0, 0.0, 0.0, -2.0, 0.0, 500.0);
594        let cs = CoordinateSystem::Custom(custom_matrix);
595        let y = 100.0;
596        let page_height = 600.0; // Not used for custom
597
598        let pdf_y = cs.y_to_pdf_standard(y, page_height);
599
600        // Custom matrix transforms (0, 100): y' = b*0 + d*100 + f = 0*0 + (-2)*100 + 500 = 300
601        assert_eq!(pdf_y, 300.0);
602    }
603
604    #[test]
605    fn test_coordinate_system_grows_upward_pdf_standard() {
606        let cs = CoordinateSystem::PdfStandard;
607        assert!(cs.grows_upward(), "PdfStandard should grow upward");
608    }
609
610    #[test]
611    fn test_coordinate_system_grows_upward_screen_space() {
612        let cs = CoordinateSystem::ScreenSpace;
613        assert!(
614            !cs.grows_upward(),
615            "ScreenSpace should NOT grow upward (Y increases downward)"
616        );
617    }
618
619    #[test]
620    fn test_coordinate_system_grows_upward_custom_positive_d() {
621        let custom_matrix = TransformMatrix::new(1.0, 0.0, 0.0, 2.0, 0.0, 0.0);
622        let cs = CoordinateSystem::Custom(custom_matrix);
623        assert!(
624            cs.grows_upward(),
625            "Custom with positive d should grow upward"
626        );
627    }
628
629    #[test]
630    fn test_coordinate_system_grows_upward_custom_negative_d() {
631        let custom_matrix = TransformMatrix::new(1.0, 0.0, 0.0, -1.0, 0.0, 100.0);
632        let cs = CoordinateSystem::Custom(custom_matrix);
633        assert!(
634            !cs.grows_upward(),
635            "Custom with negative d should NOT grow upward"
636        );
637    }
638
639    #[test]
640    fn test_coordinate_system_grows_upward_custom_zero_d() {
641        let custom_matrix = TransformMatrix::new(1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
642        let cs = CoordinateSystem::Custom(custom_matrix);
643        assert!(
644            !cs.grows_upward(),
645            "Custom with zero d should NOT grow upward"
646        );
647    }
648
649    // =============================================================================
650    // RIGOROUS TESTS FOR RenderContext
651    // =============================================================================
652
653    #[test]
654    fn test_render_context_new_pdf_standard() {
655        let context = RenderContext::new(CoordinateSystem::PdfStandard, 595.0, 842.0);
656
657        assert_eq!(context.page_width, 595.0);
658        assert_eq!(context.page_height, 842.0);
659        assert!(matches!(
660            context.coordinate_system,
661            CoordinateSystem::PdfStandard
662        ));
663
664        // Transform should be identity for PdfStandard
665        assert_eq!(context.current_transform.a, 1.0);
666        assert_eq!(context.current_transform.b, 0.0);
667        assert_eq!(context.current_transform.c, 0.0);
668        assert_eq!(context.current_transform.d, 1.0);
669        assert_eq!(context.current_transform.e, 0.0);
670        assert_eq!(context.current_transform.f, 0.0);
671    }
672
673    #[test]
674    fn test_render_context_new_screen_space() {
675        let context = RenderContext::new(CoordinateSystem::ScreenSpace, 595.0, 842.0);
676
677        assert_eq!(context.page_width, 595.0);
678        assert_eq!(context.page_height, 842.0);
679        assert!(matches!(
680            context.coordinate_system,
681            CoordinateSystem::ScreenSpace
682        ));
683
684        // Transform should be Y-flip for ScreenSpace
685        assert_eq!(context.current_transform.a, 1.0);
686        assert_eq!(context.current_transform.b, 0.0);
687        assert_eq!(context.current_transform.c, 0.0);
688        assert_eq!(context.current_transform.d, -1.0);
689        assert_eq!(context.current_transform.e, 0.0);
690        assert_eq!(context.current_transform.f, 842.0);
691    }
692
693    #[test]
694    fn test_render_context_new_custom() {
695        let custom_matrix = TransformMatrix::scale(2.0, 2.0);
696        let context = RenderContext::new(CoordinateSystem::Custom(custom_matrix), 595.0, 842.0);
697
698        assert_eq!(context.page_width, 595.0);
699        assert_eq!(context.page_height, 842.0);
700
701        // Transform should be the custom matrix
702        assert_eq!(context.current_transform.a, 2.0);
703        assert_eq!(context.current_transform.d, 2.0);
704    }
705
706    #[test]
707    fn test_render_context_to_pdf_standard_pdf_standard() {
708        let context = RenderContext::new(CoordinateSystem::PdfStandard, 595.0, 842.0);
709        let point = Point::new(100.0, 200.0);
710        let pdf_point = context.to_pdf_standard(point);
711
712        // PdfStandard should not change point
713        assert_eq!(pdf_point, point);
714    }
715
716    #[test]
717    fn test_render_context_to_pdf_standard_screen_space() {
718        let context = RenderContext::new(CoordinateSystem::ScreenSpace, 595.0, 842.0);
719        let point = Point::new(100.0, 200.0);
720        let pdf_point = context.to_pdf_standard(point);
721
722        // ScreenSpace should flip Y: (100, 842-200) = (100, 642)
723        assert_eq!(pdf_point, Point::new(100.0, 642.0));
724    }
725
726    #[test]
727    fn test_render_context_y_to_pdf_pdf_standard() {
728        let context = RenderContext::new(CoordinateSystem::PdfStandard, 595.0, 842.0);
729        let y = 300.0;
730        let pdf_y = context.y_to_pdf(y);
731
732        // PdfStandard should not change Y
733        assert_eq!(pdf_y, 300.0);
734    }
735
736    #[test]
737    fn test_render_context_y_to_pdf_screen_space() {
738        let context = RenderContext::new(CoordinateSystem::ScreenSpace, 595.0, 842.0);
739        let y = 300.0;
740        let pdf_y = context.y_to_pdf(y);
741
742        // ScreenSpace should flip Y: 842 - 300 = 542
743        assert_eq!(pdf_y, 542.0);
744    }
745
746    #[test]
747    fn test_render_context_edge_case_zero_dimensions() {
748        let context = RenderContext::new(CoordinateSystem::PdfStandard, 0.0, 0.0);
749
750        assert_eq!(context.page_width, 0.0);
751        assert_eq!(context.page_height, 0.0);
752
753        let point = Point::new(10.0, 20.0);
754        let pdf_point = context.to_pdf_standard(point);
755
756        // Should still work with zero dimensions
757        assert_eq!(pdf_point, point);
758    }
759
760    #[test]
761    fn test_render_context_edge_case_negative_dimensions() {
762        let context = RenderContext::new(CoordinateSystem::ScreenSpace, 595.0, -842.0);
763
764        assert_eq!(context.page_width, 595.0);
765        assert_eq!(context.page_height, -842.0);
766
767        let point = Point::new(100.0, 200.0);
768        let pdf_point = context.to_pdf_standard(point);
769
770        // ScreenSpace with negative height: -842 - 200 = -1042
771        assert_eq!(pdf_point, Point::new(100.0, -1042.0));
772    }
773
774    #[test]
775    fn test_coordinate_system_equality() {
776        let cs1 = CoordinateSystem::PdfStandard;
777        let cs2 = CoordinateSystem::PdfStandard;
778        assert_eq!(cs1, cs2);
779
780        let cs3 = CoordinateSystem::ScreenSpace;
781        let cs4 = CoordinateSystem::ScreenSpace;
782        assert_eq!(cs3, cs4);
783
784        assert_ne!(cs1, cs3);
785
786        let matrix1 = TransformMatrix::IDENTITY;
787        let matrix2 = TransformMatrix::IDENTITY;
788        let cs5 = CoordinateSystem::Custom(matrix1);
789        let cs6 = CoordinateSystem::Custom(matrix2);
790        assert_eq!(cs5, cs6);
791    }
792
793    #[test]
794    fn test_transform_matrix_equality() {
795        let m1 = TransformMatrix::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
796        let m2 = TransformMatrix::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
797        assert_eq!(m1, m2);
798
799        let m3 = TransformMatrix::new(1.0, 2.0, 3.0, 4.0, 5.0, 7.0);
800        assert_ne!(m1, m3);
801    }
802}