1use super::soft_mask::SoftMask;
7use crate::error::{PdfError, Result};
8use crate::graphics::{LineCap, LineJoin};
9use crate::text::Font;
10use std::collections::HashMap;
11use std::fmt::Write;
12
13#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum RenderingIntent {
16 AbsoluteColorimetric,
18 RelativeColorimetric,
20 Saturation,
22 Perceptual,
24}
25
26impl RenderingIntent {
27 pub fn pdf_name(&self) -> &'static str {
29 match self {
30 RenderingIntent::AbsoluteColorimetric => "AbsoluteColorimetric",
31 RenderingIntent::RelativeColorimetric => "RelativeColorimetric",
32 RenderingIntent::Saturation => "Saturation",
33 RenderingIntent::Perceptual => "Perceptual",
34 }
35 }
36}
37
38#[derive(Debug, Clone, PartialEq)]
40pub enum BlendMode {
41 Normal,
43 Multiply,
45 Screen,
47 Overlay,
49 SoftLight,
51 HardLight,
53 ColorDodge,
55 ColorBurn,
57 Darken,
59 Lighten,
61 Difference,
63 Exclusion,
65 Hue,
67 Saturation,
69 Color,
71 Luminosity,
73}
74
75impl BlendMode {
76 pub fn pdf_name(&self) -> &'static str {
78 match self {
79 BlendMode::Normal => "Normal",
80 BlendMode::Multiply => "Multiply",
81 BlendMode::Screen => "Screen",
82 BlendMode::Overlay => "Overlay",
83 BlendMode::SoftLight => "SoftLight",
84 BlendMode::HardLight => "HardLight",
85 BlendMode::ColorDodge => "ColorDodge",
86 BlendMode::ColorBurn => "ColorBurn",
87 BlendMode::Darken => "Darken",
88 BlendMode::Lighten => "Lighten",
89 BlendMode::Difference => "Difference",
90 BlendMode::Exclusion => "Exclusion",
91 BlendMode::Hue => "Hue",
92 BlendMode::Saturation => "Saturation",
93 BlendMode::Color => "Color",
94 BlendMode::Luminosity => "Luminosity",
95 }
96 }
97}
98
99#[derive(Debug, Clone, PartialEq)]
101pub struct LineDashPattern {
102 pub array: Vec<f64>,
104 pub phase: f64,
106}
107
108impl LineDashPattern {
109 pub fn new(array: Vec<f64>, phase: f64) -> Self {
111 Self { array, phase }
112 }
113
114 pub fn solid() -> Self {
116 Self {
117 array: Vec::new(),
118 phase: 0.0,
119 }
120 }
121
122 pub fn dashed(dash_length: f64, gap_length: f64) -> Self {
124 Self {
125 array: vec![dash_length, gap_length],
126 phase: 0.0,
127 }
128 }
129
130 pub fn dotted(dot_size: f64, gap_size: f64) -> Self {
132 Self {
133 array: vec![dot_size, gap_size],
134 phase: 0.0,
135 }
136 }
137
138 pub fn to_pdf_string(&self) -> String {
140 if self.array.is_empty() {
141 "[] 0".to_string()
142 } else {
143 let array_str = self
144 .array
145 .iter()
146 .map(|&x| format!("{x:.2}"))
147 .collect::<Vec<_>>()
148 .join(" ");
149 format!("[{array_str}] {:.2}", self.phase)
150 }
151 }
152}
153
154#[derive(Debug, Clone, PartialEq)]
156pub struct ExtGStateFont {
157 pub font: Font,
159 pub size: f64,
161}
162
163impl ExtGStateFont {
164 pub fn new(font: Font, size: f64) -> Self {
166 Self { font, size }
167 }
168}
169
170#[derive(Debug, Clone, PartialEq)]
172#[allow(clippy::large_enum_variant)]
173pub enum TransferFunction {
174 Identity,
176 Single(TransferFunctionData),
178 Separate {
180 c_or_r: TransferFunctionData,
182 m_or_g: TransferFunctionData,
184 y_or_b: TransferFunctionData,
186 k: Option<TransferFunctionData>,
188 },
189}
190
191#[derive(Debug, Clone, PartialEq)]
193pub struct TransferFunctionData {
194 pub function_type: u32,
196 pub domain: Vec<f64>,
198 pub range: Vec<f64>,
200 pub params: TransferFunctionParams,
202}
203
204#[derive(Debug, Clone, PartialEq)]
206pub enum TransferFunctionParams {
207 Sampled {
209 samples: Vec<f64>,
211 size: Vec<u32>,
213 bits_per_sample: u32,
215 },
216 Exponential {
218 c0: Vec<f64>,
220 c1: Vec<f64>,
222 n: f64,
224 },
225 Stitching {
227 functions: Vec<TransferFunctionData>,
229 bounds: Vec<f64>,
231 encode: Vec<f64>,
233 },
234 PostScript {
236 code: String,
238 },
239}
240
241impl TransferFunction {
242 pub fn identity() -> Self {
244 TransferFunction::Identity
245 }
246
247 pub fn gamma(gamma_value: f64) -> Self {
249 TransferFunction::Single(TransferFunctionData {
250 function_type: 2,
251 domain: vec![0.0, 1.0],
252 range: vec![0.0, 1.0],
253 params: TransferFunctionParams::Exponential {
254 c0: vec![0.0],
255 c1: vec![1.0],
256 n: gamma_value,
257 },
258 })
259 }
260
261 pub fn linear(slope: f64, intercept: f64) -> Self {
263 TransferFunction::Single(TransferFunctionData {
264 function_type: 2,
265 domain: vec![0.0, 1.0],
266 range: vec![0.0, 1.0],
267 params: TransferFunctionParams::Exponential {
268 c0: vec![intercept],
269 c1: vec![slope + intercept],
270 n: 1.0,
271 },
272 })
273 }
274
275 pub fn to_pdf_string(&self) -> String {
277 match self {
278 TransferFunction::Identity => "/Identity".to_string(),
279 TransferFunction::Single(data) => data.to_pdf_string(),
280 TransferFunction::Separate {
281 c_or_r,
282 m_or_g,
283 y_or_b,
284 k,
285 } => {
286 let mut result = String::from("[");
287 result.push_str(&c_or_r.to_pdf_string());
288 result.push(' ');
289 result.push_str(&m_or_g.to_pdf_string());
290 result.push(' ');
291 result.push_str(&y_or_b.to_pdf_string());
292 if let Some(k_func) = k {
293 result.push(' ');
294 result.push_str(&k_func.to_pdf_string());
295 }
296 result.push(']');
297 result
298 }
299 }
300 }
301}
302
303impl TransferFunctionData {
304 pub fn to_pdf_string(&self) -> String {
306 let mut dict = String::from("<<");
307
308 dict.push_str(&format!(" /FunctionType {}", self.function_type));
310
311 dict.push_str(" /Domain [");
313 for (i, val) in self.domain.iter().enumerate() {
314 if i > 0 {
315 dict.push(' ');
316 }
317 dict.push_str(&format!("{:.3}", val));
318 }
319 dict.push(']');
320
321 dict.push_str(" /Range [");
323 for (i, val) in self.range.iter().enumerate() {
324 if i > 0 {
325 dict.push(' ');
326 }
327 dict.push_str(&format!("{:.3}", val));
328 }
329 dict.push(']');
330
331 match &self.params {
333 TransferFunctionParams::Exponential { c0, c1, n } => {
334 dict.push_str(" /C0 [");
336 for (i, val) in c0.iter().enumerate() {
337 if i > 0 {
338 dict.push(' ');
339 }
340 dict.push_str(&format!("{:.3}", val));
341 }
342 dict.push_str("] /C1 [");
343 for (i, val) in c1.iter().enumerate() {
344 if i > 0 {
345 dict.push(' ');
346 }
347 dict.push_str(&format!("{:.3}", val));
348 }
349 dict.push_str(&format!("] /N {:.3}", n));
350 }
351 TransferFunctionParams::Sampled {
352 size,
353 bits_per_sample,
354 samples,
355 ..
356 } => {
357 dict.push_str(" /Size [");
359 for (i, val) in size.iter().enumerate() {
360 if i > 0 {
361 dict.push(' ');
362 }
363 dict.push_str(&format!("{}", val));
364 }
365 dict.push_str(&format!("] /BitsPerSample {}", bits_per_sample));
366 dict.push_str(" /Length ");
368 dict.push_str(&format!("{}", samples.len()));
369 }
370 TransferFunctionParams::Stitching {
371 bounds,
372 encode,
373 functions,
374 } => {
375 dict.push_str(" /Bounds [");
377 for (i, val) in bounds.iter().enumerate() {
378 if i > 0 {
379 dict.push(' ');
380 }
381 dict.push_str(&format!("{:.3}", val));
382 }
383 dict.push_str("] /Encode [");
384 for (i, val) in encode.iter().enumerate() {
385 if i > 0 {
386 dict.push(' ');
387 }
388 dict.push_str(&format!("{:.3}", val));
389 }
390 dict.push_str("] /Functions [");
391 for (i, func) in functions.iter().enumerate() {
392 if i > 0 {
393 dict.push(' ');
394 }
395 dict.push_str(&func.to_pdf_string());
396 }
397 dict.push(']');
398 }
399 TransferFunctionParams::PostScript { code } => {
400 dict.push_str(&format!(
402 " /Length {} stream\n{}\nendstream",
403 code.len(),
404 code
405 ));
406 }
407 }
408
409 dict.push_str(" >>");
410 dict
411 }
412}
413
414#[derive(Debug, Clone, PartialEq)]
416pub enum Halftone {
417 Default,
419 Type1 {
421 frequency: f64,
423 angle: f64,
425 spot_function: SpotFunction,
427 },
428 Type5 {
430 colorants: HashMap<String, HalftoneColorant>,
432 default: Box<Halftone>,
434 },
435 Type6 {
437 width: u32,
439 height: u32,
441 thresholds: Vec<u8>,
443 },
444 Type10 {
446 frequency: f64,
448 },
449 Type16 {
451 width: u32,
453 height: u32,
455 thresholds: Vec<Vec<u8>>,
457 },
458}
459
460#[derive(Debug, Clone, PartialEq)]
462pub enum SpotFunction {
463 SimpleDot,
465 InvertedSimpleDot,
467 Round,
469 InvertedRound,
471 Ellipse,
473 Square,
475 Cross,
477 Diamond,
479 Line,
481 Custom(String),
483}
484
485impl SpotFunction {
486 pub fn pdf_name(&self) -> String {
488 match self {
489 SpotFunction::SimpleDot => "SimpleDot".to_string(),
490 SpotFunction::InvertedSimpleDot => "InvertedSimpleDot".to_string(),
491 SpotFunction::Round => "Round".to_string(),
492 SpotFunction::InvertedRound => "InvertedRound".to_string(),
493 SpotFunction::Ellipse => "Ellipse".to_string(),
494 SpotFunction::Square => "Square".to_string(),
495 SpotFunction::Cross => "Cross".to_string(),
496 SpotFunction::Diamond => "Diamond".to_string(),
497 SpotFunction::Line => "Line".to_string(),
498 SpotFunction::Custom(name) => name.clone(),
499 }
500 }
501}
502
503#[derive(Debug, Clone, PartialEq)]
505pub struct HalftoneColorant {
506 pub frequency: f64,
508 pub angle: f64,
510 pub spot_function: SpotFunction,
512}
513
514#[derive(Debug, Clone)]
516pub struct ExtGState {
517 pub line_width: Option<f64>,
520 pub line_cap: Option<LineCap>,
522 pub line_join: Option<LineJoin>,
524 pub miter_limit: Option<f64>,
526 pub dash_pattern: Option<LineDashPattern>,
528
529 pub rendering_intent: Option<RenderingIntent>,
532
533 pub overprint_stroke: Option<bool>,
536 pub overprint_fill: Option<bool>,
538 pub overprint_mode: Option<u8>,
540
541 pub font: Option<ExtGStateFont>,
544
545 pub black_generation: Option<TransferFunction>,
548 pub black_generation_2: Option<TransferFunction>,
550 pub undercolor_removal: Option<TransferFunction>,
552 pub undercolor_removal_2: Option<TransferFunction>,
554 pub transfer_function: Option<TransferFunction>,
556 pub transfer_function_2: Option<TransferFunction>,
558
559 pub halftone: Option<Halftone>,
562
563 pub flatness: Option<f64>,
566 pub smoothness: Option<f64>,
568
569 pub stroke_adjustment: Option<bool>,
572
573 pub blend_mode: Option<BlendMode>,
576 pub soft_mask: Option<SoftMask>,
578 pub alpha_stroke: Option<f64>,
580 pub alpha_fill: Option<f64>,
582 pub alpha_is_shape: Option<bool>,
584 pub text_knockout: Option<bool>,
586
587 pub use_black_point_compensation: Option<bool>,
590}
591
592impl Default for ExtGState {
593 fn default() -> Self {
594 Self::new()
595 }
596}
597
598impl ExtGState {
599 pub fn new() -> Self {
601 Self {
602 line_width: None,
603 line_cap: None,
604 line_join: None,
605 miter_limit: None,
606 dash_pattern: None,
607 rendering_intent: None,
608 overprint_stroke: None,
609 overprint_fill: None,
610 overprint_mode: None,
611 font: None,
612 black_generation: None,
613 black_generation_2: None,
614 undercolor_removal: None,
615 undercolor_removal_2: None,
616 transfer_function: None,
617 transfer_function_2: None,
618 halftone: None,
619 flatness: None,
620 smoothness: None,
621 stroke_adjustment: None,
622 blend_mode: None,
623 soft_mask: None,
624 alpha_stroke: None,
625 alpha_fill: None,
626 alpha_is_shape: None,
627 text_knockout: None,
628 use_black_point_compensation: None,
629 }
630 }
631
632 pub fn with_line_width(mut self, width: f64) -> Self {
635 self.line_width = Some(width.max(0.0));
636 self
637 }
638
639 pub fn with_line_cap(mut self, cap: LineCap) -> Self {
641 self.line_cap = Some(cap);
642 self
643 }
644
645 pub fn with_line_join(mut self, join: LineJoin) -> Self {
647 self.line_join = Some(join);
648 self
649 }
650
651 pub fn with_miter_limit(mut self, limit: f64) -> Self {
653 self.miter_limit = Some(limit.max(1.0));
654 self
655 }
656
657 pub fn with_dash_pattern(mut self, pattern: LineDashPattern) -> Self {
659 self.dash_pattern = Some(pattern);
660 self
661 }
662
663 pub fn with_rendering_intent(mut self, intent: RenderingIntent) -> Self {
666 self.rendering_intent = Some(intent);
667 self
668 }
669
670 pub fn with_overprint_stroke(mut self, overprint: bool) -> Self {
673 self.overprint_stroke = Some(overprint);
674 self
675 }
676
677 pub fn with_overprint_fill(mut self, overprint: bool) -> Self {
679 self.overprint_fill = Some(overprint);
680 self
681 }
682
683 pub fn with_overprint_mode(mut self, mode: u8) -> Self {
685 self.overprint_mode = Some(mode);
686 self
687 }
688
689 pub fn with_font(mut self, font: Font, size: f64) -> Self {
692 self.font = Some(ExtGStateFont::new(font, size.max(0.0)));
693 self
694 }
695
696 pub fn with_flatness(mut self, flatness: f64) -> Self {
699 self.flatness = Some(flatness.clamp(0.0, 100.0));
700 self
701 }
702
703 pub fn with_smoothness(mut self, smoothness: f64) -> Self {
705 self.smoothness = Some(smoothness.clamp(0.0, 1.0));
706 self
707 }
708
709 pub fn with_stroke_adjustment(mut self, adjustment: bool) -> Self {
711 self.stroke_adjustment = Some(adjustment);
712 self
713 }
714
715 pub fn with_blend_mode(mut self, mode: BlendMode) -> Self {
718 self.blend_mode = Some(mode);
719 self
720 }
721
722 pub fn with_alpha_stroke(mut self, alpha: f64) -> Self {
724 self.alpha_stroke = Some(alpha.clamp(0.0, 1.0));
725 self
726 }
727
728 pub fn with_alpha_fill(mut self, alpha: f64) -> Self {
730 self.alpha_fill = Some(alpha.clamp(0.0, 1.0));
731 self
732 }
733
734 pub fn with_alpha(mut self, alpha: f64) -> Self {
736 let clamped = alpha.clamp(0.0, 1.0);
737 self.alpha_stroke = Some(clamped);
738 self.alpha_fill = Some(clamped);
739 self
740 }
741
742 pub fn with_alpha_is_shape(mut self, is_shape: bool) -> Self {
744 self.alpha_is_shape = Some(is_shape);
745 self
746 }
747
748 pub fn with_text_knockout(mut self, knockout: bool) -> Self {
750 self.text_knockout = Some(knockout);
751 self
752 }
753
754 pub fn set_soft_mask(&mut self, mask: SoftMask) {
756 self.soft_mask = Some(mask);
757 }
758
759 pub fn set_soft_mask_name(&mut self, name: String) {
761 self.soft_mask = Some(SoftMask::luminosity(name));
762 }
763
764 pub fn set_soft_mask_none(&mut self) {
766 self.soft_mask = Some(SoftMask::none());
767 }
768
769 pub fn with_black_point_compensation(mut self, use_compensation: bool) -> Self {
771 self.use_black_point_compensation = Some(use_compensation);
772 self
773 }
774
775 pub fn with_transfer_function(mut self, func: TransferFunction) -> Self {
778 self.transfer_function = Some(func);
779 self
780 }
781
782 pub fn with_gamma_correction(mut self, gamma: f64) -> Self {
784 self.transfer_function = Some(TransferFunction::gamma(gamma));
785 self
786 }
787
788 pub fn with_linear_transfer(mut self, slope: f64, intercept: f64) -> Self {
790 self.transfer_function = Some(TransferFunction::linear(slope, intercept));
791 self
792 }
793
794 pub fn with_transfer_function_2(mut self, func: TransferFunction) -> Self {
796 self.transfer_function_2 = Some(func);
797 self
798 }
799
800 pub fn with_black_generation(mut self, func: TransferFunction) -> Self {
802 self.black_generation = Some(func);
803 self
804 }
805
806 pub fn with_undercolor_removal(mut self, func: TransferFunction) -> Self {
808 self.undercolor_removal = Some(func);
809 self
810 }
811
812 pub fn uses_transparency(&self) -> bool {
814 self.alpha_stroke.is_some_and(|a| a < 1.0)
815 || self.alpha_fill.is_some_and(|a| a < 1.0)
816 || self.blend_mode.is_some()
817 || self.soft_mask.is_some()
818 }
819
820 pub fn to_pdf_dictionary(&self) -> Result<String> {
822 let mut dict = String::from("<< /Type /ExtGState");
823
824 if let Some(width) = self.line_width {
826 write!(&mut dict, " /LW {width:.3}").map_err(|_| {
827 PdfError::InvalidStructure("Failed to write line width".to_string())
828 })?;
829 }
830
831 if let Some(cap) = self.line_cap {
832 write!(&mut dict, " /LC {}", cap as u8)
833 .map_err(|_| PdfError::InvalidStructure("Failed to write line cap".to_string()))?;
834 }
835
836 if let Some(join) = self.line_join {
837 write!(&mut dict, " /LJ {}", join as u8)
838 .map_err(|_| PdfError::InvalidStructure("Failed to write line join".to_string()))?;
839 }
840
841 if let Some(limit) = self.miter_limit {
842 write!(&mut dict, " /ML {limit:.3}").map_err(|_| {
843 PdfError::InvalidStructure("Failed to write miter limit".to_string())
844 })?;
845 }
846
847 if let Some(ref pattern) = self.dash_pattern {
848 write!(&mut dict, " /D {}", pattern.to_pdf_string()).map_err(|_| {
849 PdfError::InvalidStructure("Failed to write dash pattern".to_string())
850 })?;
851 }
852
853 if let Some(intent) = self.rendering_intent {
855 write!(&mut dict, " /RI /{}", intent.pdf_name()).map_err(|_| {
856 PdfError::InvalidStructure("Failed to write rendering intent".to_string())
857 })?;
858 }
859
860 if let Some(op) = self.overprint_stroke {
862 write!(&mut dict, " /OP {op}").map_err(|_| {
863 PdfError::InvalidStructure("Failed to write overprint stroke".to_string())
864 })?;
865 }
866
867 if let Some(op) = self.overprint_fill {
868 write!(&mut dict, " /op {op}").map_err(|_| {
869 PdfError::InvalidStructure("Failed to write overprint fill".to_string())
870 })?;
871 }
872
873 if let Some(mode) = self.overprint_mode {
874 write!(&mut dict, " /OPM {mode}").map_err(|_| {
875 PdfError::InvalidStructure("Failed to write overprint mode".to_string())
876 })?;
877 }
878
879 if let Some(ref font) = self.font {
881 write!(
882 &mut dict,
883 " /Font [/{} {:.3}]",
884 font.font.pdf_name(),
885 font.size
886 )
887 .map_err(|_| PdfError::InvalidStructure("Failed to write font".to_string()))?;
888 }
889
890 if let Some(flatness) = self.flatness {
892 write!(&mut dict, " /FL {flatness:.3}")
893 .map_err(|_| PdfError::InvalidStructure("Failed to write flatness".to_string()))?;
894 }
895
896 if let Some(smoothness) = self.smoothness {
897 write!(&mut dict, " /SM {smoothness:.3}").map_err(|_| {
898 PdfError::InvalidStructure("Failed to write smoothness".to_string())
899 })?;
900 }
901
902 if let Some(sa) = self.stroke_adjustment {
904 write!(&mut dict, " /SA {sa}").map_err(|_| {
905 PdfError::InvalidStructure("Failed to write stroke adjustment".to_string())
906 })?;
907 }
908
909 if let Some(ref mode) = self.blend_mode {
911 write!(&mut dict, " /BM /{}", mode.pdf_name()).map_err(|_| {
912 PdfError::InvalidStructure("Failed to write blend mode".to_string())
913 })?;
914 }
915
916 if let Some(ref mask) = self.soft_mask {
917 if mask.is_none() {
918 write!(&mut dict, " /SMask /None").map_err(|_| {
919 PdfError::InvalidStructure("Failed to write soft mask".to_string())
920 })?;
921 } else {
922 write!(&mut dict, " /SMask {}", mask.to_pdf_string()).map_err(|_| {
925 PdfError::InvalidStructure("Failed to write soft mask".to_string())
926 })?;
927 }
928 }
929
930 if let Some(alpha) = self.alpha_stroke {
931 write!(&mut dict, " /CA {alpha:.3}").map_err(|_| {
932 PdfError::InvalidStructure("Failed to write stroke alpha".to_string())
933 })?;
934 }
935
936 if let Some(alpha) = self.alpha_fill {
937 write!(&mut dict, " /ca {alpha:.3}").map_err(|_| {
938 PdfError::InvalidStructure("Failed to write fill alpha".to_string())
939 })?;
940 }
941
942 if let Some(ais) = self.alpha_is_shape {
943 write!(&mut dict, " /AIS {ais}").map_err(|_| {
944 PdfError::InvalidStructure("Failed to write alpha is shape".to_string())
945 })?;
946 }
947
948 if let Some(tk) = self.text_knockout {
949 write!(&mut dict, " /TK {tk}").map_err(|_| {
950 PdfError::InvalidStructure("Failed to write text knockout".to_string())
951 })?;
952 }
953
954 if let Some(ref tf) = self.transfer_function {
956 write!(&mut dict, " /TR {}", tf.to_pdf_string()).map_err(|_| {
957 PdfError::InvalidStructure("Failed to write transfer function".to_string())
958 })?;
959 }
960
961 if let Some(ref tf) = self.transfer_function_2 {
962 write!(&mut dict, " /TR2 {}", tf.to_pdf_string()).map_err(|_| {
963 PdfError::InvalidStructure("Failed to write transfer function 2".to_string())
964 })?;
965 }
966
967 if let Some(ref bg) = self.black_generation {
968 write!(&mut dict, " /BG {}", bg.to_pdf_string()).map_err(|_| {
969 PdfError::InvalidStructure("Failed to write black generation".to_string())
970 })?;
971 }
972
973 if let Some(ref bg) = self.black_generation_2 {
974 write!(&mut dict, " /BG2 {}", bg.to_pdf_string()).map_err(|_| {
975 PdfError::InvalidStructure("Failed to write black generation 2".to_string())
976 })?;
977 }
978
979 if let Some(ref ucr) = self.undercolor_removal {
980 write!(&mut dict, " /UCR {}", ucr.to_pdf_string()).map_err(|_| {
981 PdfError::InvalidStructure("Failed to write undercolor removal".to_string())
982 })?;
983 }
984
985 if let Some(ref ucr) = self.undercolor_removal_2 {
986 write!(&mut dict, " /UCR2 {}", ucr.to_pdf_string()).map_err(|_| {
987 PdfError::InvalidStructure("Failed to write undercolor removal 2".to_string())
988 })?;
989 }
990
991 if let Some(use_comp) = self.use_black_point_compensation {
993 write!(&mut dict, " /UseBlackPtComp {use_comp}").map_err(|_| {
994 PdfError::InvalidStructure("Failed to write black point compensation".to_string())
995 })?;
996 }
997
998 dict.push_str(" >>");
999 Ok(dict)
1000 }
1001
1002 pub fn is_empty(&self) -> bool {
1004 self.line_width.is_none()
1005 && self.line_cap.is_none()
1006 && self.line_join.is_none()
1007 && self.miter_limit.is_none()
1008 && self.dash_pattern.is_none()
1009 && self.rendering_intent.is_none()
1010 && self.overprint_stroke.is_none()
1011 && self.overprint_fill.is_none()
1012 && self.overprint_mode.is_none()
1013 && self.font.is_none()
1014 && self.flatness.is_none()
1015 && self.smoothness.is_none()
1016 && self.stroke_adjustment.is_none()
1017 && self.blend_mode.is_none()
1018 && self.soft_mask.is_none()
1019 && self.alpha_stroke.is_none()
1020 && self.alpha_fill.is_none()
1021 && self.alpha_is_shape.is_none()
1022 && self.text_knockout.is_none()
1023 && self.transfer_function.is_none()
1024 && self.transfer_function_2.is_none()
1025 && self.black_generation.is_none()
1026 && self.black_generation_2.is_none()
1027 && self.undercolor_removal.is_none()
1028 && self.undercolor_removal_2.is_none()
1029 && self.use_black_point_compensation.is_none()
1030 }
1031
1032 pub fn to_dict(&self) -> crate::objects::Dictionary {
1034 use crate::objects::{Dictionary, Object};
1035
1036 let mut dict = Dictionary::new();
1037 dict.set("Type", Object::Name("ExtGState".to_string()));
1038
1039 if let Some(width) = self.line_width {
1041 dict.set("LW", Object::Real(width));
1042 }
1043
1044 if let Some(cap) = self.line_cap {
1045 dict.set("LC", Object::Integer(cap as i64));
1046 }
1047
1048 if let Some(join) = self.line_join {
1049 dict.set("LJ", Object::Integer(join as i64));
1050 }
1051
1052 if let Some(limit) = self.miter_limit {
1053 dict.set("ML", Object::Real(limit));
1054 }
1055
1056 if let Some(mode) = &self.blend_mode {
1058 dict.set("BM", Object::Name(mode.pdf_name().to_string()));
1059 }
1060
1061 if let Some(alpha) = self.alpha_stroke {
1062 dict.set("CA", Object::Real(alpha));
1063 }
1064
1065 if let Some(alpha) = self.alpha_fill {
1066 dict.set("ca", Object::Real(alpha));
1067 }
1068
1069 if let Some(ais) = self.alpha_is_shape {
1070 dict.set("AIS", Object::Boolean(ais));
1071 }
1072
1073 if let Some(tk) = self.text_knockout {
1074 dict.set("TK", Object::Boolean(tk));
1075 }
1076
1077 if let Some(intent) = &self.rendering_intent {
1079 dict.set("RI", Object::Name(intent.pdf_name().to_string()));
1080 }
1081
1082 if let Some(op) = self.overprint_stroke {
1083 dict.set("OP", Object::Boolean(op));
1084 }
1085
1086 if let Some(op) = self.overprint_fill {
1087 dict.set("op", Object::Boolean(op));
1088 }
1089
1090 if let Some(mode) = self.overprint_mode {
1091 dict.set("OPM", Object::Integer(mode as i64));
1092 }
1093
1094 if let Some(flatness) = self.flatness {
1095 dict.set("FL", Object::Real(flatness));
1096 }
1097
1098 if let Some(smoothness) = self.smoothness {
1099 dict.set("SM", Object::Real(smoothness));
1100 }
1101
1102 if let Some(sa) = self.stroke_adjustment {
1103 dict.set("SA", Object::Boolean(sa));
1104 }
1105
1106 dict
1107 }
1108}
1109
1110#[derive(Debug, Clone)]
1112pub struct ExtGStateManager {
1113 states: HashMap<String, ExtGState>,
1114 next_id: usize,
1115}
1116
1117impl Default for ExtGStateManager {
1118 fn default() -> Self {
1119 Self::new()
1120 }
1121}
1122
1123impl ExtGStateManager {
1124 pub fn new() -> Self {
1126 Self {
1127 states: HashMap::new(),
1128 next_id: 1,
1129 }
1130 }
1131
1132 pub fn add_state(&mut self, state: ExtGState) -> Result<String> {
1134 if state.is_empty() {
1135 return Err(PdfError::InvalidStructure(
1136 "ExtGState cannot be empty".to_string(),
1137 ));
1138 }
1139
1140 let name = format!("GS{}", self.next_id);
1141 self.states.insert(name.clone(), state);
1142 self.next_id += 1;
1143 Ok(name)
1144 }
1145
1146 pub fn get_state(&self, name: &str) -> Option<&ExtGState> {
1148 self.states.get(name)
1149 }
1150
1151 pub fn states(&self) -> &HashMap<String, ExtGState> {
1153 &self.states
1154 }
1155
1156 pub fn to_resource_dictionary(&self) -> Result<String> {
1158 if self.states.is_empty() {
1159 return Ok(String::new());
1160 }
1161
1162 let mut dict = String::from("/ExtGState <<");
1163
1164 for (name, state) in &self.states {
1165 let state_dict = state.to_pdf_dictionary()?;
1166 write!(&mut dict, " /{name} {state_dict}").map_err(|_| {
1167 PdfError::InvalidStructure("Failed to write ExtGState resource".to_string())
1168 })?;
1169 }
1170
1171 dict.push_str(" >>");
1172 Ok(dict)
1173 }
1174
1175 pub fn clear(&mut self) {
1177 self.states.clear();
1178 self.next_id = 1;
1179 }
1180
1181 pub fn count(&self) -> usize {
1183 self.states.len()
1184 }
1185}
1186
1187#[cfg(test)]
1188mod tests {
1189 use super::*;
1190
1191 #[test]
1192 fn test_rendering_intent_pdf_names() {
1193 assert_eq!(
1194 RenderingIntent::AbsoluteColorimetric.pdf_name(),
1195 "AbsoluteColorimetric"
1196 );
1197 assert_eq!(
1198 RenderingIntent::RelativeColorimetric.pdf_name(),
1199 "RelativeColorimetric"
1200 );
1201 assert_eq!(RenderingIntent::Saturation.pdf_name(), "Saturation");
1202 assert_eq!(RenderingIntent::Perceptual.pdf_name(), "Perceptual");
1203 }
1204
1205 #[test]
1206 fn test_blend_mode_pdf_names() {
1207 assert_eq!(BlendMode::Normal.pdf_name(), "Normal");
1208 assert_eq!(BlendMode::Multiply.pdf_name(), "Multiply");
1209 assert_eq!(BlendMode::Screen.pdf_name(), "Screen");
1210 assert_eq!(BlendMode::Overlay.pdf_name(), "Overlay");
1211 }
1212
1213 #[test]
1214 fn test_line_dash_pattern_creation() {
1215 let solid = LineDashPattern::solid();
1216 assert!(solid.array.is_empty());
1217 assert_eq!(solid.phase, 0.0);
1218
1219 let dashed = LineDashPattern::dashed(5.0, 3.0);
1220 assert_eq!(dashed.array, vec![5.0, 3.0]);
1221 assert_eq!(dashed.phase, 0.0);
1222
1223 let dotted = LineDashPattern::dotted(1.0, 2.0);
1224 assert_eq!(dotted.array, vec![1.0, 2.0]);
1225 }
1226
1227 #[test]
1228 fn test_line_dash_pattern_pdf_string() {
1229 let solid = LineDashPattern::solid();
1230 assert_eq!(solid.to_pdf_string(), "[] 0");
1231
1232 let dashed = LineDashPattern::dashed(5.0, 3.0);
1233 assert_eq!(dashed.to_pdf_string(), "[5.00 3.00] 0.00");
1234
1235 let custom = LineDashPattern::new(vec![10.0, 5.0, 2.0, 5.0], 2.5);
1236 assert_eq!(custom.to_pdf_string(), "[10.00 5.00 2.00 5.00] 2.50");
1237 }
1238
1239 #[test]
1240 fn test_extgstate_font() {
1241 let font = ExtGStateFont::new(Font::Helvetica, 12.0);
1242 assert_eq!(font.font, Font::Helvetica);
1243 assert_eq!(font.size, 12.0);
1244 }
1245
1246 #[test]
1247 fn test_extgstate_creation() {
1248 let state = ExtGState::new();
1249 assert!(state.is_empty());
1250 assert!(!state.uses_transparency());
1251 }
1252
1253 #[test]
1254 fn test_extgstate_line_parameters() {
1255 let state = ExtGState::new()
1256 .with_line_width(2.5)
1257 .with_line_cap(LineCap::Round)
1258 .with_line_join(LineJoin::Bevel)
1259 .with_miter_limit(4.0);
1260
1261 assert_eq!(state.line_width, Some(2.5));
1262 assert_eq!(state.line_cap, Some(LineCap::Round));
1263 assert_eq!(state.line_join, Some(LineJoin::Bevel));
1264 assert_eq!(state.miter_limit, Some(4.0));
1265 assert!(!state.is_empty());
1266 }
1267
1268 #[test]
1269 fn test_extgstate_transparency() {
1270 let state = ExtGState::new()
1271 .with_alpha_stroke(0.8)
1272 .with_alpha_fill(0.6)
1273 .with_blend_mode(BlendMode::Multiply);
1274
1275 assert_eq!(state.alpha_stroke, Some(0.8));
1276 assert_eq!(state.alpha_fill, Some(0.6));
1277 assert_eq!(state.blend_mode, Some(BlendMode::Multiply));
1278 assert!(state.uses_transparency());
1279 }
1280
1281 #[test]
1282 fn test_extgstate_alpha_clamping() {
1283 let state = ExtGState::new()
1284 .with_alpha_stroke(1.5) .with_alpha_fill(-0.1); assert_eq!(state.alpha_stroke, Some(1.0));
1288 assert_eq!(state.alpha_fill, Some(0.0));
1289 }
1290
1291 #[test]
1292 fn test_extgstate_combined_alpha() {
1293 let state = ExtGState::new().with_alpha(0.5);
1294
1295 assert_eq!(state.alpha_stroke, Some(0.5));
1296 assert_eq!(state.alpha_fill, Some(0.5));
1297 }
1298
1299 #[test]
1300 fn test_extgstate_rendering_intent() {
1301 let state = ExtGState::new().with_rendering_intent(RenderingIntent::Perceptual);
1302
1303 assert_eq!(state.rendering_intent, Some(RenderingIntent::Perceptual));
1304 }
1305
1306 #[test]
1307 fn test_extgstate_overprint() {
1308 let state = ExtGState::new()
1309 .with_overprint_stroke(true)
1310 .with_overprint_fill(false)
1311 .with_overprint_mode(1);
1312
1313 assert_eq!(state.overprint_stroke, Some(true));
1314 assert_eq!(state.overprint_fill, Some(false));
1315 assert_eq!(state.overprint_mode, Some(1));
1316 }
1317
1318 #[test]
1319 fn test_extgstate_font_setting() {
1320 let state = ExtGState::new().with_font(Font::HelveticaBold, 14.0);
1321
1322 assert!(state.font.is_some());
1323 let font = state.font.unwrap();
1324 assert_eq!(font.font, Font::HelveticaBold);
1325 assert_eq!(font.size, 14.0);
1326 }
1327
1328 #[test]
1329 fn test_extgstate_tolerance_parameters() {
1330 let state = ExtGState::new()
1331 .with_flatness(1.5)
1332 .with_smoothness(0.8)
1333 .with_stroke_adjustment(true);
1334
1335 assert_eq!(state.flatness, Some(1.5));
1336 assert_eq!(state.smoothness, Some(0.8));
1337 assert_eq!(state.stroke_adjustment, Some(true));
1338 }
1339
1340 #[test]
1341 fn test_extgstate_pdf_dictionary_generation() {
1342 let state = ExtGState::new()
1343 .with_line_width(2.0)
1344 .with_line_cap(LineCap::Round)
1345 .with_alpha(0.5)
1346 .with_blend_mode(BlendMode::Multiply);
1347
1348 let dict = state.to_pdf_dictionary().unwrap();
1349 assert!(dict.contains("/Type /ExtGState"));
1350 assert!(dict.contains("/LW 2.000"));
1351 assert!(dict.contains("/LC 1"));
1352 assert!(dict.contains("/CA 0.500"));
1353 assert!(dict.contains("/ca 0.500"));
1354 assert!(dict.contains("/BM /Multiply"));
1355 }
1356
1357 #[test]
1358 fn test_extgstate_manager_creation() {
1359 let manager = ExtGStateManager::new();
1360 assert_eq!(manager.count(), 0);
1361 assert!(manager.states().is_empty());
1362 }
1363
1364 #[test]
1365 fn test_extgstate_manager_add_state() {
1366 let mut manager = ExtGStateManager::new();
1367 let state = ExtGState::new().with_line_width(2.0);
1368
1369 let name = manager.add_state(state).unwrap();
1370 assert_eq!(name, "GS1");
1371 assert_eq!(manager.count(), 1);
1372
1373 let retrieved = manager.get_state(&name).unwrap();
1374 assert_eq!(retrieved.line_width, Some(2.0));
1375 }
1376
1377 #[test]
1378 fn test_extgstate_manager_empty_state_rejection() {
1379 let mut manager = ExtGStateManager::new();
1380 let empty_state = ExtGState::new();
1381
1382 let result = manager.add_state(empty_state);
1383 assert!(result.is_err());
1384 assert_eq!(manager.count(), 0);
1385 }
1386
1387 #[test]
1388 fn test_extgstate_manager_multiple_states() {
1389 let mut manager = ExtGStateManager::new();
1390
1391 let state1 = ExtGState::new().with_line_width(1.0);
1392 let state2 = ExtGState::new().with_alpha(0.5);
1393
1394 let name1 = manager.add_state(state1).unwrap();
1395 let name2 = manager.add_state(state2).unwrap();
1396
1397 assert_eq!(name1, "GS1");
1398 assert_eq!(name2, "GS2");
1399 assert_eq!(manager.count(), 2);
1400 }
1401
1402 #[test]
1403 fn test_extgstate_manager_resource_dictionary() {
1404 let mut manager = ExtGStateManager::new();
1405
1406 let state = ExtGState::new().with_line_width(2.0);
1407 manager.add_state(state).unwrap();
1408
1409 let dict = manager.to_resource_dictionary().unwrap();
1410 assert!(dict.contains("/ExtGState"));
1411 assert!(dict.contains("/GS1"));
1412 assert!(dict.contains("/LW 2.000"));
1413 }
1414
1415 #[test]
1416 fn test_extgstate_manager_clear() {
1417 let mut manager = ExtGStateManager::new();
1418
1419 let state = ExtGState::new().with_line_width(1.0);
1420 manager.add_state(state).unwrap();
1421 assert_eq!(manager.count(), 1);
1422
1423 manager.clear();
1424 assert_eq!(manager.count(), 0);
1425 assert!(manager.states().is_empty());
1426 }
1427
1428 #[test]
1429 fn test_extgstate_value_validation() {
1430 let state = ExtGState::new().with_line_width(-1.0);
1432 assert_eq!(state.line_width, Some(0.0));
1433
1434 let state = ExtGState::new().with_miter_limit(0.5);
1436 assert_eq!(state.miter_limit, Some(1.0));
1437
1438 let state = ExtGState::new().with_flatness(150.0);
1440 assert_eq!(state.flatness, Some(100.0));
1441
1442 let state = ExtGState::new().with_smoothness(1.5);
1444 assert_eq!(state.smoothness, Some(1.0));
1445
1446 let state = ExtGState::new().with_font(Font::Helvetica, -5.0);
1448 assert_eq!(state.font.unwrap().size, 0.0);
1449 }
1450
1451 #[test]
1452 fn test_line_dash_patterns() {
1453 let state = ExtGState::new().with_dash_pattern(LineDashPattern::dashed(10.0, 5.0));
1454
1455 let dict = state.to_pdf_dictionary().unwrap();
1456 assert!(dict.contains("/D [10.00 5.00] 0.00"));
1457 }
1458
1459 #[test]
1460 fn test_complex_extgstate() {
1461 let dash_pattern = LineDashPattern::new(vec![3.0, 2.0, 1.0, 2.0], 1.0);
1462
1463 let state = ExtGState::new()
1464 .with_line_width(1.5)
1465 .with_line_cap(LineCap::Square)
1466 .with_line_join(LineJoin::Round)
1467 .with_miter_limit(10.0)
1468 .with_dash_pattern(dash_pattern)
1469 .with_rendering_intent(RenderingIntent::Saturation)
1470 .with_overprint_stroke(true)
1471 .with_overprint_fill(false)
1472 .with_font(Font::TimesBold, 18.0)
1473 .with_flatness(0.5)
1474 .with_smoothness(0.1)
1475 .with_stroke_adjustment(false)
1476 .with_blend_mode(BlendMode::SoftLight)
1477 .with_alpha_stroke(0.8)
1478 .with_alpha_fill(0.6)
1479 .with_alpha_is_shape(true)
1480 .with_text_knockout(false);
1481
1482 assert!(!state.is_empty());
1483 assert!(state.uses_transparency());
1484
1485 let dict = state.to_pdf_dictionary().unwrap();
1486 assert!(dict.contains("/Type /ExtGState"));
1487 assert!(dict.contains("/LW 1.500"));
1488 assert!(dict.contains("/LC 2"));
1489 assert!(dict.contains("/LJ 1"));
1490 assert!(dict.contains("/ML 10.000"));
1491 assert!(dict.contains("/D [3.00 2.00 1.00 2.00] 1.00"));
1492 assert!(dict.contains("/RI /Saturation"));
1493 assert!(dict.contains("/OP true"));
1494 assert!(dict.contains("/op false"));
1495 assert!(dict.contains("/Font [/Times-Bold 18.000]"));
1496 assert!(dict.contains("/FL 0.500"));
1497 assert!(dict.contains("/SM 0.100"));
1498 assert!(dict.contains("/SA false"));
1499 assert!(dict.contains("/BM /SoftLight"));
1500 assert!(dict.contains("/CA 0.800"));
1501 assert!(dict.contains("/ca 0.600"));
1502 assert!(dict.contains("/AIS true"));
1503 assert!(dict.contains("/TK false"));
1504 }
1505
1506 #[test]
1507 fn test_transfer_function_identity() {
1508 let tf = TransferFunction::identity();
1509 assert_eq!(tf.to_pdf_string(), "/Identity");
1510 }
1511
1512 #[test]
1513 fn test_transfer_function_gamma() {
1514 let tf = TransferFunction::gamma(2.2);
1515 let pdf = tf.to_pdf_string();
1516 assert!(pdf.contains("/FunctionType 2"));
1517 assert!(pdf.contains("/N 2.200"));
1518 assert!(pdf.contains("/Domain [0.000 1.000]"));
1519 assert!(pdf.contains("/Range [0.000 1.000]"));
1520 assert!(pdf.contains("/C0 [0.000]"));
1521 assert!(pdf.contains("/C1 [1.000]"));
1522 }
1523
1524 #[test]
1525 fn test_transfer_function_linear() {
1526 let tf = TransferFunction::linear(0.8, 0.1);
1527 let pdf = tf.to_pdf_string();
1528 assert!(pdf.contains("/FunctionType 2"));
1529 assert!(pdf.contains("/N 1.000"));
1530 assert!(pdf.contains("/C0 [0.100]")); assert!(pdf.contains("/C1 [0.900]")); }
1533
1534 #[test]
1535 fn test_extgstate_with_transfer_functions() {
1536 let state = ExtGState::new()
1537 .with_gamma_correction(1.8)
1538 .with_transfer_function_2(TransferFunction::identity())
1539 .with_black_generation(TransferFunction::linear(1.0, 0.0))
1540 .with_undercolor_removal(TransferFunction::gamma(2.2));
1541
1542 assert!(!state.is_empty());
1543
1544 let dict = state.to_pdf_dictionary().unwrap();
1545 assert!(dict.contains("/TR"));
1546 assert!(dict.contains("/TR2 /Identity"));
1547 assert!(dict.contains("/BG"));
1548 assert!(dict.contains("/UCR"));
1549 assert!(dict.contains("/N 1.800")); assert!(dict.contains("/N 2.200")); }
1552
1553 #[test]
1554 fn test_transfer_function_separate() {
1555 let c_func = TransferFunctionData {
1556 function_type: 2,
1557 domain: vec![0.0, 1.0],
1558 range: vec![0.0, 1.0],
1559 params: TransferFunctionParams::Exponential {
1560 c0: vec![0.0],
1561 c1: vec![1.0],
1562 n: 1.5,
1563 },
1564 };
1565
1566 let m_func = c_func.clone();
1567 let y_func = c_func.clone();
1568 let k_func = Some(TransferFunctionData {
1569 function_type: 2,
1570 domain: vec![0.0, 1.0],
1571 range: vec![0.0, 1.0],
1572 params: TransferFunctionParams::Exponential {
1573 c0: vec![0.1],
1574 c1: vec![0.9],
1575 n: 2.0,
1576 },
1577 });
1578
1579 let tf = TransferFunction::Separate {
1580 c_or_r: c_func,
1581 m_or_g: m_func,
1582 y_or_b: y_func,
1583 k: k_func,
1584 };
1585
1586 let pdf = tf.to_pdf_string();
1587 assert!(pdf.starts_with('['));
1588 assert!(pdf.ends_with(']'));
1589 assert!(pdf.contains("/FunctionType 2"));
1590 assert_eq!(pdf.matches("/FunctionType 2").count(), 4);
1592 }
1593}