1use crate::error::{PdfError, Result};
7use crate::graphics::{LineCap, LineJoin};
8use crate::text::Font;
9use std::collections::HashMap;
10use std::fmt::Write;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum RenderingIntent {
15 AbsoluteColorimetric,
17 RelativeColorimetric,
19 Saturation,
21 Perceptual,
23}
24
25impl RenderingIntent {
26 pub fn pdf_name(&self) -> &'static str {
28 match self {
29 RenderingIntent::AbsoluteColorimetric => "AbsoluteColorimetric",
30 RenderingIntent::RelativeColorimetric => "RelativeColorimetric",
31 RenderingIntent::Saturation => "Saturation",
32 RenderingIntent::Perceptual => "Perceptual",
33 }
34 }
35}
36
37#[derive(Debug, Clone, PartialEq)]
39pub enum BlendMode {
40 Normal,
42 Multiply,
44 Screen,
46 Overlay,
48 SoftLight,
50 HardLight,
52 ColorDodge,
54 ColorBurn,
56 Darken,
58 Lighten,
60 Difference,
62 Exclusion,
64 Hue,
66 Saturation,
68 Color,
70 Luminosity,
72}
73
74impl BlendMode {
75 pub fn pdf_name(&self) -> &'static str {
77 match self {
78 BlendMode::Normal => "Normal",
79 BlendMode::Multiply => "Multiply",
80 BlendMode::Screen => "Screen",
81 BlendMode::Overlay => "Overlay",
82 BlendMode::SoftLight => "SoftLight",
83 BlendMode::HardLight => "HardLight",
84 BlendMode::ColorDodge => "ColorDodge",
85 BlendMode::ColorBurn => "ColorBurn",
86 BlendMode::Darken => "Darken",
87 BlendMode::Lighten => "Lighten",
88 BlendMode::Difference => "Difference",
89 BlendMode::Exclusion => "Exclusion",
90 BlendMode::Hue => "Hue",
91 BlendMode::Saturation => "Saturation",
92 BlendMode::Color => "Color",
93 BlendMode::Luminosity => "Luminosity",
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq)]
100pub struct LineDashPattern {
101 pub array: Vec<f64>,
103 pub phase: f64,
105}
106
107impl LineDashPattern {
108 pub fn new(array: Vec<f64>, phase: f64) -> Self {
110 Self { array, phase }
111 }
112
113 pub fn solid() -> Self {
115 Self {
116 array: Vec::new(),
117 phase: 0.0,
118 }
119 }
120
121 pub fn dashed(dash_length: f64, gap_length: f64) -> Self {
123 Self {
124 array: vec![dash_length, gap_length],
125 phase: 0.0,
126 }
127 }
128
129 pub fn dotted(dot_size: f64, gap_size: f64) -> Self {
131 Self {
132 array: vec![dot_size, gap_size],
133 phase: 0.0,
134 }
135 }
136
137 pub fn to_pdf_string(&self) -> String {
139 if self.array.is_empty() {
140 "[] 0".to_string()
141 } else {
142 let array_str = self
143 .array
144 .iter()
145 .map(|&x| format!("{x:.2}"))
146 .collect::<Vec<_>>()
147 .join(" ");
148 format!("[{array_str}] {:.2}", self.phase)
149 }
150 }
151}
152
153#[derive(Debug, Clone, PartialEq)]
155pub struct ExtGStateFont {
156 pub font: Font,
158 pub size: f64,
160}
161
162impl ExtGStateFont {
163 pub fn new(font: Font, size: f64) -> Self {
165 Self { font, size }
166 }
167}
168
169#[derive(Debug, Clone, PartialEq)]
171pub enum TransferFunction {
172 Identity,
174 Custom(String),
176}
177
178#[derive(Debug, Clone, PartialEq)]
180pub enum Halftone {
181 Default,
183 Custom(String),
185}
186
187#[derive(Debug, Clone, PartialEq)]
189pub enum SoftMask {
190 None,
192 Custom(String),
194}
195
196#[derive(Debug, Clone)]
198pub struct ExtGState {
199 pub line_width: Option<f64>,
202 pub line_cap: Option<LineCap>,
204 pub line_join: Option<LineJoin>,
206 pub miter_limit: Option<f64>,
208 pub dash_pattern: Option<LineDashPattern>,
210
211 pub rendering_intent: Option<RenderingIntent>,
214
215 pub overprint_stroke: Option<bool>,
218 pub overprint_fill: Option<bool>,
220 pub overprint_mode: Option<u8>,
222
223 pub font: Option<ExtGStateFont>,
226
227 pub black_generation: Option<TransferFunction>,
230 pub black_generation_2: Option<TransferFunction>,
232 pub undercolor_removal: Option<TransferFunction>,
234 pub undercolor_removal_2: Option<TransferFunction>,
236 pub transfer_function: Option<TransferFunction>,
238 pub transfer_function_2: Option<TransferFunction>,
240
241 pub halftone: Option<Halftone>,
244
245 pub flatness: Option<f64>,
248 pub smoothness: Option<f64>,
250
251 pub stroke_adjustment: Option<bool>,
254
255 pub blend_mode: Option<BlendMode>,
258 pub soft_mask: Option<SoftMask>,
260 pub alpha_stroke: Option<f64>,
262 pub alpha_fill: Option<f64>,
264 pub alpha_is_shape: Option<bool>,
266 pub text_knockout: Option<bool>,
268
269 pub use_black_point_compensation: Option<bool>,
272}
273
274impl Default for ExtGState {
275 fn default() -> Self {
276 Self::new()
277 }
278}
279
280impl ExtGState {
281 pub fn new() -> Self {
283 Self {
284 line_width: None,
285 line_cap: None,
286 line_join: None,
287 miter_limit: None,
288 dash_pattern: None,
289 rendering_intent: None,
290 overprint_stroke: None,
291 overprint_fill: None,
292 overprint_mode: None,
293 font: None,
294 black_generation: None,
295 black_generation_2: None,
296 undercolor_removal: None,
297 undercolor_removal_2: None,
298 transfer_function: None,
299 transfer_function_2: None,
300 halftone: None,
301 flatness: None,
302 smoothness: None,
303 stroke_adjustment: None,
304 blend_mode: None,
305 soft_mask: None,
306 alpha_stroke: None,
307 alpha_fill: None,
308 alpha_is_shape: None,
309 text_knockout: None,
310 use_black_point_compensation: None,
311 }
312 }
313
314 pub fn with_line_width(mut self, width: f64) -> Self {
317 self.line_width = Some(width.max(0.0));
318 self
319 }
320
321 pub fn with_line_cap(mut self, cap: LineCap) -> Self {
323 self.line_cap = Some(cap);
324 self
325 }
326
327 pub fn with_line_join(mut self, join: LineJoin) -> Self {
329 self.line_join = Some(join);
330 self
331 }
332
333 pub fn with_miter_limit(mut self, limit: f64) -> Self {
335 self.miter_limit = Some(limit.max(1.0));
336 self
337 }
338
339 pub fn with_dash_pattern(mut self, pattern: LineDashPattern) -> Self {
341 self.dash_pattern = Some(pattern);
342 self
343 }
344
345 pub fn with_rendering_intent(mut self, intent: RenderingIntent) -> Self {
348 self.rendering_intent = Some(intent);
349 self
350 }
351
352 pub fn with_overprint_stroke(mut self, overprint: bool) -> Self {
355 self.overprint_stroke = Some(overprint);
356 self
357 }
358
359 pub fn with_overprint_fill(mut self, overprint: bool) -> Self {
361 self.overprint_fill = Some(overprint);
362 self
363 }
364
365 pub fn with_overprint_mode(mut self, mode: u8) -> Self {
367 self.overprint_mode = Some(mode);
368 self
369 }
370
371 pub fn with_font(mut self, font: Font, size: f64) -> Self {
374 self.font = Some(ExtGStateFont::new(font, size.max(0.0)));
375 self
376 }
377
378 pub fn with_flatness(mut self, flatness: f64) -> Self {
381 self.flatness = Some(flatness.clamp(0.0, 100.0));
382 self
383 }
384
385 pub fn with_smoothness(mut self, smoothness: f64) -> Self {
387 self.smoothness = Some(smoothness.clamp(0.0, 1.0));
388 self
389 }
390
391 pub fn with_stroke_adjustment(mut self, adjustment: bool) -> Self {
393 self.stroke_adjustment = Some(adjustment);
394 self
395 }
396
397 pub fn with_blend_mode(mut self, mode: BlendMode) -> Self {
400 self.blend_mode = Some(mode);
401 self
402 }
403
404 pub fn with_alpha_stroke(mut self, alpha: f64) -> Self {
406 self.alpha_stroke = Some(alpha.clamp(0.0, 1.0));
407 self
408 }
409
410 pub fn with_alpha_fill(mut self, alpha: f64) -> Self {
412 self.alpha_fill = Some(alpha.clamp(0.0, 1.0));
413 self
414 }
415
416 pub fn with_alpha(mut self, alpha: f64) -> Self {
418 let clamped = alpha.clamp(0.0, 1.0);
419 self.alpha_stroke = Some(clamped);
420 self.alpha_fill = Some(clamped);
421 self
422 }
423
424 pub fn with_alpha_is_shape(mut self, is_shape: bool) -> Self {
426 self.alpha_is_shape = Some(is_shape);
427 self
428 }
429
430 pub fn with_text_knockout(mut self, knockout: bool) -> Self {
432 self.text_knockout = Some(knockout);
433 self
434 }
435
436 pub fn with_black_point_compensation(mut self, use_compensation: bool) -> Self {
438 self.use_black_point_compensation = Some(use_compensation);
439 self
440 }
441
442 pub fn uses_transparency(&self) -> bool {
444 self.alpha_stroke.is_some_and(|a| a < 1.0)
445 || self.alpha_fill.is_some_and(|a| a < 1.0)
446 || self.blend_mode.is_some()
447 || self.soft_mask.is_some()
448 }
449
450 pub fn to_pdf_dictionary(&self) -> Result<String> {
452 let mut dict = String::from("<< /Type /ExtGState");
453
454 if let Some(width) = self.line_width {
456 write!(&mut dict, " /LW {width:.3}").map_err(|_| {
457 PdfError::InvalidStructure("Failed to write line width".to_string())
458 })?;
459 }
460
461 if let Some(cap) = self.line_cap {
462 write!(&mut dict, " /LC {}", cap as u8)
463 .map_err(|_| PdfError::InvalidStructure("Failed to write line cap".to_string()))?;
464 }
465
466 if let Some(join) = self.line_join {
467 write!(&mut dict, " /LJ {}", join as u8)
468 .map_err(|_| PdfError::InvalidStructure("Failed to write line join".to_string()))?;
469 }
470
471 if let Some(limit) = self.miter_limit {
472 write!(&mut dict, " /ML {limit:.3}").map_err(|_| {
473 PdfError::InvalidStructure("Failed to write miter limit".to_string())
474 })?;
475 }
476
477 if let Some(ref pattern) = self.dash_pattern {
478 write!(&mut dict, " /D {}", pattern.to_pdf_string()).map_err(|_| {
479 PdfError::InvalidStructure("Failed to write dash pattern".to_string())
480 })?;
481 }
482
483 if let Some(intent) = self.rendering_intent {
485 write!(&mut dict, " /RI /{}", intent.pdf_name()).map_err(|_| {
486 PdfError::InvalidStructure("Failed to write rendering intent".to_string())
487 })?;
488 }
489
490 if let Some(op) = self.overprint_stroke {
492 write!(&mut dict, " /OP {op}").map_err(|_| {
493 PdfError::InvalidStructure("Failed to write overprint stroke".to_string())
494 })?;
495 }
496
497 if let Some(op) = self.overprint_fill {
498 write!(&mut dict, " /op {op}").map_err(|_| {
499 PdfError::InvalidStructure("Failed to write overprint fill".to_string())
500 })?;
501 }
502
503 if let Some(mode) = self.overprint_mode {
504 write!(&mut dict, " /OPM {mode}").map_err(|_| {
505 PdfError::InvalidStructure("Failed to write overprint mode".to_string())
506 })?;
507 }
508
509 if let Some(ref font) = self.font {
511 write!(
512 &mut dict,
513 " /Font [/{} {:.3}]",
514 font.font.pdf_name(),
515 font.size
516 )
517 .map_err(|_| PdfError::InvalidStructure("Failed to write font".to_string()))?;
518 }
519
520 if let Some(flatness) = self.flatness {
522 write!(&mut dict, " /FL {flatness:.3}")
523 .map_err(|_| PdfError::InvalidStructure("Failed to write flatness".to_string()))?;
524 }
525
526 if let Some(smoothness) = self.smoothness {
527 write!(&mut dict, " /SM {smoothness:.3}").map_err(|_| {
528 PdfError::InvalidStructure("Failed to write smoothness".to_string())
529 })?;
530 }
531
532 if let Some(sa) = self.stroke_adjustment {
534 write!(&mut dict, " /SA {sa}").map_err(|_| {
535 PdfError::InvalidStructure("Failed to write stroke adjustment".to_string())
536 })?;
537 }
538
539 if let Some(ref mode) = self.blend_mode {
541 write!(&mut dict, " /BM /{}", mode.pdf_name()).map_err(|_| {
542 PdfError::InvalidStructure("Failed to write blend mode".to_string())
543 })?;
544 }
545
546 if let Some(alpha) = self.alpha_stroke {
547 write!(&mut dict, " /CA {alpha:.3}").map_err(|_| {
548 PdfError::InvalidStructure("Failed to write stroke alpha".to_string())
549 })?;
550 }
551
552 if let Some(alpha) = self.alpha_fill {
553 write!(&mut dict, " /ca {alpha:.3}").map_err(|_| {
554 PdfError::InvalidStructure("Failed to write fill alpha".to_string())
555 })?;
556 }
557
558 if let Some(ais) = self.alpha_is_shape {
559 write!(&mut dict, " /AIS {ais}").map_err(|_| {
560 PdfError::InvalidStructure("Failed to write alpha is shape".to_string())
561 })?;
562 }
563
564 if let Some(tk) = self.text_knockout {
565 write!(&mut dict, " /TK {tk}").map_err(|_| {
566 PdfError::InvalidStructure("Failed to write text knockout".to_string())
567 })?;
568 }
569
570 if let Some(use_comp) = self.use_black_point_compensation {
572 write!(&mut dict, " /UseBlackPtComp {use_comp}").map_err(|_| {
573 PdfError::InvalidStructure("Failed to write black point compensation".to_string())
574 })?;
575 }
576
577 dict.push_str(" >>");
578 Ok(dict)
579 }
580
581 pub fn is_empty(&self) -> bool {
583 self.line_width.is_none()
584 && self.line_cap.is_none()
585 && self.line_join.is_none()
586 && self.miter_limit.is_none()
587 && self.dash_pattern.is_none()
588 && self.rendering_intent.is_none()
589 && self.overprint_stroke.is_none()
590 && self.overprint_fill.is_none()
591 && self.overprint_mode.is_none()
592 && self.font.is_none()
593 && self.flatness.is_none()
594 && self.smoothness.is_none()
595 && self.stroke_adjustment.is_none()
596 && self.blend_mode.is_none()
597 && self.soft_mask.is_none()
598 && self.alpha_stroke.is_none()
599 && self.alpha_fill.is_none()
600 && self.alpha_is_shape.is_none()
601 && self.text_knockout.is_none()
602 && self.use_black_point_compensation.is_none()
603 }
604}
605
606#[derive(Debug, Clone)]
608pub struct ExtGStateManager {
609 states: HashMap<String, ExtGState>,
610 next_id: usize,
611}
612
613impl Default for ExtGStateManager {
614 fn default() -> Self {
615 Self::new()
616 }
617}
618
619impl ExtGStateManager {
620 pub fn new() -> Self {
622 Self {
623 states: HashMap::new(),
624 next_id: 1,
625 }
626 }
627
628 pub fn add_state(&mut self, state: ExtGState) -> Result<String> {
630 if state.is_empty() {
631 return Err(PdfError::InvalidStructure(
632 "ExtGState cannot be empty".to_string(),
633 ));
634 }
635
636 let name = format!("GS{}", self.next_id);
637 self.states.insert(name.clone(), state);
638 self.next_id += 1;
639 Ok(name)
640 }
641
642 pub fn get_state(&self, name: &str) -> Option<&ExtGState> {
644 self.states.get(name)
645 }
646
647 pub fn states(&self) -> &HashMap<String, ExtGState> {
649 &self.states
650 }
651
652 pub fn to_resource_dictionary(&self) -> Result<String> {
654 if self.states.is_empty() {
655 return Ok(String::new());
656 }
657
658 let mut dict = String::from("/ExtGState <<");
659
660 for (name, state) in &self.states {
661 let state_dict = state.to_pdf_dictionary()?;
662 write!(&mut dict, " /{name} {state_dict}").map_err(|_| {
663 PdfError::InvalidStructure("Failed to write ExtGState resource".to_string())
664 })?;
665 }
666
667 dict.push_str(" >>");
668 Ok(dict)
669 }
670
671 pub fn clear(&mut self) {
673 self.states.clear();
674 self.next_id = 1;
675 }
676
677 pub fn count(&self) -> usize {
679 self.states.len()
680 }
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686
687 #[test]
688 fn test_rendering_intent_pdf_names() {
689 assert_eq!(
690 RenderingIntent::AbsoluteColorimetric.pdf_name(),
691 "AbsoluteColorimetric"
692 );
693 assert_eq!(
694 RenderingIntent::RelativeColorimetric.pdf_name(),
695 "RelativeColorimetric"
696 );
697 assert_eq!(RenderingIntent::Saturation.pdf_name(), "Saturation");
698 assert_eq!(RenderingIntent::Perceptual.pdf_name(), "Perceptual");
699 }
700
701 #[test]
702 fn test_blend_mode_pdf_names() {
703 assert_eq!(BlendMode::Normal.pdf_name(), "Normal");
704 assert_eq!(BlendMode::Multiply.pdf_name(), "Multiply");
705 assert_eq!(BlendMode::Screen.pdf_name(), "Screen");
706 assert_eq!(BlendMode::Overlay.pdf_name(), "Overlay");
707 }
708
709 #[test]
710 fn test_line_dash_pattern_creation() {
711 let solid = LineDashPattern::solid();
712 assert!(solid.array.is_empty());
713 assert_eq!(solid.phase, 0.0);
714
715 let dashed = LineDashPattern::dashed(5.0, 3.0);
716 assert_eq!(dashed.array, vec![5.0, 3.0]);
717 assert_eq!(dashed.phase, 0.0);
718
719 let dotted = LineDashPattern::dotted(1.0, 2.0);
720 assert_eq!(dotted.array, vec![1.0, 2.0]);
721 }
722
723 #[test]
724 fn test_line_dash_pattern_pdf_string() {
725 let solid = LineDashPattern::solid();
726 assert_eq!(solid.to_pdf_string(), "[] 0");
727
728 let dashed = LineDashPattern::dashed(5.0, 3.0);
729 assert_eq!(dashed.to_pdf_string(), "[5.00 3.00] 0.00");
730
731 let custom = LineDashPattern::new(vec![10.0, 5.0, 2.0, 5.0], 2.5);
732 assert_eq!(custom.to_pdf_string(), "[10.00 5.00 2.00 5.00] 2.50");
733 }
734
735 #[test]
736 fn test_extgstate_font() {
737 let font = ExtGStateFont::new(Font::Helvetica, 12.0);
738 assert_eq!(font.font, Font::Helvetica);
739 assert_eq!(font.size, 12.0);
740 }
741
742 #[test]
743 fn test_extgstate_creation() {
744 let state = ExtGState::new();
745 assert!(state.is_empty());
746 assert!(!state.uses_transparency());
747 }
748
749 #[test]
750 fn test_extgstate_line_parameters() {
751 let state = ExtGState::new()
752 .with_line_width(2.5)
753 .with_line_cap(LineCap::Round)
754 .with_line_join(LineJoin::Bevel)
755 .with_miter_limit(4.0);
756
757 assert_eq!(state.line_width, Some(2.5));
758 assert_eq!(state.line_cap, Some(LineCap::Round));
759 assert_eq!(state.line_join, Some(LineJoin::Bevel));
760 assert_eq!(state.miter_limit, Some(4.0));
761 assert!(!state.is_empty());
762 }
763
764 #[test]
765 fn test_extgstate_transparency() {
766 let state = ExtGState::new()
767 .with_alpha_stroke(0.8)
768 .with_alpha_fill(0.6)
769 .with_blend_mode(BlendMode::Multiply);
770
771 assert_eq!(state.alpha_stroke, Some(0.8));
772 assert_eq!(state.alpha_fill, Some(0.6));
773 assert_eq!(state.blend_mode, Some(BlendMode::Multiply));
774 assert!(state.uses_transparency());
775 }
776
777 #[test]
778 fn test_extgstate_alpha_clamping() {
779 let state = ExtGState::new()
780 .with_alpha_stroke(1.5) .with_alpha_fill(-0.1); assert_eq!(state.alpha_stroke, Some(1.0));
784 assert_eq!(state.alpha_fill, Some(0.0));
785 }
786
787 #[test]
788 fn test_extgstate_combined_alpha() {
789 let state = ExtGState::new().with_alpha(0.5);
790
791 assert_eq!(state.alpha_stroke, Some(0.5));
792 assert_eq!(state.alpha_fill, Some(0.5));
793 }
794
795 #[test]
796 fn test_extgstate_rendering_intent() {
797 let state = ExtGState::new().with_rendering_intent(RenderingIntent::Perceptual);
798
799 assert_eq!(state.rendering_intent, Some(RenderingIntent::Perceptual));
800 }
801
802 #[test]
803 fn test_extgstate_overprint() {
804 let state = ExtGState::new()
805 .with_overprint_stroke(true)
806 .with_overprint_fill(false)
807 .with_overprint_mode(1);
808
809 assert_eq!(state.overprint_stroke, Some(true));
810 assert_eq!(state.overprint_fill, Some(false));
811 assert_eq!(state.overprint_mode, Some(1));
812 }
813
814 #[test]
815 fn test_extgstate_font_setting() {
816 let state = ExtGState::new().with_font(Font::HelveticaBold, 14.0);
817
818 assert!(state.font.is_some());
819 let font = state.font.unwrap();
820 assert_eq!(font.font, Font::HelveticaBold);
821 assert_eq!(font.size, 14.0);
822 }
823
824 #[test]
825 fn test_extgstate_tolerance_parameters() {
826 let state = ExtGState::new()
827 .with_flatness(1.5)
828 .with_smoothness(0.8)
829 .with_stroke_adjustment(true);
830
831 assert_eq!(state.flatness, Some(1.5));
832 assert_eq!(state.smoothness, Some(0.8));
833 assert_eq!(state.stroke_adjustment, Some(true));
834 }
835
836 #[test]
837 fn test_extgstate_pdf_dictionary_generation() {
838 let state = ExtGState::new()
839 .with_line_width(2.0)
840 .with_line_cap(LineCap::Round)
841 .with_alpha(0.5)
842 .with_blend_mode(BlendMode::Multiply);
843
844 let dict = state.to_pdf_dictionary().unwrap();
845 assert!(dict.contains("/Type /ExtGState"));
846 assert!(dict.contains("/LW 2.000"));
847 assert!(dict.contains("/LC 1"));
848 assert!(dict.contains("/CA 0.500"));
849 assert!(dict.contains("/ca 0.500"));
850 assert!(dict.contains("/BM /Multiply"));
851 }
852
853 #[test]
854 fn test_extgstate_manager_creation() {
855 let manager = ExtGStateManager::new();
856 assert_eq!(manager.count(), 0);
857 assert!(manager.states().is_empty());
858 }
859
860 #[test]
861 fn test_extgstate_manager_add_state() {
862 let mut manager = ExtGStateManager::new();
863 let state = ExtGState::new().with_line_width(2.0);
864
865 let name = manager.add_state(state).unwrap();
866 assert_eq!(name, "GS1");
867 assert_eq!(manager.count(), 1);
868
869 let retrieved = manager.get_state(&name).unwrap();
870 assert_eq!(retrieved.line_width, Some(2.0));
871 }
872
873 #[test]
874 fn test_extgstate_manager_empty_state_rejection() {
875 let mut manager = ExtGStateManager::new();
876 let empty_state = ExtGState::new();
877
878 let result = manager.add_state(empty_state);
879 assert!(result.is_err());
880 assert_eq!(manager.count(), 0);
881 }
882
883 #[test]
884 fn test_extgstate_manager_multiple_states() {
885 let mut manager = ExtGStateManager::new();
886
887 let state1 = ExtGState::new().with_line_width(1.0);
888 let state2 = ExtGState::new().with_alpha(0.5);
889
890 let name1 = manager.add_state(state1).unwrap();
891 let name2 = manager.add_state(state2).unwrap();
892
893 assert_eq!(name1, "GS1");
894 assert_eq!(name2, "GS2");
895 assert_eq!(manager.count(), 2);
896 }
897
898 #[test]
899 fn test_extgstate_manager_resource_dictionary() {
900 let mut manager = ExtGStateManager::new();
901
902 let state = ExtGState::new().with_line_width(2.0);
903 manager.add_state(state).unwrap();
904
905 let dict = manager.to_resource_dictionary().unwrap();
906 assert!(dict.contains("/ExtGState"));
907 assert!(dict.contains("/GS1"));
908 assert!(dict.contains("/LW 2.000"));
909 }
910
911 #[test]
912 fn test_extgstate_manager_clear() {
913 let mut manager = ExtGStateManager::new();
914
915 let state = ExtGState::new().with_line_width(1.0);
916 manager.add_state(state).unwrap();
917 assert_eq!(manager.count(), 1);
918
919 manager.clear();
920 assert_eq!(manager.count(), 0);
921 assert!(manager.states().is_empty());
922 }
923
924 #[test]
925 fn test_extgstate_value_validation() {
926 let state = ExtGState::new().with_line_width(-1.0);
928 assert_eq!(state.line_width, Some(0.0));
929
930 let state = ExtGState::new().with_miter_limit(0.5);
932 assert_eq!(state.miter_limit, Some(1.0));
933
934 let state = ExtGState::new().with_flatness(150.0);
936 assert_eq!(state.flatness, Some(100.0));
937
938 let state = ExtGState::new().with_smoothness(1.5);
940 assert_eq!(state.smoothness, Some(1.0));
941
942 let state = ExtGState::new().with_font(Font::Helvetica, -5.0);
944 assert_eq!(state.font.unwrap().size, 0.0);
945 }
946
947 #[test]
948 fn test_line_dash_patterns() {
949 let state = ExtGState::new().with_dash_pattern(LineDashPattern::dashed(10.0, 5.0));
950
951 let dict = state.to_pdf_dictionary().unwrap();
952 assert!(dict.contains("/D [10.00 5.00] 0.00"));
953 }
954
955 #[test]
956 fn test_complex_extgstate() {
957 let dash_pattern = LineDashPattern::new(vec![3.0, 2.0, 1.0, 2.0], 1.0);
958
959 let state = ExtGState::new()
960 .with_line_width(1.5)
961 .with_line_cap(LineCap::Square)
962 .with_line_join(LineJoin::Round)
963 .with_miter_limit(10.0)
964 .with_dash_pattern(dash_pattern)
965 .with_rendering_intent(RenderingIntent::Saturation)
966 .with_overprint_stroke(true)
967 .with_overprint_fill(false)
968 .with_font(Font::TimesBold, 18.0)
969 .with_flatness(0.5)
970 .with_smoothness(0.1)
971 .with_stroke_adjustment(false)
972 .with_blend_mode(BlendMode::SoftLight)
973 .with_alpha_stroke(0.8)
974 .with_alpha_fill(0.6)
975 .with_alpha_is_shape(true)
976 .with_text_knockout(false);
977
978 assert!(!state.is_empty());
979 assert!(state.uses_transparency());
980
981 let dict = state.to_pdf_dictionary().unwrap();
982 assert!(dict.contains("/Type /ExtGState"));
983 assert!(dict.contains("/LW 1.500"));
984 assert!(dict.contains("/LC 2"));
985 assert!(dict.contains("/LJ 1"));
986 assert!(dict.contains("/ML 10.000"));
987 assert!(dict.contains("/D [3.00 2.00 1.00 2.00] 1.00"));
988 assert!(dict.contains("/RI /Saturation"));
989 assert!(dict.contains("/OP true"));
990 assert!(dict.contains("/op false"));
991 assert!(dict.contains("/Font [/Times-Bold 18.000]"));
992 assert!(dict.contains("/FL 0.500"));
993 assert!(dict.contains("/SM 0.100"));
994 assert!(dict.contains("/SA false"));
995 assert!(dict.contains("/BM /SoftLight"));
996 assert!(dict.contains("/CA 0.800"));
997 assert!(dict.contains("/ca 0.600"));
998 assert!(dict.contains("/AIS true"));
999 assert!(dict.contains("/TK false"));
1000 }
1001}