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)]
60pub struct TransformMatrix {
61 pub a: f64,
63 pub b: f64,
65 pub c: f64,
67 pub d: f64,
69 pub e: f64,
71 pub f: f64,
73}
74
75impl Default for CoordinateSystem {
76 fn default() -> Self {
77 Self::PdfStandard
78 }
79}
80
81impl TransformMatrix {
82 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 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 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 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 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 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 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 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 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 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 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 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 let transformed = matrix.transform_point(Point::new(0.0, y));
200 transformed.y
201 }
202 }
203 }
204
205 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, }
212 }
213}
214
215#[derive(Debug)]
217pub struct RenderContext {
218 pub coordinate_system: CoordinateSystem,
220 pub page_width: f64,
222 pub page_height: f64,
223 pub current_transform: TransformMatrix,
225}
226
227impl RenderContext {
228 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 pub fn to_pdf_standard(&self, point: Point) -> Point {
242 self.coordinate_system
243 .to_pdf_standard(point, self.page_height)
244 }
245
246 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); }
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)); }
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 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 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 #[test]
336 fn test_transform_matrix_rotate_90_degrees() {
337 let rotate = TransformMatrix::rotate(std::f64::consts::FRAC_PI_2); let point = Point::new(1.0, 0.0);
339 let transformed = rotate.transform_point(point);
340
341 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); let point = Point::new(1.0, 0.0);
350 let transformed = rotate.transform_point(point);
351
352 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); let point = Point::new(1.0, 0.0);
361 let transformed = rotate.transform_point(point);
362
363 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 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 let combined = translate.multiply(&scale);
389 let point = Point::new(5.0, 5.0);
390 let transformed = combined.transform_point(point);
391
392 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 let combined = scale.multiply(&translate);
404 let point = Point::new(5.0, 5.0);
405 let transformed = combined.transform_point(point);
406
407 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 assert!(ctm.contains("0.123457")); assert!(ctm.contains("0.987654")); 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 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 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 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 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 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 #[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 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 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 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 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 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; let pdf_y = cs.y_to_pdf_standard(y, page_height);
599
600 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 #[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 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 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 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 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 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 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 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 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 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}