1use crate::geometry::Point;
27
28#[derive(Debug, Clone, Copy, PartialEq)]
30pub enum CoordinateSystem {
31 PdfStandard,
34
35 ScreenSpace,
38
39 Custom(TransformMatrix),
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
58pub struct TransformMatrix {
59 pub a: f64,
61 pub b: f64,
63 pub c: f64,
65 pub d: f64,
67 pub e: f64,
69 pub f: f64,
71}
72
73impl Default for CoordinateSystem {
74 fn default() -> Self {
75 Self::PdfStandard
76 }
77}
78
79impl TransformMatrix {
80 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 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 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 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 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 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 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 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 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 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 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 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 let transformed = matrix.transform_point(Point::new(0.0, y));
198 transformed.y
199 }
200 }
201 }
202
203 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, }
210 }
211}
212
213#[derive(Debug)]
215pub struct RenderContext {
216 pub coordinate_system: CoordinateSystem,
218 pub page_width: f64,
220 pub page_height: f64,
221 pub current_transform: TransformMatrix,
223}
224
225impl RenderContext {
226 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 pub fn to_pdf_standard(&self, point: Point) -> Point {
240 self.coordinate_system
241 .to_pdf_standard(point, self.page_height)
242 }
243
244 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); }
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)); }
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 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 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
329 #[test]
334 fn test_transform_matrix_rotate_90_degrees() {
335 let rotate = TransformMatrix::rotate(std::f64::consts::FRAC_PI_2); let point = Point::new(1.0, 0.0);
337 let transformed = rotate.transform_point(point);
338
339 assert!((transformed.x - 0.0).abs() < 1e-10, "X should be ~0");
341 assert!((transformed.y - 1.0).abs() < 1e-10, "Y should be ~1");
342 }
343
344 #[test]
345 fn test_transform_matrix_rotate_180_degrees() {
346 let rotate = TransformMatrix::rotate(std::f64::consts::PI); let point = Point::new(1.0, 0.0);
348 let transformed = rotate.transform_point(point);
349
350 assert!((transformed.x - (-1.0)).abs() < 1e-10, "X should be ~-1");
352 assert!((transformed.y - 0.0).abs() < 1e-10, "Y should be ~0");
353 }
354
355 #[test]
356 fn test_transform_matrix_rotate_270_degrees() {
357 let rotate = TransformMatrix::rotate(3.0 * std::f64::consts::FRAC_PI_2); let point = Point::new(1.0, 0.0);
359 let transformed = rotate.transform_point(point);
360
361 assert!((transformed.x - 0.0).abs() < 1e-10, "X should be ~0");
363 assert!((transformed.y - (-1.0)).abs() < 1e-10, "Y should be ~-1");
364 }
365
366 #[test]
367 fn test_transform_matrix_multiply_identity() {
368 let matrix = TransformMatrix::new(2.0, 3.0, 4.0, 5.0, 6.0, 7.0);
369 let result = matrix.multiply(&TransformMatrix::IDENTITY);
370
371 assert_eq!(result.a, 2.0);
373 assert_eq!(result.b, 3.0);
374 assert_eq!(result.c, 4.0);
375 assert_eq!(result.d, 5.0);
376 assert_eq!(result.e, 6.0);
377 assert_eq!(result.f, 7.0);
378 }
379
380 #[test]
381 fn test_transform_matrix_multiply_translate_then_scale() {
382 let translate = TransformMatrix::translate(10.0, 20.0);
383 let scale = TransformMatrix::scale(2.0, 3.0);
384
385 let combined = translate.multiply(&scale);
387 let point = Point::new(5.0, 5.0);
388 let transformed = combined.transform_point(point);
389
390 assert_eq!(transformed.x, 20.0);
392 assert_eq!(transformed.y, 35.0);
393 }
394
395 #[test]
396 fn test_transform_matrix_multiply_scale_then_translate() {
397 let scale = TransformMatrix::scale(2.0, 3.0);
398 let translate = TransformMatrix::translate(10.0, 20.0);
399
400 let combined = scale.multiply(&translate);
402 let point = Point::new(5.0, 5.0);
403 let transformed = combined.transform_point(point);
404
405 assert_eq!(transformed.x, 30.0);
407 assert_eq!(transformed.y, 75.0);
408 }
409
410 #[test]
411 fn test_transform_matrix_to_pdf_ctm() {
412 let matrix = TransformMatrix::new(1.5, 0.5, -0.5, 2.0, 10.0, 20.0);
413 let ctm = matrix.to_pdf_ctm();
414
415 assert_eq!(
416 ctm,
417 "1.500000 0.500000 -0.500000 2.000000 10.000000 20.000000 cm"
418 );
419 }
420
421 #[test]
422 fn test_transform_matrix_to_pdf_ctm_with_precision() {
423 let matrix = TransformMatrix::new(
424 0.123456789,
425 0.987654321,
426 -0.111111111,
427 0.222222222,
428 100.123456,
429 200.987654,
430 );
431 let ctm = matrix.to_pdf_ctm();
432
433 assert!(ctm.contains("0.123457")); assert!(ctm.contains("0.987654")); assert!(ctm.contains("-0.111111"));
437 assert!(ctm.contains("0.222222"));
438 assert!(ctm.contains("100.123456"));
439 assert!(ctm.contains("200.987654"));
440 assert!(ctm.ends_with(" cm"));
441 }
442
443 #[test]
444 fn test_transform_matrix_flip_y_zero_height() {
445 let flip = TransformMatrix::flip_y(0.0);
446 let point = Point::new(100.0, 50.0);
447 let transformed = flip.transform_point(point);
448
449 assert_eq!(transformed.x, 100.0);
451 assert_eq!(transformed.y, -50.0);
452 }
453
454 #[test]
455 fn test_transform_matrix_flip_y_negative_height() {
456 let flip = TransformMatrix::flip_y(-100.0);
457 let point = Point::new(50.0, 25.0);
458 let transformed = flip.transform_point(point);
459
460 assert_eq!(transformed.x, 50.0);
462 assert_eq!(transformed.y, -125.0);
463 }
464
465 #[test]
466 fn test_transform_matrix_scale_zero() {
467 let scale = TransformMatrix::scale(0.0, 0.0);
468 let point = Point::new(100.0, 200.0);
469 let transformed = scale.transform_point(point);
470
471 assert_eq!(transformed.x, 0.0);
473 assert_eq!(transformed.y, 0.0);
474 }
475
476 #[test]
477 fn test_transform_matrix_scale_negative() {
478 let scale = TransformMatrix::scale(-1.0, -2.0);
479 let point = Point::new(10.0, 20.0);
480 let transformed = scale.transform_point(point);
481
482 assert_eq!(transformed.x, -10.0);
484 assert_eq!(transformed.y, -40.0);
485 }
486
487 #[test]
488 fn test_transform_matrix_translate_zero() {
489 let translate = TransformMatrix::translate(0.0, 0.0);
490 let point = Point::new(50.0, 75.0);
491 let transformed = translate.transform_point(point);
492
493 assert_eq!(transformed, point);
495 }
496
497 #[test]
498 fn test_transform_matrix_translate_negative() {
499 let translate = TransformMatrix::translate(-10.0, -20.0);
500 let point = Point::new(100.0, 200.0);
501 let transformed = translate.transform_point(point);
502
503 assert_eq!(transformed.x, 90.0);
504 assert_eq!(transformed.y, 180.0);
505 }
506
507 #[test]
512 fn test_coordinate_system_default() {
513 let default_cs = CoordinateSystem::default();
514 assert!(
515 matches!(default_cs, CoordinateSystem::PdfStandard),
516 "Default should be PdfStandard"
517 );
518 }
519
520 #[test]
521 fn test_coordinate_system_pdf_standard_identity() {
522 let cs = CoordinateSystem::PdfStandard;
523 let matrix = cs.to_pdf_standard_matrix(500.0);
524
525 assert_eq!(matrix.a, 1.0);
527 assert_eq!(matrix.b, 0.0);
528 assert_eq!(matrix.c, 0.0);
529 assert_eq!(matrix.d, 1.0);
530 assert_eq!(matrix.e, 0.0);
531 assert_eq!(matrix.f, 0.0);
532 }
533
534 #[test]
535 fn test_coordinate_system_screen_space_flip() {
536 let cs = CoordinateSystem::ScreenSpace;
537 let page_height = 600.0;
538 let matrix = cs.to_pdf_standard_matrix(page_height);
539
540 assert_eq!(matrix.a, 1.0);
542 assert_eq!(matrix.b, 0.0);
543 assert_eq!(matrix.c, 0.0);
544 assert_eq!(matrix.d, -1.0);
545 assert_eq!(matrix.e, 0.0);
546 assert_eq!(matrix.f, page_height);
547 }
548
549 #[test]
550 fn test_coordinate_system_custom_matrix() {
551 let custom_matrix = TransformMatrix::new(2.0, 0.0, 0.0, 2.0, 50.0, 100.0);
552 let cs = CoordinateSystem::Custom(custom_matrix);
553
554 let retrieved_matrix = cs.to_pdf_standard_matrix(500.0);
555
556 assert_eq!(retrieved_matrix.a, 2.0);
558 assert_eq!(retrieved_matrix.b, 0.0);
559 assert_eq!(retrieved_matrix.c, 0.0);
560 assert_eq!(retrieved_matrix.d, 2.0);
561 assert_eq!(retrieved_matrix.e, 50.0);
562 assert_eq!(retrieved_matrix.f, 100.0);
563 }
564
565 #[test]
566 fn test_coordinate_system_y_to_pdf_standard_pdf_standard() {
567 let cs = CoordinateSystem::PdfStandard;
568 let y = 200.0;
569 let page_height = 842.0;
570
571 let pdf_y = cs.y_to_pdf_standard(y, page_height);
572
573 assert_eq!(pdf_y, 200.0);
575 }
576
577 #[test]
578 fn test_coordinate_system_y_to_pdf_standard_screen_space() {
579 let cs = CoordinateSystem::ScreenSpace;
580 let y = 200.0;
581 let page_height = 842.0;
582
583 let pdf_y = cs.y_to_pdf_standard(y, page_height);
584
585 assert_eq!(pdf_y, 642.0);
587 }
588
589 #[test]
590 fn test_coordinate_system_y_to_pdf_standard_custom() {
591 let custom_matrix = TransformMatrix::new(1.0, 0.0, 0.0, -2.0, 0.0, 500.0);
592 let cs = CoordinateSystem::Custom(custom_matrix);
593 let y = 100.0;
594 let page_height = 600.0; let pdf_y = cs.y_to_pdf_standard(y, page_height);
597
598 assert_eq!(pdf_y, 300.0);
600 }
601
602 #[test]
603 fn test_coordinate_system_grows_upward_pdf_standard() {
604 let cs = CoordinateSystem::PdfStandard;
605 assert!(cs.grows_upward(), "PdfStandard should grow upward");
606 }
607
608 #[test]
609 fn test_coordinate_system_grows_upward_screen_space() {
610 let cs = CoordinateSystem::ScreenSpace;
611 assert!(
612 !cs.grows_upward(),
613 "ScreenSpace should NOT grow upward (Y increases downward)"
614 );
615 }
616
617 #[test]
618 fn test_coordinate_system_grows_upward_custom_positive_d() {
619 let custom_matrix = TransformMatrix::new(1.0, 0.0, 0.0, 2.0, 0.0, 0.0);
620 let cs = CoordinateSystem::Custom(custom_matrix);
621 assert!(
622 cs.grows_upward(),
623 "Custom with positive d should grow upward"
624 );
625 }
626
627 #[test]
628 fn test_coordinate_system_grows_upward_custom_negative_d() {
629 let custom_matrix = TransformMatrix::new(1.0, 0.0, 0.0, -1.0, 0.0, 100.0);
630 let cs = CoordinateSystem::Custom(custom_matrix);
631 assert!(
632 !cs.grows_upward(),
633 "Custom with negative d should NOT grow upward"
634 );
635 }
636
637 #[test]
638 fn test_coordinate_system_grows_upward_custom_zero_d() {
639 let custom_matrix = TransformMatrix::new(1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
640 let cs = CoordinateSystem::Custom(custom_matrix);
641 assert!(
642 !cs.grows_upward(),
643 "Custom with zero d should NOT grow upward"
644 );
645 }
646
647 #[test]
652 fn test_render_context_new_pdf_standard() {
653 let context = RenderContext::new(CoordinateSystem::PdfStandard, 595.0, 842.0);
654
655 assert_eq!(context.page_width, 595.0);
656 assert_eq!(context.page_height, 842.0);
657 assert!(matches!(
658 context.coordinate_system,
659 CoordinateSystem::PdfStandard
660 ));
661
662 assert_eq!(context.current_transform.a, 1.0);
664 assert_eq!(context.current_transform.b, 0.0);
665 assert_eq!(context.current_transform.c, 0.0);
666 assert_eq!(context.current_transform.d, 1.0);
667 assert_eq!(context.current_transform.e, 0.0);
668 assert_eq!(context.current_transform.f, 0.0);
669 }
670
671 #[test]
672 fn test_render_context_new_screen_space() {
673 let context = RenderContext::new(CoordinateSystem::ScreenSpace, 595.0, 842.0);
674
675 assert_eq!(context.page_width, 595.0);
676 assert_eq!(context.page_height, 842.0);
677 assert!(matches!(
678 context.coordinate_system,
679 CoordinateSystem::ScreenSpace
680 ));
681
682 assert_eq!(context.current_transform.a, 1.0);
684 assert_eq!(context.current_transform.b, 0.0);
685 assert_eq!(context.current_transform.c, 0.0);
686 assert_eq!(context.current_transform.d, -1.0);
687 assert_eq!(context.current_transform.e, 0.0);
688 assert_eq!(context.current_transform.f, 842.0);
689 }
690
691 #[test]
692 fn test_render_context_new_custom() {
693 let custom_matrix = TransformMatrix::scale(2.0, 2.0);
694 let context = RenderContext::new(CoordinateSystem::Custom(custom_matrix), 595.0, 842.0);
695
696 assert_eq!(context.page_width, 595.0);
697 assert_eq!(context.page_height, 842.0);
698
699 assert_eq!(context.current_transform.a, 2.0);
701 assert_eq!(context.current_transform.d, 2.0);
702 }
703
704 #[test]
705 fn test_render_context_to_pdf_standard_pdf_standard() {
706 let context = RenderContext::new(CoordinateSystem::PdfStandard, 595.0, 842.0);
707 let point = Point::new(100.0, 200.0);
708 let pdf_point = context.to_pdf_standard(point);
709
710 assert_eq!(pdf_point, point);
712 }
713
714 #[test]
715 fn test_render_context_to_pdf_standard_screen_space() {
716 let context = RenderContext::new(CoordinateSystem::ScreenSpace, 595.0, 842.0);
717 let point = Point::new(100.0, 200.0);
718 let pdf_point = context.to_pdf_standard(point);
719
720 assert_eq!(pdf_point, Point::new(100.0, 642.0));
722 }
723
724 #[test]
725 fn test_render_context_y_to_pdf_pdf_standard() {
726 let context = RenderContext::new(CoordinateSystem::PdfStandard, 595.0, 842.0);
727 let y = 300.0;
728 let pdf_y = context.y_to_pdf(y);
729
730 assert_eq!(pdf_y, 300.0);
732 }
733
734 #[test]
735 fn test_render_context_y_to_pdf_screen_space() {
736 let context = RenderContext::new(CoordinateSystem::ScreenSpace, 595.0, 842.0);
737 let y = 300.0;
738 let pdf_y = context.y_to_pdf(y);
739
740 assert_eq!(pdf_y, 542.0);
742 }
743
744 #[test]
745 fn test_render_context_edge_case_zero_dimensions() {
746 let context = RenderContext::new(CoordinateSystem::PdfStandard, 0.0, 0.0);
747
748 assert_eq!(context.page_width, 0.0);
749 assert_eq!(context.page_height, 0.0);
750
751 let point = Point::new(10.0, 20.0);
752 let pdf_point = context.to_pdf_standard(point);
753
754 assert_eq!(pdf_point, point);
756 }
757
758 #[test]
759 fn test_render_context_edge_case_negative_dimensions() {
760 let context = RenderContext::new(CoordinateSystem::ScreenSpace, 595.0, -842.0);
761
762 assert_eq!(context.page_width, 595.0);
763 assert_eq!(context.page_height, -842.0);
764
765 let point = Point::new(100.0, 200.0);
766 let pdf_point = context.to_pdf_standard(point);
767
768 assert_eq!(pdf_point, Point::new(100.0, -1042.0));
770 }
771
772 #[test]
773 fn test_coordinate_system_equality() {
774 let cs1 = CoordinateSystem::PdfStandard;
775 let cs2 = CoordinateSystem::PdfStandard;
776 assert_eq!(cs1, cs2);
777
778 let cs3 = CoordinateSystem::ScreenSpace;
779 let cs4 = CoordinateSystem::ScreenSpace;
780 assert_eq!(cs3, cs4);
781
782 assert_ne!(cs1, cs3);
783
784 let matrix1 = TransformMatrix::IDENTITY;
785 let matrix2 = TransformMatrix::IDENTITY;
786 let cs5 = CoordinateSystem::Custom(matrix1);
787 let cs6 = CoordinateSystem::Custom(matrix2);
788 assert_eq!(cs5, cs6);
789 }
790
791 #[test]
792 fn test_transform_matrix_equality() {
793 let m1 = TransformMatrix::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
794 let m2 = TransformMatrix::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
795 assert_eq!(m1, m2);
796
797 let m3 = TransformMatrix::new(1.0, 2.0, 3.0, 4.0, 5.0, 7.0);
798 assert_ne!(m1, m3);
799 }
800}