1mod color;
2mod color_profiles;
3mod path;
4mod patterns;
5mod pdf_image;
6mod shadings;
7mod state;
8
9pub use color::Color;
10pub use color_profiles::{IccColorSpace, IccProfile, IccProfileManager, StandardIccProfile};
11pub use path::{LineCap, LineJoin, PathBuilder};
12pub use patterns::{
13 PaintType, PatternGraphicsContext, PatternManager, PatternMatrix, PatternType, TilingPattern,
14 TilingType,
15};
16pub use pdf_image::{ColorSpace as ImageColorSpace, Image, ImageFormat};
17pub use shadings::{
18 AxialShading, ColorStop, FunctionBasedShading, Point, RadialShading, ShadingDefinition,
19 ShadingManager, ShadingPattern, ShadingType,
20};
21pub use state::{
22 BlendMode, ExtGState, ExtGStateFont, ExtGStateManager, Halftone, LineDashPattern,
23 RenderingIntent, SoftMask, TransferFunction,
24};
25
26use crate::error::Result;
27use crate::text::{ColumnContent, ColumnLayout, Font, ListElement, Table};
28use std::fmt::Write;
29
30#[derive(Clone)]
31pub struct GraphicsContext {
32 operations: String,
33 current_color: Color,
34 stroke_color: Color,
35 line_width: f64,
36 fill_opacity: f64,
37 stroke_opacity: f64,
38 extgstate_manager: ExtGStateManager,
40 pending_extgstate: Option<ExtGState>,
41 current_dash_pattern: Option<LineDashPattern>,
42 current_miter_limit: f64,
43 current_line_cap: LineCap,
44 current_line_join: LineJoin,
45 current_rendering_intent: RenderingIntent,
46 current_flatness: f64,
47 current_smoothness: f64,
48}
49
50impl Default for GraphicsContext {
51 fn default() -> Self {
52 Self::new()
53 }
54}
55
56impl GraphicsContext {
57 pub fn new() -> Self {
58 Self {
59 operations: String::new(),
60 current_color: Color::black(),
61 stroke_color: Color::black(),
62 line_width: 1.0,
63 fill_opacity: 1.0,
64 stroke_opacity: 1.0,
65 extgstate_manager: ExtGStateManager::new(),
67 pending_extgstate: None,
68 current_dash_pattern: None,
69 current_miter_limit: 10.0,
70 current_line_cap: LineCap::Butt,
71 current_line_join: LineJoin::Miter,
72 current_rendering_intent: RenderingIntent::RelativeColorimetric,
73 current_flatness: 1.0,
74 current_smoothness: 0.0,
75 }
76 }
77
78 pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
79 writeln!(&mut self.operations, "{x:.2} {y:.2} m").unwrap();
80 self
81 }
82
83 pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
84 writeln!(&mut self.operations, "{x:.2} {y:.2} l").unwrap();
85 self
86 }
87
88 pub fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> &mut Self {
89 writeln!(
90 &mut self.operations,
91 "{x1:.2} {y1:.2} {x2:.2} {y2:.2} {x3:.2} {y3:.2} c"
92 )
93 .unwrap();
94 self
95 }
96
97 pub fn rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
98 writeln!(
99 &mut self.operations,
100 "{x:.2} {y:.2} {width:.2} {height:.2} re"
101 )
102 .unwrap();
103 self
104 }
105
106 pub fn circle(&mut self, cx: f64, cy: f64, radius: f64) -> &mut Self {
107 let k = 0.552284749831;
108 let r = radius;
109
110 self.move_to(cx + r, cy);
111 self.curve_to(cx + r, cy + k * r, cx + k * r, cy + r, cx, cy + r);
112 self.curve_to(cx - k * r, cy + r, cx - r, cy + k * r, cx - r, cy);
113 self.curve_to(cx - r, cy - k * r, cx - k * r, cy - r, cx, cy - r);
114 self.curve_to(cx + k * r, cy - r, cx + r, cy - k * r, cx + r, cy);
115 self.close_path()
116 }
117
118 pub fn close_path(&mut self) -> &mut Self {
119 self.operations.push_str("h\n");
120 self
121 }
122
123 pub fn stroke(&mut self) -> &mut Self {
124 self.apply_pending_extgstate().unwrap_or_default();
125 self.apply_stroke_color();
126 self.operations.push_str("S\n");
127 self
128 }
129
130 pub fn fill(&mut self) -> &mut Self {
131 self.apply_pending_extgstate().unwrap_or_default();
132 self.apply_fill_color();
133 self.operations.push_str("f\n");
134 self
135 }
136
137 pub fn fill_stroke(&mut self) -> &mut Self {
138 self.apply_pending_extgstate().unwrap_or_default();
139 self.apply_fill_color();
140 self.apply_stroke_color();
141 self.operations.push_str("B\n");
142 self
143 }
144
145 pub fn set_stroke_color(&mut self, color: Color) -> &mut Self {
146 self.stroke_color = color;
147 self
148 }
149
150 pub fn set_fill_color(&mut self, color: Color) -> &mut Self {
151 self.current_color = color;
152 self
153 }
154
155 pub fn set_line_width(&mut self, width: f64) -> &mut Self {
156 self.line_width = width;
157 writeln!(&mut self.operations, "{width:.2} w").unwrap();
158 self
159 }
160
161 pub fn set_line_cap(&mut self, cap: LineCap) -> &mut Self {
162 self.current_line_cap = cap;
163 writeln!(&mut self.operations, "{} J", cap as u8).unwrap();
164 self
165 }
166
167 pub fn set_line_join(&mut self, join: LineJoin) -> &mut Self {
168 self.current_line_join = join;
169 writeln!(&mut self.operations, "{} j", join as u8).unwrap();
170 self
171 }
172
173 pub fn set_opacity(&mut self, opacity: f64) -> &mut Self {
175 let opacity = opacity.clamp(0.0, 1.0);
176 self.fill_opacity = opacity;
177 self.stroke_opacity = opacity;
178 self
179 }
180
181 pub fn set_fill_opacity(&mut self, opacity: f64) -> &mut Self {
183 self.fill_opacity = opacity.clamp(0.0, 1.0);
184 self
185 }
186
187 pub fn set_stroke_opacity(&mut self, opacity: f64) -> &mut Self {
189 self.stroke_opacity = opacity.clamp(0.0, 1.0);
190 self
191 }
192
193 pub fn save_state(&mut self) -> &mut Self {
194 self.operations.push_str("q\n");
195 self
196 }
197
198 pub fn restore_state(&mut self) -> &mut Self {
199 self.operations.push_str("Q\n");
200 self
201 }
202
203 pub fn translate(&mut self, tx: f64, ty: f64) -> &mut Self {
204 writeln!(&mut self.operations, "1 0 0 1 {tx:.2} {ty:.2} cm").unwrap();
205 self
206 }
207
208 pub fn scale(&mut self, sx: f64, sy: f64) -> &mut Self {
209 writeln!(&mut self.operations, "{sx:.2} 0 0 {sy:.2} 0 0 cm").unwrap();
210 self
211 }
212
213 pub fn rotate(&mut self, angle: f64) -> &mut Self {
214 let cos = angle.cos();
215 let sin = angle.sin();
216 writeln!(
217 &mut self.operations,
218 "{:.6} {:.6} {:.6} {:.6} 0 0 cm",
219 cos, sin, -sin, cos
220 )
221 .unwrap();
222 self
223 }
224
225 pub fn transform(&mut self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> &mut Self {
226 writeln!(
227 &mut self.operations,
228 "{a:.2} {b:.2} {c:.2} {d:.2} {e:.2} {f:.2} cm"
229 )
230 .unwrap();
231 self
232 }
233
234 pub fn rectangle(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
235 self.rect(x, y, width, height)
236 }
237
238 pub fn draw_image(
239 &mut self,
240 image_name: &str,
241 x: f64,
242 y: f64,
243 width: f64,
244 height: f64,
245 ) -> &mut Self {
246 self.save_state();
248
249 writeln!(
252 &mut self.operations,
253 "{width:.2} 0 0 {height:.2} {x:.2} {y:.2} cm"
254 )
255 .unwrap();
256
257 writeln!(&mut self.operations, "/{image_name} Do").unwrap();
259
260 self.restore_state();
262
263 self
264 }
265
266 fn apply_stroke_color(&mut self) {
267 match self.stroke_color {
268 Color::Rgb(r, g, b) => {
269 writeln!(&mut self.operations, "{r:.3} {g:.3} {b:.3} RG").unwrap();
270 }
271 Color::Gray(g) => {
272 writeln!(&mut self.operations, "{g:.3} G").unwrap();
273 }
274 Color::Cmyk(c, m, y, k) => {
275 writeln!(&mut self.operations, "{c:.3} {m:.3} {y:.3} {k:.3} K").unwrap();
276 }
277 }
278 }
279
280 fn apply_fill_color(&mut self) {
281 match self.current_color {
282 Color::Rgb(r, g, b) => {
283 writeln!(&mut self.operations, "{r:.3} {g:.3} {b:.3} rg").unwrap();
284 }
285 Color::Gray(g) => {
286 writeln!(&mut self.operations, "{g:.3} g").unwrap();
287 }
288 Color::Cmyk(c, m, y, k) => {
289 writeln!(&mut self.operations, "{c:.3} {m:.3} {y:.3} {k:.3} k").unwrap();
290 }
291 }
292 }
293
294 pub(crate) fn generate_operations(&self) -> Result<Vec<u8>> {
295 Ok(self.operations.as_bytes().to_vec())
296 }
297
298 pub fn uses_transparency(&self) -> bool {
300 self.fill_opacity < 1.0 || self.stroke_opacity < 1.0
301 }
302
303 pub fn generate_graphics_state_dict(&self) -> Option<String> {
305 if !self.uses_transparency() {
306 return None;
307 }
308
309 let mut dict = String::from("<< /Type /ExtGState");
310
311 if self.fill_opacity < 1.0 {
312 write!(&mut dict, " /ca {:.3}", self.fill_opacity).unwrap();
313 }
314
315 if self.stroke_opacity < 1.0 {
316 write!(&mut dict, " /CA {:.3}", self.stroke_opacity).unwrap();
317 }
318
319 dict.push_str(" >>");
320 Some(dict)
321 }
322
323 pub fn fill_color(&self) -> Color {
325 self.current_color
326 }
327
328 pub fn stroke_color(&self) -> Color {
330 self.stroke_color
331 }
332
333 pub fn line_width(&self) -> f64 {
335 self.line_width
336 }
337
338 pub fn fill_opacity(&self) -> f64 {
340 self.fill_opacity
341 }
342
343 pub fn stroke_opacity(&self) -> f64 {
345 self.stroke_opacity
346 }
347
348 pub fn operations(&self) -> &str {
350 &self.operations
351 }
352
353 pub fn clear(&mut self) {
355 self.operations.clear();
356 }
357
358 pub fn begin_text(&mut self) -> &mut Self {
360 self.operations.push_str("BT\n");
361 self
362 }
363
364 pub fn end_text(&mut self) -> &mut Self {
366 self.operations.push_str("ET\n");
367 self
368 }
369
370 pub fn set_font(&mut self, font: Font, size: f64) -> &mut Self {
372 writeln!(&mut self.operations, "/{} {} Tf", font.pdf_name(), size).unwrap();
373 self
374 }
375
376 pub fn set_text_position(&mut self, x: f64, y: f64) -> &mut Self {
378 writeln!(&mut self.operations, "{x:.2} {y:.2} Td").unwrap();
379 self
380 }
381
382 pub fn show_text(&mut self, text: &str) -> Result<&mut Self> {
384 self.operations.push('(');
386 for ch in text.chars() {
387 match ch {
388 '(' => self.operations.push_str("\\("),
389 ')' => self.operations.push_str("\\)"),
390 '\\' => self.operations.push_str("\\\\"),
391 '\n' => self.operations.push_str("\\n"),
392 '\r' => self.operations.push_str("\\r"),
393 '\t' => self.operations.push_str("\\t"),
394 _ => self.operations.push(ch),
395 }
396 }
397 self.operations.push_str(") Tj\n");
398 Ok(self)
399 }
400
401 pub fn render_table(&mut self, table: &Table) -> Result<()> {
403 table.render(self)
404 }
405
406 pub fn render_list(&mut self, list: &ListElement) -> Result<()> {
408 match list {
409 ListElement::Ordered(ordered) => ordered.render(self),
410 ListElement::Unordered(unordered) => unordered.render(self),
411 }
412 }
413
414 pub fn render_column_layout(
416 &mut self,
417 layout: &ColumnLayout,
418 content: &ColumnContent,
419 x: f64,
420 y: f64,
421 height: f64,
422 ) -> Result<()> {
423 layout.render(self, content, x, y, height)
424 }
425
426 pub fn set_line_dash_pattern(&mut self, pattern: LineDashPattern) -> &mut Self {
430 self.current_dash_pattern = Some(pattern.clone());
431 writeln!(&mut self.operations, "{} d", pattern.to_pdf_string()).unwrap();
432 self
433 }
434
435 pub fn set_line_solid(&mut self) -> &mut Self {
437 self.current_dash_pattern = None;
438 self.operations.push_str("[] 0 d\n");
439 self
440 }
441
442 pub fn set_miter_limit(&mut self, limit: f64) -> &mut Self {
444 self.current_miter_limit = limit.max(1.0);
445 writeln!(&mut self.operations, "{:.2} M", self.current_miter_limit).unwrap();
446 self
447 }
448
449 pub fn set_rendering_intent(&mut self, intent: RenderingIntent) -> &mut Self {
451 self.current_rendering_intent = intent;
452 writeln!(&mut self.operations, "/{} ri", intent.pdf_name()).unwrap();
453 self
454 }
455
456 pub fn set_flatness(&mut self, flatness: f64) -> &mut Self {
458 self.current_flatness = flatness.clamp(0.0, 100.0);
459 writeln!(&mut self.operations, "{:.2} i", self.current_flatness).unwrap();
460 self
461 }
462
463 pub fn apply_extgstate(&mut self, state: ExtGState) -> Result<&mut Self> {
465 let state_name = self.extgstate_manager.add_state(state)?;
466 writeln!(&mut self.operations, "/{state_name} gs").unwrap();
467 Ok(self)
468 }
469
470 fn set_pending_extgstate(&mut self, state: ExtGState) {
472 self.pending_extgstate = Some(state);
473 }
474
475 fn apply_pending_extgstate(&mut self) -> Result<()> {
477 if let Some(state) = self.pending_extgstate.take() {
478 let state_name = self.extgstate_manager.add_state(state)?;
479 writeln!(&mut self.operations, "/{state_name} gs").unwrap();
480 }
481 Ok(())
482 }
483
484 pub fn with_extgstate<F>(&mut self, builder: F) -> Result<&mut Self>
486 where
487 F: FnOnce(ExtGState) -> ExtGState,
488 {
489 let state = builder(ExtGState::new());
490 self.apply_extgstate(state)
491 }
492
493 pub fn set_blend_mode(&mut self, mode: BlendMode) -> Result<&mut Self> {
495 let state = ExtGState::new().with_blend_mode(mode);
496 self.apply_extgstate(state)
497 }
498
499 pub fn set_alpha(&mut self, alpha: f64) -> Result<&mut Self> {
501 let state = ExtGState::new().with_alpha(alpha);
502 self.set_pending_extgstate(state);
503 Ok(self)
504 }
505
506 pub fn set_alpha_stroke(&mut self, alpha: f64) -> Result<&mut Self> {
508 let state = ExtGState::new().with_alpha_stroke(alpha);
509 self.set_pending_extgstate(state);
510 Ok(self)
511 }
512
513 pub fn set_alpha_fill(&mut self, alpha: f64) -> Result<&mut Self> {
515 let state = ExtGState::new().with_alpha_fill(alpha);
516 self.set_pending_extgstate(state);
517 Ok(self)
518 }
519
520 pub fn set_overprint_stroke(&mut self, overprint: bool) -> Result<&mut Self> {
522 let state = ExtGState::new().with_overprint_stroke(overprint);
523 self.apply_extgstate(state)
524 }
525
526 pub fn set_overprint_fill(&mut self, overprint: bool) -> Result<&mut Self> {
528 let state = ExtGState::new().with_overprint_fill(overprint);
529 self.apply_extgstate(state)
530 }
531
532 pub fn set_stroke_adjustment(&mut self, adjustment: bool) -> Result<&mut Self> {
534 let state = ExtGState::new().with_stroke_adjustment(adjustment);
535 self.apply_extgstate(state)
536 }
537
538 pub fn set_smoothness(&mut self, smoothness: f64) -> Result<&mut Self> {
540 self.current_smoothness = smoothness.clamp(0.0, 1.0);
541 let state = ExtGState::new().with_smoothness(self.current_smoothness);
542 self.apply_extgstate(state)
543 }
544
545 pub fn line_dash_pattern(&self) -> Option<&LineDashPattern> {
549 self.current_dash_pattern.as_ref()
550 }
551
552 pub fn miter_limit(&self) -> f64 {
554 self.current_miter_limit
555 }
556
557 pub fn line_cap(&self) -> LineCap {
559 self.current_line_cap
560 }
561
562 pub fn line_join(&self) -> LineJoin {
564 self.current_line_join
565 }
566
567 pub fn rendering_intent(&self) -> RenderingIntent {
569 self.current_rendering_intent
570 }
571
572 pub fn flatness(&self) -> f64 {
574 self.current_flatness
575 }
576
577 pub fn smoothness(&self) -> f64 {
579 self.current_smoothness
580 }
581
582 pub fn extgstate_manager(&self) -> &ExtGStateManager {
584 &self.extgstate_manager
585 }
586
587 pub fn extgstate_manager_mut(&mut self) -> &mut ExtGStateManager {
589 &mut self.extgstate_manager
590 }
591
592 pub fn generate_extgstate_resources(&self) -> Result<String> {
594 self.extgstate_manager.to_resource_dictionary()
595 }
596
597 pub fn has_extgstates(&self) -> bool {
599 self.extgstate_manager.count() > 0
600 }
601
602 pub fn add_command(&mut self, command: &str) {
604 self.operations.push_str(command);
605 self.operations.push('\n');
606 }
607
608 pub fn clip(&mut self) -> &mut Self {
610 self.operations.push_str("W\n");
611 self
612 }
613
614 pub fn clip_even_odd(&mut self) -> &mut Self {
616 self.operations.push_str("W*\n");
617 self
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[test]
626 fn test_graphics_context_new() {
627 let ctx = GraphicsContext::new();
628 assert_eq!(ctx.fill_color(), Color::black());
629 assert_eq!(ctx.stroke_color(), Color::black());
630 assert_eq!(ctx.line_width(), 1.0);
631 assert_eq!(ctx.fill_opacity(), 1.0);
632 assert_eq!(ctx.stroke_opacity(), 1.0);
633 assert!(ctx.operations().is_empty());
634 }
635
636 #[test]
637 fn test_graphics_context_default() {
638 let ctx = GraphicsContext::default();
639 assert_eq!(ctx.fill_color(), Color::black());
640 assert_eq!(ctx.stroke_color(), Color::black());
641 assert_eq!(ctx.line_width(), 1.0);
642 }
643
644 #[test]
645 fn test_move_to() {
646 let mut ctx = GraphicsContext::new();
647 ctx.move_to(10.0, 20.0);
648 assert!(ctx.operations().contains("10.00 20.00 m\n"));
649 }
650
651 #[test]
652 fn test_line_to() {
653 let mut ctx = GraphicsContext::new();
654 ctx.line_to(30.0, 40.0);
655 assert!(ctx.operations().contains("30.00 40.00 l\n"));
656 }
657
658 #[test]
659 fn test_curve_to() {
660 let mut ctx = GraphicsContext::new();
661 ctx.curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 60.0);
662 assert!(ctx
663 .operations()
664 .contains("10.00 20.00 30.00 40.00 50.00 60.00 c\n"));
665 }
666
667 #[test]
668 fn test_rect() {
669 let mut ctx = GraphicsContext::new();
670 ctx.rect(10.0, 20.0, 100.0, 50.0);
671 assert!(ctx.operations().contains("10.00 20.00 100.00 50.00 re\n"));
672 }
673
674 #[test]
675 fn test_rectangle_alias() {
676 let mut ctx = GraphicsContext::new();
677 ctx.rectangle(10.0, 20.0, 100.0, 50.0);
678 assert!(ctx.operations().contains("10.00 20.00 100.00 50.00 re\n"));
679 }
680
681 #[test]
682 fn test_circle() {
683 let mut ctx = GraphicsContext::new();
684 ctx.circle(50.0, 50.0, 25.0);
685
686 let ops = ctx.operations();
687 assert!(ops.contains("75.00 50.00 m\n"));
689 assert!(ops.contains(" c\n"));
691 assert!(ops.contains("h\n"));
693 }
694
695 #[test]
696 fn test_close_path() {
697 let mut ctx = GraphicsContext::new();
698 ctx.close_path();
699 assert!(ctx.operations().contains("h\n"));
700 }
701
702 #[test]
703 fn test_stroke() {
704 let mut ctx = GraphicsContext::new();
705 ctx.set_stroke_color(Color::red());
706 ctx.rect(0.0, 0.0, 10.0, 10.0);
707 ctx.stroke();
708
709 let ops = ctx.operations();
710 assert!(ops.contains("1.000 0.000 0.000 RG\n"));
711 assert!(ops.contains("S\n"));
712 }
713
714 #[test]
715 fn test_fill() {
716 let mut ctx = GraphicsContext::new();
717 ctx.set_fill_color(Color::blue());
718 ctx.rect(0.0, 0.0, 10.0, 10.0);
719 ctx.fill();
720
721 let ops = ctx.operations();
722 assert!(ops.contains("0.000 0.000 1.000 rg\n"));
723 assert!(ops.contains("f\n"));
724 }
725
726 #[test]
727 fn test_fill_stroke() {
728 let mut ctx = GraphicsContext::new();
729 ctx.set_fill_color(Color::green());
730 ctx.set_stroke_color(Color::red());
731 ctx.rect(0.0, 0.0, 10.0, 10.0);
732 ctx.fill_stroke();
733
734 let ops = ctx.operations();
735 assert!(ops.contains("0.000 1.000 0.000 rg\n"));
736 assert!(ops.contains("1.000 0.000 0.000 RG\n"));
737 assert!(ops.contains("B\n"));
738 }
739
740 #[test]
741 fn test_set_stroke_color() {
742 let mut ctx = GraphicsContext::new();
743 ctx.set_stroke_color(Color::rgb(0.5, 0.6, 0.7));
744 assert_eq!(ctx.stroke_color(), Color::Rgb(0.5, 0.6, 0.7));
745 }
746
747 #[test]
748 fn test_set_fill_color() {
749 let mut ctx = GraphicsContext::new();
750 ctx.set_fill_color(Color::gray(0.5));
751 assert_eq!(ctx.fill_color(), Color::Gray(0.5));
752 }
753
754 #[test]
755 fn test_set_line_width() {
756 let mut ctx = GraphicsContext::new();
757 ctx.set_line_width(2.5);
758 assert_eq!(ctx.line_width(), 2.5);
759 assert!(ctx.operations().contains("2.50 w\n"));
760 }
761
762 #[test]
763 fn test_set_line_cap() {
764 let mut ctx = GraphicsContext::new();
765 ctx.set_line_cap(LineCap::Round);
766 assert!(ctx.operations().contains("1 J\n"));
767
768 ctx.set_line_cap(LineCap::Butt);
769 assert!(ctx.operations().contains("0 J\n"));
770
771 ctx.set_line_cap(LineCap::Square);
772 assert!(ctx.operations().contains("2 J\n"));
773 }
774
775 #[test]
776 fn test_set_line_join() {
777 let mut ctx = GraphicsContext::new();
778 ctx.set_line_join(LineJoin::Round);
779 assert!(ctx.operations().contains("1 j\n"));
780
781 ctx.set_line_join(LineJoin::Miter);
782 assert!(ctx.operations().contains("0 j\n"));
783
784 ctx.set_line_join(LineJoin::Bevel);
785 assert!(ctx.operations().contains("2 j\n"));
786 }
787
788 #[test]
789 fn test_save_restore_state() {
790 let mut ctx = GraphicsContext::new();
791 ctx.save_state();
792 assert!(ctx.operations().contains("q\n"));
793
794 ctx.restore_state();
795 assert!(ctx.operations().contains("Q\n"));
796 }
797
798 #[test]
799 fn test_translate() {
800 let mut ctx = GraphicsContext::new();
801 ctx.translate(50.0, 100.0);
802 assert!(ctx.operations().contains("1 0 0 1 50.00 100.00 cm\n"));
803 }
804
805 #[test]
806 fn test_scale() {
807 let mut ctx = GraphicsContext::new();
808 ctx.scale(2.0, 3.0);
809 assert!(ctx.operations().contains("2.00 0 0 3.00 0 0 cm\n"));
810 }
811
812 #[test]
813 fn test_rotate() {
814 let mut ctx = GraphicsContext::new();
815 let angle = std::f64::consts::PI / 4.0; ctx.rotate(angle);
817
818 let ops = ctx.operations();
819 assert!(ops.contains(" cm\n"));
820 assert!(ops.contains("0.707107")); }
823
824 #[test]
825 fn test_transform() {
826 let mut ctx = GraphicsContext::new();
827 ctx.transform(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
828 assert!(ctx
829 .operations()
830 .contains("1.00 2.00 3.00 4.00 5.00 6.00 cm\n"));
831 }
832
833 #[test]
834 fn test_draw_image() {
835 let mut ctx = GraphicsContext::new();
836 ctx.draw_image("Image1", 10.0, 20.0, 100.0, 150.0);
837
838 let ops = ctx.operations();
839 assert!(ops.contains("q\n")); assert!(ops.contains("100.00 0 0 150.00 10.00 20.00 cm\n")); assert!(ops.contains("/Image1 Do\n")); assert!(ops.contains("Q\n")); }
844
845 #[test]
846 fn test_gray_color_operations() {
847 let mut ctx = GraphicsContext::new();
848 ctx.set_stroke_color(Color::gray(0.5));
849 ctx.set_fill_color(Color::gray(0.7));
850 ctx.stroke();
851 ctx.fill();
852
853 let ops = ctx.operations();
854 assert!(ops.contains("0.500 G\n")); assert!(ops.contains("0.700 g\n")); }
857
858 #[test]
859 fn test_cmyk_color_operations() {
860 let mut ctx = GraphicsContext::new();
861 ctx.set_stroke_color(Color::cmyk(0.1, 0.2, 0.3, 0.4));
862 ctx.set_fill_color(Color::cmyk(0.5, 0.6, 0.7, 0.8));
863 ctx.stroke();
864 ctx.fill();
865
866 let ops = ctx.operations();
867 assert!(ops.contains("0.100 0.200 0.300 0.400 K\n")); assert!(ops.contains("0.500 0.600 0.700 0.800 k\n")); }
870
871 #[test]
872 fn test_method_chaining() {
873 let mut ctx = GraphicsContext::new();
874 ctx.move_to(0.0, 0.0)
875 .line_to(10.0, 0.0)
876 .line_to(10.0, 10.0)
877 .line_to(0.0, 10.0)
878 .close_path()
879 .set_fill_color(Color::red())
880 .fill();
881
882 let ops = ctx.operations();
883 assert!(ops.contains("0.00 0.00 m\n"));
884 assert!(ops.contains("10.00 0.00 l\n"));
885 assert!(ops.contains("10.00 10.00 l\n"));
886 assert!(ops.contains("0.00 10.00 l\n"));
887 assert!(ops.contains("h\n"));
888 assert!(ops.contains("f\n"));
889 }
890
891 #[test]
892 fn test_generate_operations() {
893 let mut ctx = GraphicsContext::new();
894 ctx.rect(0.0, 0.0, 10.0, 10.0);
895
896 let result = ctx.generate_operations();
897 assert!(result.is_ok());
898 let bytes = result.unwrap();
899 let ops_string = String::from_utf8(bytes).unwrap();
900 assert!(ops_string.contains("0.00 0.00 10.00 10.00 re"));
901 }
902
903 #[test]
904 fn test_clear_operations() {
905 let mut ctx = GraphicsContext::new();
906 ctx.rect(0.0, 0.0, 10.0, 10.0);
907 assert!(!ctx.operations().is_empty());
908
909 ctx.clear();
910 assert!(ctx.operations().is_empty());
911 }
912
913 #[test]
914 fn test_complex_path() {
915 let mut ctx = GraphicsContext::new();
916 ctx.save_state()
917 .translate(100.0, 100.0)
918 .rotate(std::f64::consts::PI / 6.0)
919 .scale(2.0, 2.0)
920 .set_line_width(2.0)
921 .set_stroke_color(Color::blue())
922 .move_to(0.0, 0.0)
923 .line_to(50.0, 0.0)
924 .curve_to(50.0, 25.0, 25.0, 50.0, 0.0, 50.0)
925 .close_path()
926 .stroke()
927 .restore_state();
928
929 let ops = ctx.operations();
930 assert!(ops.contains("q\n"));
931 assert!(ops.contains("cm\n"));
932 assert!(ops.contains("2.00 w\n"));
933 assert!(ops.contains("0.000 0.000 1.000 RG\n"));
934 assert!(ops.contains("S\n"));
935 assert!(ops.contains("Q\n"));
936 }
937
938 #[test]
939 fn test_graphics_context_clone() {
940 let mut ctx = GraphicsContext::new();
941 ctx.set_fill_color(Color::red());
942 ctx.set_stroke_color(Color::blue());
943 ctx.set_line_width(3.0);
944 ctx.set_opacity(0.5);
945 ctx.rect(0.0, 0.0, 10.0, 10.0);
946
947 let ctx_clone = ctx.clone();
948 assert_eq!(ctx_clone.fill_color(), Color::red());
949 assert_eq!(ctx_clone.stroke_color(), Color::blue());
950 assert_eq!(ctx_clone.line_width(), 3.0);
951 assert_eq!(ctx_clone.fill_opacity(), 0.5);
952 assert_eq!(ctx_clone.stroke_opacity(), 0.5);
953 assert_eq!(ctx_clone.operations(), ctx.operations());
954 }
955
956 #[test]
957 fn test_set_opacity() {
958 let mut ctx = GraphicsContext::new();
959
960 ctx.set_opacity(0.5);
962 assert_eq!(ctx.fill_opacity(), 0.5);
963 assert_eq!(ctx.stroke_opacity(), 0.5);
964
965 ctx.set_opacity(1.5);
967 assert_eq!(ctx.fill_opacity(), 1.0);
968 assert_eq!(ctx.stroke_opacity(), 1.0);
969
970 ctx.set_opacity(-0.5);
971 assert_eq!(ctx.fill_opacity(), 0.0);
972 assert_eq!(ctx.stroke_opacity(), 0.0);
973 }
974
975 #[test]
976 fn test_set_fill_opacity() {
977 let mut ctx = GraphicsContext::new();
978
979 ctx.set_fill_opacity(0.3);
980 assert_eq!(ctx.fill_opacity(), 0.3);
981 assert_eq!(ctx.stroke_opacity(), 1.0); ctx.set_fill_opacity(2.0);
985 assert_eq!(ctx.fill_opacity(), 1.0);
986 }
987
988 #[test]
989 fn test_set_stroke_opacity() {
990 let mut ctx = GraphicsContext::new();
991
992 ctx.set_stroke_opacity(0.7);
993 assert_eq!(ctx.stroke_opacity(), 0.7);
994 assert_eq!(ctx.fill_opacity(), 1.0); ctx.set_stroke_opacity(-1.0);
998 assert_eq!(ctx.stroke_opacity(), 0.0);
999 }
1000
1001 #[test]
1002 fn test_uses_transparency() {
1003 let mut ctx = GraphicsContext::new();
1004
1005 assert!(!ctx.uses_transparency());
1007
1008 ctx.set_fill_opacity(0.5);
1010 assert!(ctx.uses_transparency());
1011
1012 ctx.set_fill_opacity(1.0);
1014 assert!(!ctx.uses_transparency());
1015 ctx.set_stroke_opacity(0.8);
1016 assert!(ctx.uses_transparency());
1017
1018 ctx.set_fill_opacity(0.5);
1020 assert!(ctx.uses_transparency());
1021 }
1022
1023 #[test]
1024 fn test_generate_graphics_state_dict() {
1025 let mut ctx = GraphicsContext::new();
1026
1027 assert_eq!(ctx.generate_graphics_state_dict(), None);
1029
1030 ctx.set_fill_opacity(0.5);
1032 let dict = ctx.generate_graphics_state_dict().unwrap();
1033 assert!(dict.contains("/Type /ExtGState"));
1034 assert!(dict.contains("/ca 0.500"));
1035 assert!(!dict.contains("/CA"));
1036
1037 ctx.set_fill_opacity(1.0);
1039 ctx.set_stroke_opacity(0.75);
1040 let dict = ctx.generate_graphics_state_dict().unwrap();
1041 assert!(dict.contains("/Type /ExtGState"));
1042 assert!(dict.contains("/CA 0.750"));
1043 assert!(!dict.contains("/ca"));
1044
1045 ctx.set_fill_opacity(0.25);
1047 let dict = ctx.generate_graphics_state_dict().unwrap();
1048 assert!(dict.contains("/Type /ExtGState"));
1049 assert!(dict.contains("/ca 0.250"));
1050 assert!(dict.contains("/CA 0.750"));
1051 }
1052
1053 #[test]
1054 fn test_opacity_with_graphics_operations() {
1055 let mut ctx = GraphicsContext::new();
1056
1057 ctx.set_fill_color(Color::red())
1058 .set_opacity(0.5)
1059 .rect(10.0, 10.0, 100.0, 100.0)
1060 .fill();
1061
1062 assert_eq!(ctx.fill_opacity(), 0.5);
1063 assert_eq!(ctx.stroke_opacity(), 0.5);
1064
1065 let ops = ctx.operations();
1066 assert!(ops.contains("10.00 10.00 100.00 100.00 re"));
1067 assert!(ops.contains("1.000 0.000 0.000 rg")); assert!(ops.contains("f")); }
1070
1071 #[test]
1072 fn test_begin_end_text() {
1073 let mut ctx = GraphicsContext::new();
1074 ctx.begin_text();
1075 assert!(ctx.operations().contains("BT\n"));
1076
1077 ctx.end_text();
1078 assert!(ctx.operations().contains("ET\n"));
1079 }
1080
1081 #[test]
1082 fn test_set_font() {
1083 let mut ctx = GraphicsContext::new();
1084 ctx.set_font(Font::Helvetica, 12.0);
1085 assert!(ctx.operations().contains("/Helvetica 12 Tf\n"));
1086
1087 ctx.set_font(Font::TimesBold, 14.5);
1088 assert!(ctx.operations().contains("/Times-Bold 14.5 Tf\n"));
1089 }
1090
1091 #[test]
1092 fn test_set_text_position() {
1093 let mut ctx = GraphicsContext::new();
1094 ctx.set_text_position(100.0, 200.0);
1095 assert!(ctx.operations().contains("100.00 200.00 Td\n"));
1096 }
1097
1098 #[test]
1099 fn test_show_text() {
1100 let mut ctx = GraphicsContext::new();
1101 ctx.show_text("Hello World").unwrap();
1102 assert!(ctx.operations().contains("(Hello World) Tj\n"));
1103 }
1104
1105 #[test]
1106 fn test_show_text_with_escaping() {
1107 let mut ctx = GraphicsContext::new();
1108 ctx.show_text("Test (parentheses)").unwrap();
1109 assert!(ctx.operations().contains("(Test \\(parentheses\\)) Tj\n"));
1110
1111 ctx.clear();
1112 ctx.show_text("Back\\slash").unwrap();
1113 assert!(ctx.operations().contains("(Back\\\\slash) Tj\n"));
1114
1115 ctx.clear();
1116 ctx.show_text("Line\nBreak").unwrap();
1117 assert!(ctx.operations().contains("(Line\\nBreak) Tj\n"));
1118 }
1119
1120 #[test]
1121 fn test_text_operations_chaining() {
1122 let mut ctx = GraphicsContext::new();
1123 ctx.begin_text()
1124 .set_font(Font::Courier, 10.0)
1125 .set_text_position(50.0, 100.0)
1126 .show_text("Test")
1127 .unwrap()
1128 .end_text();
1129
1130 let ops = ctx.operations();
1131 assert!(ops.contains("BT\n"));
1132 assert!(ops.contains("/Courier 10 Tf\n"));
1133 assert!(ops.contains("50.00 100.00 Td\n"));
1134 assert!(ops.contains("(Test) Tj\n"));
1135 assert!(ops.contains("ET\n"));
1136 }
1137
1138 #[test]
1139 fn test_clip() {
1140 let mut ctx = GraphicsContext::new();
1141 ctx.clip();
1142 assert!(ctx.operations().contains("W\n"));
1143 }
1144
1145 #[test]
1146 fn test_clip_even_odd() {
1147 let mut ctx = GraphicsContext::new();
1148 ctx.clip_even_odd();
1149 assert!(ctx.operations().contains("W*\n"));
1150 }
1151
1152 #[test]
1153 fn test_clipping_with_path() {
1154 let mut ctx = GraphicsContext::new();
1155
1156 ctx.rect(10.0, 10.0, 100.0, 50.0).clip();
1158
1159 let ops = ctx.operations();
1160 assert!(ops.contains("10.00 10.00 100.00 50.00 re\n"));
1161 assert!(ops.contains("W\n"));
1162 }
1163
1164 #[test]
1165 fn test_clipping_even_odd_with_path() {
1166 let mut ctx = GraphicsContext::new();
1167
1168 ctx.move_to(0.0, 0.0)
1170 .line_to(100.0, 0.0)
1171 .line_to(100.0, 100.0)
1172 .line_to(0.0, 100.0)
1173 .close_path()
1174 .clip_even_odd();
1175
1176 let ops = ctx.operations();
1177 assert!(ops.contains("0.00 0.00 m\n"));
1178 assert!(ops.contains("100.00 0.00 l\n"));
1179 assert!(ops.contains("100.00 100.00 l\n"));
1180 assert!(ops.contains("0.00 100.00 l\n"));
1181 assert!(ops.contains("h\n"));
1182 assert!(ops.contains("W*\n"));
1183 }
1184
1185 #[test]
1186 fn test_clipping_chaining() {
1187 let mut ctx = GraphicsContext::new();
1188
1189 ctx.save_state()
1191 .rect(20.0, 20.0, 60.0, 60.0)
1192 .clip()
1193 .set_fill_color(Color::red())
1194 .rect(0.0, 0.0, 100.0, 100.0)
1195 .fill()
1196 .restore_state();
1197
1198 let ops = ctx.operations();
1199 assert!(ops.contains("q\n"));
1200 assert!(ops.contains("20.00 20.00 60.00 60.00 re\n"));
1201 assert!(ops.contains("W\n"));
1202 assert!(ops.contains("1.000 0.000 0.000 rg\n"));
1203 assert!(ops.contains("0.00 0.00 100.00 100.00 re\n"));
1204 assert!(ops.contains("f\n"));
1205 assert!(ops.contains("Q\n"));
1206 }
1207
1208 #[test]
1209 fn test_multiple_clipping_regions() {
1210 let mut ctx = GraphicsContext::new();
1211
1212 ctx.save_state()
1214 .rect(0.0, 0.0, 200.0, 200.0)
1215 .clip()
1216 .save_state()
1217 .circle(100.0, 100.0, 50.0)
1218 .clip_even_odd()
1219 .set_fill_color(Color::blue())
1220 .rect(50.0, 50.0, 100.0, 100.0)
1221 .fill()
1222 .restore_state()
1223 .restore_state();
1224
1225 let ops = ctx.operations();
1226 let q_count = ops.matches("q\n").count();
1228 let q_restore_count = ops.matches("Q\n").count();
1229 assert_eq!(q_count, 2);
1230 assert_eq!(q_restore_count, 2);
1231
1232 assert!(ops.contains("W\n"));
1234 assert!(ops.contains("W*\n"));
1235 }
1236}