1use fop_layout::area::BorderStyle;
6use fop_types::{Color, Length, Result};
7use std::fmt::Write as FmtWrite;
8
9pub struct PdfGraphics {
11 operations: String,
13}
14
15impl PdfGraphics {
16 pub fn new() -> Self {
18 Self {
19 operations: String::new(),
20 }
21 }
22
23 pub fn content(&self) -> &str {
25 &self.operations
26 }
27
28 pub fn set_stroke_color(&mut self, color: Color) -> Result<()> {
30 writeln!(
31 &mut self.operations,
32 "{:.3} {:.3} {:.3} RG",
33 color.r_f32(),
34 color.g_f32(),
35 color.b_f32()
36 )
37 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
38 Ok(())
39 }
40
41 pub fn set_fill_color(&mut self, color: Color) -> Result<()> {
43 writeln!(
44 &mut self.operations,
45 "{:.3} {:.3} {:.3} rg",
46 color.r_f32(),
47 color.g_f32(),
48 color.b_f32()
49 )
50 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
51 Ok(())
52 }
53
54 pub fn set_opacity(&mut self, gs_name: &str) -> Result<()> {
66 writeln!(&mut self.operations, "/{} gs", gs_name)
67 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
68 Ok(())
69 }
70
71 pub fn set_stroke_opacity(&mut self, gs_name: &str) -> Result<()> {
82 writeln!(&mut self.operations, "/{} gs", gs_name)
83 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
84 Ok(())
85 }
86
87 pub fn set_line_width(&mut self, width: Length) -> Result<()> {
89 writeln!(&mut self.operations, "{:.3} w", width.to_pt())
90 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
91 Ok(())
92 }
93
94 pub fn set_dash_pattern(&mut self, dash_array: &[f64], phase: f64) -> Result<()> {
105 write!(&mut self.operations, "[")
106 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
107 for (i, &dash) in dash_array.iter().enumerate() {
108 if i > 0 {
109 write!(&mut self.operations, " ")
110 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
111 }
112 write!(&mut self.operations, "{:.3}", dash)
113 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
114 }
115 writeln!(&mut self.operations, "] {:.3} d", phase)
116 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
117 Ok(())
118 }
119
120 pub fn draw_rectangle(
122 &mut self,
123 x: Length,
124 y: Length,
125 width: Length,
126 height: Length,
127 ) -> Result<()> {
128 writeln!(
129 &mut self.operations,
130 "{:.3} {:.3} {:.3} {:.3} re S",
131 x.to_pt(),
132 y.to_pt(),
133 width.to_pt(),
134 height.to_pt()
135 )
136 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
137 Ok(())
138 }
139
140 pub fn fill_rectangle(
142 &mut self,
143 x: Length,
144 y: Length,
145 width: Length,
146 height: Length,
147 ) -> Result<()> {
148 self.fill_rectangle_with_radius(x, y, width, height, None)
149 }
150
151 pub fn fill_rectangle_with_radius(
153 &mut self,
154 x: Length,
155 y: Length,
156 width: Length,
157 height: Length,
158 border_radius: Option<[Length; 4]>,
159 ) -> Result<()> {
160 if let Some(radii) = border_radius {
161 self.draw_rounded_rectangle(x, y, width, height, radii, true)
163 } else {
164 writeln!(
166 &mut self.operations,
167 "{:.3} {:.3} {:.3} {:.3} re f",
168 x.to_pt(),
169 y.to_pt(),
170 width.to_pt(),
171 height.to_pt()
172 )
173 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
174 Ok(())
175 }
176 }
177
178 #[allow(clippy::too_many_arguments)]
190 pub fn draw_rounded_rectangle(
191 &mut self,
192 x: Length,
193 y: Length,
194 width: Length,
195 height: Length,
196 radii: [Length; 4],
197 fill: bool,
198 ) -> Result<()> {
199 let x_pt = x.to_pt();
200 let y_pt = y.to_pt();
201 let w_pt = width.to_pt();
202 let h_pt = height.to_pt();
203
204 let [tl, tr, br, bl] = radii;
206 let tl_pt = tl.to_pt().min(w_pt / 2.0).min(h_pt / 2.0);
207 let tr_pt = tr.to_pt().min(w_pt / 2.0).min(h_pt / 2.0);
208 let br_pt = br.to_pt().min(w_pt / 2.0).min(h_pt / 2.0);
209 let bl_pt = bl.to_pt().min(w_pt / 2.0).min(h_pt / 2.0);
210
211 const KAPPA: f64 = 0.552284749831;
214
215 write!(&mut self.operations, "{:.3} {:.3} m ", x_pt + bl_pt, y_pt)
218 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
219
220 write!(
222 &mut self.operations,
223 "{:.3} {:.3} l ",
224 x_pt + w_pt - br_pt,
225 y_pt
226 )
227 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
228
229 if br_pt > 0.0 {
231 write!(
232 &mut self.operations,
233 "{:.3} {:.3} {:.3} {:.3} {:.3} {:.3} c ",
234 x_pt + w_pt - br_pt + br_pt * KAPPA,
235 y_pt,
236 x_pt + w_pt,
237 y_pt + br_pt - br_pt * KAPPA,
238 x_pt + w_pt,
239 y_pt + br_pt
240 )
241 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
242 }
243
244 write!(
246 &mut self.operations,
247 "{:.3} {:.3} l ",
248 x_pt + w_pt,
249 y_pt + h_pt - tr_pt
250 )
251 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
252
253 if tr_pt > 0.0 {
255 write!(
256 &mut self.operations,
257 "{:.3} {:.3} {:.3} {:.3} {:.3} {:.3} c ",
258 x_pt + w_pt,
259 y_pt + h_pt - tr_pt + tr_pt * KAPPA,
260 x_pt + w_pt - tr_pt + tr_pt * KAPPA,
261 y_pt + h_pt,
262 x_pt + w_pt - tr_pt,
263 y_pt + h_pt
264 )
265 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
266 }
267
268 write!(
270 &mut self.operations,
271 "{:.3} {:.3} l ",
272 x_pt + tl_pt,
273 y_pt + h_pt
274 )
275 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
276
277 if tl_pt > 0.0 {
279 write!(
280 &mut self.operations,
281 "{:.3} {:.3} {:.3} {:.3} {:.3} {:.3} c ",
282 x_pt + tl_pt - tl_pt * KAPPA,
283 y_pt + h_pt,
284 x_pt,
285 y_pt + h_pt - tl_pt + tl_pt * KAPPA,
286 x_pt,
287 y_pt + h_pt - tl_pt
288 )
289 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
290 }
291
292 write!(&mut self.operations, "{:.3} {:.3} l ", x_pt, y_pt + bl_pt)
294 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
295
296 if bl_pt > 0.0 {
298 write!(
299 &mut self.operations,
300 "{:.3} {:.3} {:.3} {:.3} {:.3} {:.3} c ",
301 x_pt,
302 y_pt + bl_pt - bl_pt * KAPPA,
303 x_pt + bl_pt - bl_pt * KAPPA,
304 y_pt,
305 x_pt + bl_pt,
306 y_pt
307 )
308 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
309 }
310
311 if fill {
313 writeln!(&mut self.operations, "f")
314 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
315 } else {
316 writeln!(&mut self.operations, "S")
317 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
318 }
319
320 Ok(())
321 }
322
323 pub fn draw_line(&mut self, x1: Length, y1: Length, x2: Length, y2: Length) -> Result<()> {
325 writeln!(
326 &mut self.operations,
327 "{:.3} {:.3} m {:.3} {:.3} l S",
328 x1.to_pt(),
329 y1.to_pt(),
330 x2.to_pt(),
331 y2.to_pt()
332 )
333 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
334 Ok(())
335 }
336
337 #[allow(clippy::too_many_arguments)]
339 pub fn draw_borders(
340 &mut self,
341 x: Length,
342 y: Length,
343 width: Length,
344 height: Length,
345 border_widths: [Length; 4], border_colors: [Color; 4],
347 border_styles: [BorderStyle; 4],
348 ) -> Result<()> {
349 self.draw_borders_with_radius(
350 x,
351 y,
352 width,
353 height,
354 border_widths,
355 border_colors,
356 border_styles,
357 None,
358 )
359 }
360
361 #[allow(clippy::too_many_arguments)]
363 pub fn draw_borders_with_radius(
364 &mut self,
365 x: Length,
366 y: Length,
367 width: Length,
368 height: Length,
369 border_widths: [Length; 4], border_colors: [Color; 4],
371 border_styles: [BorderStyle; 4],
372 border_radius: Option<[Length; 4]>, ) -> Result<()> {
374 let [top_width, right_width, bottom_width, left_width] = border_widths;
375 let [top_color, right_color, bottom_color, left_color] = border_colors;
376 let [top_style, right_style, bottom_style, left_style] = border_styles;
377
378 if let Some(radii) = border_radius {
380 let uniform_color =
382 top_color == right_color && top_color == bottom_color && top_color == left_color;
383 let uniform_width =
384 top_width == right_width && top_width == bottom_width && top_width == left_width;
385 let uniform_style =
386 top_style == right_style && top_style == bottom_style && top_style == left_style;
387
388 if uniform_color
389 && uniform_width
390 && uniform_style
391 && top_width > Length::ZERO
392 && !matches!(top_style, BorderStyle::None | BorderStyle::Hidden)
393 {
394 self.set_stroke_color(top_color)?;
396 self.set_line_width(top_width)?;
397 self.apply_border_style(top_style)?;
398 self.draw_rounded_rectangle(x, y, width, height, radii, false)?;
399 self.set_dash_pattern(&[], 0.0)?;
401 return Ok(());
402 }
403 }
404
405 if top_width > Length::ZERO && !matches!(top_style, BorderStyle::None | BorderStyle::Hidden)
410 {
411 self.set_stroke_color(top_color)?;
412 self.set_line_width(top_width)?;
413 self.apply_border_style(top_style)?;
414 let y_top = y + height;
415 self.draw_line(x, y_top, x + width, y_top)?;
416 self.set_dash_pattern(&[], 0.0)?;
418 }
419
420 if right_width > Length::ZERO
422 && !matches!(right_style, BorderStyle::None | BorderStyle::Hidden)
423 {
424 self.set_stroke_color(right_color)?;
425 self.set_line_width(right_width)?;
426 self.apply_border_style(right_style)?;
427 let x_right = x + width;
428 self.draw_line(x_right, y, x_right, y + height)?;
429 self.set_dash_pattern(&[], 0.0)?;
431 }
432
433 if bottom_width > Length::ZERO
435 && !matches!(bottom_style, BorderStyle::None | BorderStyle::Hidden)
436 {
437 self.set_stroke_color(bottom_color)?;
438 self.set_line_width(bottom_width)?;
439 self.apply_border_style(bottom_style)?;
440 self.draw_line(x, y, x + width, y)?;
441 self.set_dash_pattern(&[], 0.0)?;
443 }
444
445 if left_width > Length::ZERO
447 && !matches!(left_style, BorderStyle::None | BorderStyle::Hidden)
448 {
449 self.set_stroke_color(left_color)?;
450 self.set_line_width(left_width)?;
451 self.apply_border_style(left_style)?;
452 self.draw_line(x, y, x, y + height)?;
453 self.set_dash_pattern(&[], 0.0)?;
455 }
456
457 Ok(())
458 }
459
460 fn apply_border_style(&mut self, style: BorderStyle) -> Result<()> {
462 match style {
463 BorderStyle::Solid => self.set_dash_pattern(&[], 0.0),
464 BorderStyle::Dashed => self.set_dash_pattern(&[6.0, 3.0], 0.0),
465 BorderStyle::Dotted => self.set_dash_pattern(&[1.0, 2.0], 0.0),
466 _ => self.set_dash_pattern(&[], 0.0),
469 }
470 }
471
472 pub fn save_state(&mut self) -> Result<()> {
474 writeln!(&mut self.operations, "q")
475 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
476 Ok(())
477 }
478
479 pub fn restore_state(&mut self) -> Result<()> {
481 writeln!(&mut self.operations, "Q")
482 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
483 Ok(())
484 }
485
486 pub fn save_clip_state(
505 &mut self,
506 x: Length,
507 y: Length,
508 width: Length,
509 height: Length,
510 ) -> Result<()> {
511 writeln!(&mut self.operations, "q")
513 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
514
515 writeln!(
517 &mut self.operations,
518 "{:.3} {:.3} {:.3} {:.3} re W n",
519 x.to_pt(),
520 y.to_pt(),
521 width.to_pt(),
522 height.to_pt()
523 )
524 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
525
526 Ok(())
527 }
528
529 pub fn restore_clip_state(&mut self) -> Result<()> {
537 self.restore_state()
538 }
539
540 pub fn fill_gradient(
553 &mut self,
554 x: Length,
555 y: Length,
556 width: Length,
557 height: Length,
558 gradient_index: usize,
559 ) -> Result<()> {
560 writeln!(&mut self.operations, "q")
562 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
563
564 writeln!(
567 &mut self.operations,
568 "{:.3} 0 0 {:.3} {:.3} {:.3} cm",
569 width.to_pt() / 100.0, height.to_pt() / 100.0, x.to_pt(),
572 y.to_pt()
573 )
574 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
575
576 writeln!(&mut self.operations, "/Sh{} sh", gradient_index)
578 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
579
580 writeln!(&mut self.operations, "Q")
582 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
583
584 Ok(())
585 }
586
587 pub fn fill_gradient_with_radius(
589 &mut self,
590 x: Length,
591 y: Length,
592 width: Length,
593 height: Length,
594 gradient_index: usize,
595 border_radius: Option<[Length; 4]>,
596 ) -> Result<()> {
597 if border_radius.is_some() {
598 self.save_state()?;
601
602 if let Some(radii) = border_radius {
604 self.draw_rounded_rectangle(x, y, width, height, radii, false)?;
605 writeln!(&mut self.operations, "W n")
607 .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
608 }
609
610 self.fill_gradient(x, y, width, height, gradient_index)?;
612
613 self.restore_state()?;
615 } else {
616 self.fill_gradient(x, y, width, height, gradient_index)?;
618 }
619
620 Ok(())
621 }
622}
623
624impl Default for PdfGraphics {
625 fn default() -> Self {
626 Self::new()
627 }
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633
634 #[test]
635 fn test_graphics_creation() {
636 let graphics = PdfGraphics::new();
637 assert_eq!(graphics.content(), "");
638 }
639
640 #[test]
641 fn test_set_stroke_color() {
642 let mut graphics = PdfGraphics::new();
643 graphics
644 .set_stroke_color(Color::RED)
645 .expect("test: should succeed");
646
647 assert!(graphics.content().contains("1.000 0.000 0.000 RG"));
648 }
649
650 #[test]
651 fn test_set_fill_color() {
652 let mut graphics = PdfGraphics::new();
653 graphics
654 .set_fill_color(Color::BLUE)
655 .expect("test: should succeed");
656
657 assert!(graphics.content().contains("0.000 0.000 1.000 rg"));
658 }
659
660 #[test]
661 fn test_set_line_width() {
662 let mut graphics = PdfGraphics::new();
663 graphics
664 .set_line_width(Length::from_pt(2.0))
665 .expect("test: should succeed");
666
667 assert!(graphics.content().contains("2.000 w"));
668 }
669
670 #[test]
671 fn test_draw_rectangle() {
672 let mut graphics = PdfGraphics::new();
673 graphics
674 .draw_rectangle(
675 Length::from_pt(10.0),
676 Length::from_pt(20.0),
677 Length::from_pt(100.0),
678 Length::from_pt(50.0),
679 )
680 .expect("test: should succeed");
681
682 assert!(graphics
683 .content()
684 .contains("10.000 20.000 100.000 50.000 re S"));
685 }
686
687 #[test]
688 fn test_fill_rectangle() {
689 let mut graphics = PdfGraphics::new();
690 graphics
691 .fill_rectangle(
692 Length::from_pt(0.0),
693 Length::from_pt(0.0),
694 Length::from_pt(50.0),
695 Length::from_pt(50.0),
696 )
697 .expect("test: should succeed");
698
699 assert!(graphics.content().contains("re f"));
700 }
701
702 #[test]
703 fn test_draw_line() {
704 let mut graphics = PdfGraphics::new();
705 graphics
706 .draw_line(
707 Length::from_pt(0.0),
708 Length::from_pt(0.0),
709 Length::from_pt(100.0),
710 Length::from_pt(100.0),
711 )
712 .expect("test: should succeed");
713
714 assert!(graphics.content().contains("m"));
715 assert!(graphics.content().contains("l S"));
716 }
717
718 #[test]
719 fn test_draw_borders() {
720 let mut graphics = PdfGraphics::new();
721 graphics
722 .draw_borders(
723 Length::from_pt(10.0),
724 Length::from_pt(10.0),
725 Length::from_pt(100.0),
726 Length::from_pt(50.0),
727 [Length::from_pt(1.0); 4],
728 [Color::BLACK; 4],
729 [BorderStyle::Solid; 4],
730 )
731 .expect("test: should succeed");
732
733 assert!(graphics.content().contains("m"));
735 assert!(graphics.content().contains("l S"));
736 }
737
738 #[test]
739 fn test_save_restore_state() {
740 let mut graphics = PdfGraphics::new();
741 graphics.save_state().expect("test: should succeed");
742 graphics.restore_state().expect("test: should succeed");
743
744 assert!(graphics.content().contains("q"));
745 assert!(graphics.content().contains("Q"));
746 }
747
748 #[test]
749 fn test_set_dash_pattern() {
750 let mut graphics = PdfGraphics::new();
751
752 graphics
754 .set_dash_pattern(&[], 0.0)
755 .expect("test: should succeed");
756 assert!(graphics.content().contains("[] 0.000 d"));
757
758 let mut graphics2 = PdfGraphics::new();
760 graphics2
761 .set_dash_pattern(&[6.0, 3.0], 0.0)
762 .expect("test: should succeed");
763 assert!(graphics2.content().contains("[6.000 3.000] 0.000 d"));
764
765 let mut graphics3 = PdfGraphics::new();
767 graphics3
768 .set_dash_pattern(&[1.0, 2.0], 0.0)
769 .expect("test: should succeed");
770 assert!(graphics3.content().contains("[1.000 2.000] 0.000 d"));
771 }
772
773 #[test]
774 fn test_border_styles() {
775 let mut graphics = PdfGraphics::new();
776
777 graphics
779 .draw_borders(
780 Length::from_pt(10.0),
781 Length::from_pt(10.0),
782 Length::from_pt(100.0),
783 Length::from_pt(50.0),
784 [Length::from_pt(2.0); 4],
785 [Color::RED; 4],
786 [BorderStyle::Dashed; 4],
787 )
788 .expect("test: should succeed");
789
790 assert!(graphics.content().contains("[6.000 3.000] 0.000 d"));
791 assert!(graphics.content().contains("1.000 0.000 0.000 RG")); }
793}
794
795#[cfg(test)]
796mod tests_extended {
797 use super::*;
798
799 #[test]
802 fn test_set_stroke_color_green() {
803 let mut g = PdfGraphics::new();
804 g.set_stroke_color(Color::GREEN)
805 .expect("test: should succeed");
806 assert!(g.content().contains("0.000 1.000 0.000 RG"));
807 }
808
809 #[test]
810 fn test_set_stroke_color_black() {
811 let mut g = PdfGraphics::new();
812 g.set_stroke_color(Color::BLACK)
813 .expect("test: should succeed");
814 assert!(g.content().contains("0.000 0.000 0.000 RG"));
815 }
816
817 #[test]
818 fn test_set_stroke_color_white() {
819 let mut g = PdfGraphics::new();
820 g.set_stroke_color(Color::WHITE)
821 .expect("test: should succeed");
822 assert!(g.content().contains("1.000 1.000 1.000 RG"));
823 }
824
825 #[test]
826 fn test_set_fill_color_red() {
827 let mut g = PdfGraphics::new();
828 g.set_fill_color(Color::RED).expect("test: should succeed");
829 assert!(g.content().contains("1.000 0.000 0.000 rg"));
830 }
831
832 #[test]
833 fn test_set_fill_color_green() {
834 let mut g = PdfGraphics::new();
835 g.set_fill_color(Color::GREEN)
836 .expect("test: should succeed");
837 assert!(g.content().contains("0.000 1.000 0.000 rg"));
838 }
839
840 #[test]
841 fn test_set_fill_color_custom_rgb() {
842 let mut g = PdfGraphics::new();
843 g.set_fill_color(Color::rgb(128, 64, 32))
845 .expect("test: should succeed");
846 let content = g.content().to_string();
847 assert!(content.contains("rg"), "fill operator missing: {}", content);
848 assert!(!content.contains("RG"), "should use lowercase rg not RG");
850 }
851
852 #[test]
853 fn test_stroke_uses_rg_uppercase_operator() {
854 let mut g = PdfGraphics::new();
855 g.set_stroke_color(Color::BLUE)
856 .expect("test: should succeed");
857 assert!(g.content().contains("RG"));
859 assert!(!g.content().contains(" rg"));
861 }
862
863 #[test]
864 fn test_fill_uses_rg_lowercase_operator() {
865 let mut g = PdfGraphics::new();
866 g.set_fill_color(Color::BLUE).expect("test: should succeed");
867 assert!(g.content().contains(" rg") || g.content().ends_with("rg\n"));
869 assert!(!g.content().contains("RG"));
871 }
872
873 #[test]
876 fn test_line_width_zero() {
877 let mut g = PdfGraphics::new();
878 g.set_line_width(Length::ZERO)
879 .expect("test: should succeed");
880 assert!(g.content().contains("0.000 w"));
881 }
882
883 #[test]
884 fn test_line_width_fractional() {
885 let mut g = PdfGraphics::new();
886 g.set_line_width(Length::from_pt(0.5))
887 .expect("test: should succeed");
888 assert!(g.content().contains("0.500 w"));
889 }
890
891 #[test]
892 fn test_line_width_large() {
893 let mut g = PdfGraphics::new();
894 g.set_line_width(Length::from_pt(10.0))
895 .expect("test: should succeed");
896 assert!(g.content().contains("10.000 w"));
897 }
898
899 #[test]
902 fn test_draw_line_uses_m_operator() {
903 let mut g = PdfGraphics::new();
904 g.draw_line(
905 Length::from_pt(5.0),
906 Length::from_pt(10.0),
907 Length::from_pt(50.0),
908 Length::from_pt(100.0),
909 )
910 .expect("test: should succeed");
911 let c = g.content();
912 assert!(c.contains("5.000 10.000 m"), "expected 'm' operator: {}", c);
914 assert!(
916 c.contains("50.000 100.000 l"),
917 "expected 'l' operator: {}",
918 c
919 );
920 assert!(c.contains("S"), "expected 'S' operator: {}", c);
922 }
923
924 #[test]
925 fn test_draw_rectangle_uses_re_operator() {
926 let mut g = PdfGraphics::new();
927 g.draw_rectangle(
928 Length::from_pt(1.0),
929 Length::from_pt(2.0),
930 Length::from_pt(30.0),
931 Length::from_pt(40.0),
932 )
933 .expect("test: should succeed");
934 let c = g.content();
935 assert!(c.contains("re S"), "expected 're S': {}", c);
937 assert!(c.contains("1.000 2.000 30.000 40.000"));
938 }
939
940 #[test]
941 fn test_fill_rectangle_uses_re_f_operator() {
942 let mut g = PdfGraphics::new();
943 g.fill_rectangle(
944 Length::from_pt(3.0),
945 Length::from_pt(4.0),
946 Length::from_pt(60.0),
947 Length::from_pt(80.0),
948 )
949 .expect("test: should succeed");
950 let c = g.content();
951 assert!(c.contains("re f"), "expected 're f': {}", c);
953 assert!(c.contains("3.000 4.000 60.000 80.000"));
954 }
955
956 #[test]
959 fn test_draw_line_stroke_operator_s() {
960 let mut g = PdfGraphics::new();
961 g.draw_line(
962 Length::from_pt(0.0),
963 Length::from_pt(0.0),
964 Length::from_pt(100.0),
965 Length::from_pt(0.0),
966 )
967 .expect("test: should succeed");
968 assert!(g.content().contains("l S"), "stroke 'S' missing");
970 }
971
972 #[test]
973 fn test_fill_rectangle_f_operator() {
974 let mut g = PdfGraphics::new();
975 g.fill_rectangle(
976 Length::ZERO,
977 Length::ZERO,
978 Length::from_pt(50.0),
979 Length::from_pt(50.0),
980 )
981 .expect("test: should succeed");
982 assert!(g.content().contains("re f"));
984 }
985
986 #[test]
989 fn test_save_state_q_operator() {
990 let mut g = PdfGraphics::new();
991 g.save_state().expect("test: should succeed");
992 assert!(g.content().contains("q\n"), "q operator missing");
993 }
994
995 #[test]
996 fn test_restore_state_q_operator() {
997 let mut g = PdfGraphics::new();
998 g.restore_state().expect("test: should succeed");
999 assert!(g.content().contains("Q\n"), "Q operator missing");
1000 }
1001
1002 #[test]
1003 fn test_save_restore_nesting() {
1004 let mut g = PdfGraphics::new();
1005 g.save_state().expect("test: should succeed");
1006 g.save_state().expect("test: should succeed");
1007 g.restore_state().expect("test: should succeed");
1008 g.restore_state().expect("test: should succeed");
1009 let c = g.content();
1010 assert_eq!(c.matches("q\n").count(), 2);
1011 assert_eq!(c.matches("Q\n").count(), 2);
1012 }
1013
1014 #[test]
1017 fn test_dash_pattern_single_value() {
1018 let mut g = PdfGraphics::new();
1019 g.set_dash_pattern(&[3.0], 0.0)
1020 .expect("test: should succeed");
1021 assert!(g.content().contains("[3.000] 0.000 d"));
1022 }
1023
1024 #[test]
1025 fn test_dash_pattern_with_phase() {
1026 let mut g = PdfGraphics::new();
1027 g.set_dash_pattern(&[4.0, 2.0], 1.0)
1028 .expect("test: should succeed");
1029 assert!(g.content().contains("[4.000 2.000] 1.000 d"));
1030 }
1031
1032 #[test]
1033 fn test_dash_pattern_reset_to_solid() {
1034 let mut g = PdfGraphics::new();
1035 g.set_dash_pattern(&[6.0, 3.0], 0.0)
1036 .expect("test: should succeed");
1037 g.set_dash_pattern(&[], 0.0).expect("test: should succeed");
1038 let c = g.content();
1039 assert!(c.contains("[] 0.000 d"), "solid reset missing: {}", c);
1040 }
1041
1042 #[test]
1045 fn test_save_clip_state_uses_w_n() {
1046 let mut g = PdfGraphics::new();
1047 g.save_clip_state(
1048 Length::from_pt(10.0),
1049 Length::from_pt(20.0),
1050 Length::from_pt(100.0),
1051 Length::from_pt(50.0),
1052 )
1053 .expect("test: should succeed");
1054 let c = g.content();
1055 assert!(c.contains("q\n"), "q missing: {}", c);
1057 assert!(c.contains("re W n"), "clipping 'W n' missing: {}", c);
1059 assert!(c.contains("10.000 20.000 100.000 50.000"), "coords missing");
1061 }
1062
1063 #[test]
1064 fn test_restore_clip_state_uses_q_operator() {
1065 let mut g = PdfGraphics::new();
1066 g.save_clip_state(
1067 Length::ZERO,
1068 Length::ZERO,
1069 Length::from_pt(200.0),
1070 Length::from_pt(100.0),
1071 )
1072 .expect("test: should succeed");
1073 g.restore_clip_state().expect("test: should succeed");
1074 let c = g.content();
1075 assert!(
1076 c.contains("Q\n"),
1077 "Q operator missing after restore_clip_state"
1078 );
1079 }
1080
1081 #[test]
1084 fn test_fill_gradient_cm_operator() {
1085 let mut g = PdfGraphics::new();
1086 g.fill_gradient(
1087 Length::from_pt(10.0),
1088 Length::from_pt(20.0),
1089 Length::from_pt(100.0),
1090 Length::from_pt(50.0),
1091 0,
1092 )
1093 .expect("test: should succeed");
1094 let c = g.content();
1095 assert!(c.contains("cm"), "cm operator missing: {}", c);
1097 assert!(c.contains("/Sh0 sh"), "shading ref missing: {}", c);
1099 assert!(c.contains("q\n"), "q missing");
1101 assert!(c.contains("Q\n"), "Q missing");
1102 }
1103
1104 #[test]
1105 fn test_fill_gradient_index_increments() {
1106 let mut g = PdfGraphics::new();
1107 g.fill_gradient(
1108 Length::ZERO,
1109 Length::ZERO,
1110 Length::from_pt(50.0),
1111 Length::from_pt(50.0),
1112 1,
1113 )
1114 .expect("test: should succeed");
1115 assert!(g.content().contains("/Sh1 sh"));
1116 }
1117
1118 #[test]
1121 fn test_set_opacity_gs_operator() {
1122 let mut g = PdfGraphics::new();
1123 g.set_opacity("gs1").expect("test: should succeed");
1124 assert!(g.content().contains("/gs1 gs"));
1125 }
1126
1127 #[test]
1128 fn test_set_stroke_opacity_gs_operator() {
1129 let mut g = PdfGraphics::new();
1130 g.set_stroke_opacity("gs2").expect("test: should succeed");
1131 assert!(g.content().contains("/gs2 gs"));
1132 }
1133
1134 #[test]
1137 fn test_draw_borders_dotted() {
1138 let mut g = PdfGraphics::new();
1139 g.draw_borders(
1140 Length::from_pt(5.0),
1141 Length::from_pt(5.0),
1142 Length::from_pt(80.0),
1143 Length::from_pt(40.0),
1144 [Length::from_pt(1.0); 4],
1145 [Color::BLACK; 4],
1146 [BorderStyle::Dotted; 4],
1147 )
1148 .expect("test: should succeed");
1149 assert!(
1151 g.content().contains("[1.000 2.000]"),
1152 "dotted dash pattern missing"
1153 );
1154 }
1155
1156 #[test]
1157 fn test_draw_borders_none_produces_no_lines() {
1158 let mut g = PdfGraphics::new();
1159 g.draw_borders(
1160 Length::from_pt(5.0),
1161 Length::from_pt(5.0),
1162 Length::from_pt(80.0),
1163 Length::from_pt(40.0),
1164 [Length::from_pt(1.0); 4],
1165 [Color::BLACK; 4],
1166 [BorderStyle::None; 4],
1167 )
1168 .expect("test: should succeed");
1169 let c = g.content();
1171 assert!(
1172 !c.contains("l S"),
1173 "none border style should not produce 'l S': {}",
1174 c
1175 );
1176 }
1177
1178 #[test]
1179 fn test_draw_borders_zero_width_produces_no_lines() {
1180 let mut g = PdfGraphics::new();
1181 g.draw_borders(
1182 Length::from_pt(0.0),
1183 Length::from_pt(0.0),
1184 Length::from_pt(100.0),
1185 Length::from_pt(50.0),
1186 [Length::ZERO; 4],
1187 [Color::BLACK; 4],
1188 [BorderStyle::Solid; 4],
1189 )
1190 .expect("test: should succeed");
1191 assert!(!g.content().contains("l S"), "zero-width should not stroke");
1193 }
1194
1195 #[test]
1198 fn test_fill_rectangle_with_radius_uses_bezier() {
1199 let mut g = PdfGraphics::new();
1200 let radii = [Length::from_pt(5.0); 4];
1201 g.fill_rectangle_with_radius(
1202 Length::from_pt(10.0),
1203 Length::from_pt(10.0),
1204 Length::from_pt(100.0),
1205 Length::from_pt(50.0),
1206 Some(radii),
1207 )
1208 .expect("test: should succeed");
1209 let c = g.content();
1210 assert!(c.contains(" c "), "Bezier 'c' operator missing: {}", c);
1212 assert!(c.contains("f\n"), "fill 'f' missing");
1214 }
1215
1216 #[test]
1217 fn test_fill_rectangle_no_radius_uses_simple_re() {
1218 let mut g = PdfGraphics::new();
1219 g.fill_rectangle_with_radius(
1220 Length::from_pt(10.0),
1221 Length::from_pt(10.0),
1222 Length::from_pt(100.0),
1223 Length::from_pt(50.0),
1224 None,
1225 )
1226 .expect("test: should succeed");
1227 let c = g.content();
1228 assert!(c.contains("re f"), "expected 're f': {}", c);
1230 }
1231
1232 #[test]
1235 fn test_default_creates_empty_graphics() {
1236 let g = PdfGraphics::default();
1237 assert_eq!(g.content(), "");
1238 }
1239}