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 {
145 use crate::graphics::color::finite_or_zero;
146 if self.array.is_empty() {
147 "[] 0".to_string()
148 } else {
149 let array_str = self
150 .array
151 .iter()
152 .map(|&x| format!("{:.2}", finite_or_zero(x)))
153 .collect::<Vec<_>>()
154 .join(" ");
155 format!("[{array_str}] {:.2}", finite_or_zero(self.phase))
156 }
157 }
158}
159
160#[derive(Debug, Clone, PartialEq)]
162pub struct ExtGStateFont {
163 pub font: Font,
165 pub size: f64,
167}
168
169impl ExtGStateFont {
170 pub fn new(font: Font, size: f64) -> Self {
172 Self { font, size }
173 }
174}
175
176#[derive(Debug, Clone, PartialEq)]
178#[allow(clippy::large_enum_variant)]
179pub enum TransferFunction {
180 Identity,
182 Single(TransferFunctionData),
184 Separate {
186 c_or_r: TransferFunctionData,
188 m_or_g: TransferFunctionData,
190 y_or_b: TransferFunctionData,
192 k: Option<TransferFunctionData>,
194 },
195}
196
197#[derive(Debug, Clone, PartialEq)]
199pub struct TransferFunctionData {
200 pub function_type: u32,
202 pub domain: Vec<f64>,
204 pub range: Vec<f64>,
206 pub params: TransferFunctionParams,
208}
209
210#[derive(Debug, Clone, PartialEq)]
212pub enum TransferFunctionParams {
213 Sampled {
215 samples: Vec<f64>,
217 size: Vec<u32>,
219 bits_per_sample: u32,
221 },
222 Exponential {
224 c0: Vec<f64>,
226 c1: Vec<f64>,
228 n: f64,
230 },
231 Stitching {
233 functions: Vec<TransferFunctionData>,
235 bounds: Vec<f64>,
237 encode: Vec<f64>,
239 },
240 PostScript {
242 code: String,
244 },
245}
246
247impl TransferFunction {
248 pub fn identity() -> Self {
250 TransferFunction::Identity
251 }
252
253 pub fn gamma(gamma_value: f64) -> Self {
255 TransferFunction::Single(TransferFunctionData {
256 function_type: 2,
257 domain: vec![0.0, 1.0],
258 range: vec![0.0, 1.0],
259 params: TransferFunctionParams::Exponential {
260 c0: vec![0.0],
261 c1: vec![1.0],
262 n: gamma_value,
263 },
264 })
265 }
266
267 pub fn linear(slope: f64, intercept: f64) -> Self {
269 TransferFunction::Single(TransferFunctionData {
270 function_type: 2,
271 domain: vec![0.0, 1.0],
272 range: vec![0.0, 1.0],
273 params: TransferFunctionParams::Exponential {
274 c0: vec![intercept],
275 c1: vec![slope + intercept],
276 n: 1.0,
277 },
278 })
279 }
280
281 pub fn to_pdf_string(&self) -> String {
283 match self {
284 TransferFunction::Identity => "/Identity".to_string(),
285 TransferFunction::Single(data) => data.to_pdf_string(),
286 TransferFunction::Separate {
287 c_or_r,
288 m_or_g,
289 y_or_b,
290 k,
291 } => {
292 let mut result = String::from("[");
293 result.push_str(&c_or_r.to_pdf_string());
294 result.push(' ');
295 result.push_str(&m_or_g.to_pdf_string());
296 result.push(' ');
297 result.push_str(&y_or_b.to_pdf_string());
298 if let Some(k_func) = k {
299 result.push(' ');
300 result.push_str(&k_func.to_pdf_string());
301 }
302 result.push(']');
303 result
304 }
305 }
306 }
307}
308
309impl TransferFunctionData {
310 pub fn to_pdf_string(&self) -> String {
312 let mut dict = String::from("<<");
313
314 dict.push_str(&format!(" /FunctionType {}", self.function_type));
316
317 dict.push_str(" /Domain [");
319 for (i, val) in self.domain.iter().enumerate() {
320 if i > 0 {
321 dict.push(' ');
322 }
323 dict.push_str(&format!("{:.3}", val));
324 }
325 dict.push(']');
326
327 dict.push_str(" /Range [");
329 for (i, val) in self.range.iter().enumerate() {
330 if i > 0 {
331 dict.push(' ');
332 }
333 dict.push_str(&format!("{:.3}", val));
334 }
335 dict.push(']');
336
337 match &self.params {
339 TransferFunctionParams::Exponential { c0, c1, n } => {
340 dict.push_str(" /C0 [");
342 for (i, val) in c0.iter().enumerate() {
343 if i > 0 {
344 dict.push(' ');
345 }
346 dict.push_str(&format!("{:.3}", val));
347 }
348 dict.push_str("] /C1 [");
349 for (i, val) in c1.iter().enumerate() {
350 if i > 0 {
351 dict.push(' ');
352 }
353 dict.push_str(&format!("{:.3}", val));
354 }
355 dict.push_str(&format!("] /N {:.3}", n));
356 }
357 TransferFunctionParams::Sampled {
358 size,
359 bits_per_sample,
360 samples,
361 ..
362 } => {
363 dict.push_str(" /Size [");
365 for (i, val) in size.iter().enumerate() {
366 if i > 0 {
367 dict.push(' ');
368 }
369 dict.push_str(&format!("{}", val));
370 }
371 dict.push_str(&format!("] /BitsPerSample {}", bits_per_sample));
372 dict.push_str(" /Length ");
374 dict.push_str(&format!("{}", samples.len()));
375 }
376 TransferFunctionParams::Stitching {
377 bounds,
378 encode,
379 functions,
380 } => {
381 dict.push_str(" /Bounds [");
383 for (i, val) in bounds.iter().enumerate() {
384 if i > 0 {
385 dict.push(' ');
386 }
387 dict.push_str(&format!("{:.3}", val));
388 }
389 dict.push_str("] /Encode [");
390 for (i, val) in encode.iter().enumerate() {
391 if i > 0 {
392 dict.push(' ');
393 }
394 dict.push_str(&format!("{:.3}", val));
395 }
396 dict.push_str("] /Functions [");
397 for (i, func) in functions.iter().enumerate() {
398 if i > 0 {
399 dict.push(' ');
400 }
401 dict.push_str(&func.to_pdf_string());
402 }
403 dict.push(']');
404 }
405 TransferFunctionParams::PostScript { code } => {
406 dict.push_str(&format!(
408 " /Length {} stream\n{}\nendstream",
409 code.len(),
410 code
411 ));
412 }
413 }
414
415 dict.push_str(" >>");
416 dict
417 }
418}
419
420#[derive(Debug, Clone, PartialEq)]
422pub enum Halftone {
423 Default,
425 Type1 {
427 frequency: f64,
429 angle: f64,
431 spot_function: SpotFunction,
433 },
434 Type5 {
436 colorants: HashMap<String, HalftoneColorant>,
438 default: Box<Halftone>,
440 },
441 Type6 {
443 width: u32,
445 height: u32,
447 thresholds: Vec<u8>,
449 },
450 Type10 {
452 frequency: f64,
454 },
455 Type16 {
457 width: u32,
459 height: u32,
461 thresholds: Vec<Vec<u8>>,
463 },
464}
465
466#[derive(Debug, Clone, PartialEq)]
468pub enum SpotFunction {
469 SimpleDot,
471 InvertedSimpleDot,
473 Round,
475 InvertedRound,
477 Ellipse,
479 Square,
481 Cross,
483 Diamond,
485 Line,
487 Custom(String),
489}
490
491impl SpotFunction {
492 pub fn pdf_name(&self) -> String {
494 match self {
495 SpotFunction::SimpleDot => "SimpleDot".to_string(),
496 SpotFunction::InvertedSimpleDot => "InvertedSimpleDot".to_string(),
497 SpotFunction::Round => "Round".to_string(),
498 SpotFunction::InvertedRound => "InvertedRound".to_string(),
499 SpotFunction::Ellipse => "Ellipse".to_string(),
500 SpotFunction::Square => "Square".to_string(),
501 SpotFunction::Cross => "Cross".to_string(),
502 SpotFunction::Diamond => "Diamond".to_string(),
503 SpotFunction::Line => "Line".to_string(),
504 SpotFunction::Custom(name) => name.clone(),
505 }
506 }
507}
508
509#[derive(Debug, Clone, PartialEq)]
511pub struct HalftoneColorant {
512 pub frequency: f64,
514 pub angle: f64,
516 pub spot_function: SpotFunction,
518}
519
520#[derive(Debug, Clone)]
522pub struct ExtGState {
523 pub line_width: Option<f64>,
526 pub line_cap: Option<LineCap>,
528 pub line_join: Option<LineJoin>,
530 pub miter_limit: Option<f64>,
532 pub dash_pattern: Option<LineDashPattern>,
534
535 pub rendering_intent: Option<RenderingIntent>,
538
539 pub overprint_stroke: Option<bool>,
542 pub overprint_fill: Option<bool>,
544 pub overprint_mode: Option<u8>,
546
547 pub font: Option<ExtGStateFont>,
550
551 pub black_generation: Option<TransferFunction>,
554 pub black_generation_2: Option<TransferFunction>,
556 pub undercolor_removal: Option<TransferFunction>,
558 pub undercolor_removal_2: Option<TransferFunction>,
560 pub transfer_function: Option<TransferFunction>,
562 pub transfer_function_2: Option<TransferFunction>,
564
565 pub halftone: Option<Halftone>,
568
569 pub flatness: Option<f64>,
572 pub smoothness: Option<f64>,
574
575 pub stroke_adjustment: Option<bool>,
578
579 pub blend_mode: Option<BlendMode>,
582 pub soft_mask: Option<SoftMask>,
584 pub alpha_stroke: Option<f64>,
586 pub alpha_fill: Option<f64>,
588 pub alpha_is_shape: Option<bool>,
590 pub text_knockout: Option<bool>,
592
593 pub use_black_point_compensation: Option<bool>,
596}
597
598impl Default for ExtGState {
599 fn default() -> Self {
600 Self::new()
601 }
602}
603
604impl ExtGState {
605 pub fn new() -> Self {
607 Self {
608 line_width: None,
609 line_cap: None,
610 line_join: None,
611 miter_limit: None,
612 dash_pattern: None,
613 rendering_intent: None,
614 overprint_stroke: None,
615 overprint_fill: None,
616 overprint_mode: None,
617 font: None,
618 black_generation: None,
619 black_generation_2: None,
620 undercolor_removal: None,
621 undercolor_removal_2: None,
622 transfer_function: None,
623 transfer_function_2: None,
624 halftone: None,
625 flatness: None,
626 smoothness: None,
627 stroke_adjustment: None,
628 blend_mode: None,
629 soft_mask: None,
630 alpha_stroke: None,
631 alpha_fill: None,
632 alpha_is_shape: None,
633 text_knockout: None,
634 use_black_point_compensation: None,
635 }
636 }
637
638 pub fn with_line_width(mut self, width: f64) -> Self {
641 self.line_width = Some(width.max(0.0));
642 self
643 }
644
645 pub fn with_line_cap(mut self, cap: LineCap) -> Self {
647 self.line_cap = Some(cap);
648 self
649 }
650
651 pub fn with_line_join(mut self, join: LineJoin) -> Self {
653 self.line_join = Some(join);
654 self
655 }
656
657 pub fn with_miter_limit(mut self, limit: f64) -> Self {
659 self.miter_limit = Some(limit.max(1.0));
660 self
661 }
662
663 pub fn with_dash_pattern(mut self, pattern: LineDashPattern) -> Self {
665 self.dash_pattern = Some(pattern);
666 self
667 }
668
669 pub fn with_rendering_intent(mut self, intent: RenderingIntent) -> Self {
672 self.rendering_intent = Some(intent);
673 self
674 }
675
676 pub fn with_overprint_stroke(mut self, overprint: bool) -> Self {
679 self.overprint_stroke = Some(overprint);
680 self
681 }
682
683 pub fn with_overprint_fill(mut self, overprint: bool) -> Self {
685 self.overprint_fill = Some(overprint);
686 self
687 }
688
689 pub fn with_overprint_mode(mut self, mode: u8) -> Self {
691 self.overprint_mode = Some(mode);
692 self
693 }
694
695 pub fn with_font(mut self, font: Font, size: f64) -> Self {
698 self.font = Some(ExtGStateFont::new(font, size.max(0.0)));
699 self
700 }
701
702 pub fn with_flatness(mut self, flatness: f64) -> Self {
705 self.flatness = Some(flatness.clamp(0.0, 100.0));
706 self
707 }
708
709 pub fn with_smoothness(mut self, smoothness: f64) -> Self {
711 self.smoothness = Some(smoothness.clamp(0.0, 1.0));
712 self
713 }
714
715 pub fn with_stroke_adjustment(mut self, adjustment: bool) -> Self {
717 self.stroke_adjustment = Some(adjustment);
718 self
719 }
720
721 pub fn with_blend_mode(mut self, mode: BlendMode) -> Self {
724 self.blend_mode = Some(mode);
725 self
726 }
727
728 pub fn with_alpha_stroke(mut self, alpha: f64) -> Self {
730 self.alpha_stroke = Some(alpha.clamp(0.0, 1.0));
731 self
732 }
733
734 pub fn with_alpha_fill(mut self, alpha: f64) -> Self {
736 self.alpha_fill = Some(alpha.clamp(0.0, 1.0));
737 self
738 }
739
740 pub fn with_alpha(mut self, alpha: f64) -> Self {
742 let clamped = alpha.clamp(0.0, 1.0);
743 self.alpha_stroke = Some(clamped);
744 self.alpha_fill = Some(clamped);
745 self
746 }
747
748 pub fn with_alpha_is_shape(mut self, is_shape: bool) -> Self {
750 self.alpha_is_shape = Some(is_shape);
751 self
752 }
753
754 pub fn with_text_knockout(mut self, knockout: bool) -> Self {
756 self.text_knockout = Some(knockout);
757 self
758 }
759
760 pub fn set_soft_mask(&mut self, mask: SoftMask) {
762 self.soft_mask = Some(mask);
763 }
764
765 pub fn set_soft_mask_name(&mut self, name: String) {
767 self.soft_mask = Some(SoftMask::luminosity(name));
768 }
769
770 pub fn set_soft_mask_none(&mut self) {
772 self.soft_mask = Some(SoftMask::none());
773 }
774
775 pub fn with_black_point_compensation(mut self, use_compensation: bool) -> Self {
777 self.use_black_point_compensation = Some(use_compensation);
778 self
779 }
780
781 pub fn with_transfer_function(mut self, func: TransferFunction) -> Self {
784 self.transfer_function = Some(func);
785 self
786 }
787
788 pub fn with_gamma_correction(mut self, gamma: f64) -> Self {
790 self.transfer_function = Some(TransferFunction::gamma(gamma));
791 self
792 }
793
794 pub fn with_linear_transfer(mut self, slope: f64, intercept: f64) -> Self {
796 self.transfer_function = Some(TransferFunction::linear(slope, intercept));
797 self
798 }
799
800 pub fn with_transfer_function_2(mut self, func: TransferFunction) -> Self {
802 self.transfer_function_2 = Some(func);
803 self
804 }
805
806 pub fn with_black_generation(mut self, func: TransferFunction) -> Self {
808 self.black_generation = Some(func);
809 self
810 }
811
812 pub fn with_undercolor_removal(mut self, func: TransferFunction) -> Self {
814 self.undercolor_removal = Some(func);
815 self
816 }
817
818 pub fn uses_transparency(&self) -> bool {
820 self.alpha_stroke.is_some_and(|a| a < 1.0)
821 || self.alpha_fill.is_some_and(|a| a < 1.0)
822 || self.blend_mode.is_some()
823 || self.soft_mask.is_some()
824 }
825
826 pub fn to_pdf_dictionary(&self) -> Result<String> {
828 let mut dict = String::from("<< /Type /ExtGState");
829
830 if let Some(width) = self.line_width {
832 write!(&mut dict, " /LW {width:.3}").map_err(|_| {
833 PdfError::InvalidStructure("Failed to write line width".to_string())
834 })?;
835 }
836
837 if let Some(cap) = self.line_cap {
838 write!(&mut dict, " /LC {}", cap as u8)
839 .map_err(|_| PdfError::InvalidStructure("Failed to write line cap".to_string()))?;
840 }
841
842 if let Some(join) = self.line_join {
843 write!(&mut dict, " /LJ {}", join as u8)
844 .map_err(|_| PdfError::InvalidStructure("Failed to write line join".to_string()))?;
845 }
846
847 if let Some(limit) = self.miter_limit {
848 write!(&mut dict, " /ML {limit:.3}").map_err(|_| {
849 PdfError::InvalidStructure("Failed to write miter limit".to_string())
850 })?;
851 }
852
853 if let Some(ref pattern) = self.dash_pattern {
854 write!(&mut dict, " /D {}", pattern.to_pdf_string()).map_err(|_| {
855 PdfError::InvalidStructure("Failed to write dash pattern".to_string())
856 })?;
857 }
858
859 if let Some(intent) = self.rendering_intent {
861 write!(&mut dict, " /RI /{}", intent.pdf_name()).map_err(|_| {
862 PdfError::InvalidStructure("Failed to write rendering intent".to_string())
863 })?;
864 }
865
866 if let Some(op) = self.overprint_stroke {
868 write!(&mut dict, " /OP {op}").map_err(|_| {
869 PdfError::InvalidStructure("Failed to write overprint stroke".to_string())
870 })?;
871 }
872
873 if let Some(op) = self.overprint_fill {
874 write!(&mut dict, " /op {op}").map_err(|_| {
875 PdfError::InvalidStructure("Failed to write overprint fill".to_string())
876 })?;
877 }
878
879 if let Some(mode) = self.overprint_mode {
880 write!(&mut dict, " /OPM {mode}").map_err(|_| {
881 PdfError::InvalidStructure("Failed to write overprint mode".to_string())
882 })?;
883 }
884
885 if let Some(ref font) = self.font {
887 write!(
888 &mut dict,
889 " /Font [/{} {:.3}]",
890 font.font.pdf_name(),
891 font.size
892 )
893 .map_err(|_| PdfError::InvalidStructure("Failed to write font".to_string()))?;
894 }
895
896 if let Some(flatness) = self.flatness {
898 write!(&mut dict, " /FL {flatness:.3}")
899 .map_err(|_| PdfError::InvalidStructure("Failed to write flatness".to_string()))?;
900 }
901
902 if let Some(smoothness) = self.smoothness {
903 write!(&mut dict, " /SM {smoothness:.3}").map_err(|_| {
904 PdfError::InvalidStructure("Failed to write smoothness".to_string())
905 })?;
906 }
907
908 if let Some(sa) = self.stroke_adjustment {
910 write!(&mut dict, " /SA {sa}").map_err(|_| {
911 PdfError::InvalidStructure("Failed to write stroke adjustment".to_string())
912 })?;
913 }
914
915 if let Some(ref mode) = self.blend_mode {
917 write!(&mut dict, " /BM /{}", mode.pdf_name()).map_err(|_| {
918 PdfError::InvalidStructure("Failed to write blend mode".to_string())
919 })?;
920 }
921
922 if let Some(ref mask) = self.soft_mask {
923 if mask.is_none() {
924 write!(&mut dict, " /SMask /None").map_err(|_| {
925 PdfError::InvalidStructure("Failed to write soft mask".to_string())
926 })?;
927 } else {
928 write!(&mut dict, " /SMask {}", mask.to_pdf_string()).map_err(|_| {
931 PdfError::InvalidStructure("Failed to write soft mask".to_string())
932 })?;
933 }
934 }
935
936 if let Some(alpha) = self.alpha_stroke {
937 write!(&mut dict, " /CA {alpha:.3}").map_err(|_| {
938 PdfError::InvalidStructure("Failed to write stroke alpha".to_string())
939 })?;
940 }
941
942 if let Some(alpha) = self.alpha_fill {
943 write!(&mut dict, " /ca {alpha:.3}").map_err(|_| {
944 PdfError::InvalidStructure("Failed to write fill alpha".to_string())
945 })?;
946 }
947
948 if let Some(ais) = self.alpha_is_shape {
949 write!(&mut dict, " /AIS {ais}").map_err(|_| {
950 PdfError::InvalidStructure("Failed to write alpha is shape".to_string())
951 })?;
952 }
953
954 if let Some(tk) = self.text_knockout {
955 write!(&mut dict, " /TK {tk}").map_err(|_| {
956 PdfError::InvalidStructure("Failed to write text knockout".to_string())
957 })?;
958 }
959
960 if let Some(ref tf) = self.transfer_function {
962 write!(&mut dict, " /TR {}", tf.to_pdf_string()).map_err(|_| {
963 PdfError::InvalidStructure("Failed to write transfer function".to_string())
964 })?;
965 }
966
967 if let Some(ref tf) = self.transfer_function_2 {
968 write!(&mut dict, " /TR2 {}", tf.to_pdf_string()).map_err(|_| {
969 PdfError::InvalidStructure("Failed to write transfer function 2".to_string())
970 })?;
971 }
972
973 if let Some(ref bg) = self.black_generation {
974 write!(&mut dict, " /BG {}", bg.to_pdf_string()).map_err(|_| {
975 PdfError::InvalidStructure("Failed to write black generation".to_string())
976 })?;
977 }
978
979 if let Some(ref bg) = self.black_generation_2 {
980 write!(&mut dict, " /BG2 {}", bg.to_pdf_string()).map_err(|_| {
981 PdfError::InvalidStructure("Failed to write black generation 2".to_string())
982 })?;
983 }
984
985 if let Some(ref ucr) = self.undercolor_removal {
986 write!(&mut dict, " /UCR {}", ucr.to_pdf_string()).map_err(|_| {
987 PdfError::InvalidStructure("Failed to write undercolor removal".to_string())
988 })?;
989 }
990
991 if let Some(ref ucr) = self.undercolor_removal_2 {
992 write!(&mut dict, " /UCR2 {}", ucr.to_pdf_string()).map_err(|_| {
993 PdfError::InvalidStructure("Failed to write undercolor removal 2".to_string())
994 })?;
995 }
996
997 if let Some(use_comp) = self.use_black_point_compensation {
999 write!(&mut dict, " /UseBlackPtComp {use_comp}").map_err(|_| {
1000 PdfError::InvalidStructure("Failed to write black point compensation".to_string())
1001 })?;
1002 }
1003
1004 dict.push_str(" >>");
1005 Ok(dict)
1006 }
1007
1008 pub fn is_empty(&self) -> bool {
1010 self.line_width.is_none()
1011 && self.line_cap.is_none()
1012 && self.line_join.is_none()
1013 && self.miter_limit.is_none()
1014 && self.dash_pattern.is_none()
1015 && self.rendering_intent.is_none()
1016 && self.overprint_stroke.is_none()
1017 && self.overprint_fill.is_none()
1018 && self.overprint_mode.is_none()
1019 && self.font.is_none()
1020 && self.flatness.is_none()
1021 && self.smoothness.is_none()
1022 && self.stroke_adjustment.is_none()
1023 && self.blend_mode.is_none()
1024 && self.soft_mask.is_none()
1025 && self.alpha_stroke.is_none()
1026 && self.alpha_fill.is_none()
1027 && self.alpha_is_shape.is_none()
1028 && self.text_knockout.is_none()
1029 && self.transfer_function.is_none()
1030 && self.transfer_function_2.is_none()
1031 && self.black_generation.is_none()
1032 && self.black_generation_2.is_none()
1033 && self.undercolor_removal.is_none()
1034 && self.undercolor_removal_2.is_none()
1035 && self.use_black_point_compensation.is_none()
1036 }
1037
1038 pub fn to_dict(&self) -> crate::objects::Dictionary {
1040 use crate::objects::{Dictionary, Object};
1041
1042 let mut dict = Dictionary::new();
1043 dict.set("Type", Object::Name("ExtGState".to_string()));
1044
1045 if let Some(width) = self.line_width {
1047 dict.set("LW", Object::Real(width));
1048 }
1049
1050 if let Some(cap) = self.line_cap {
1051 dict.set("LC", Object::Integer(cap as i64));
1052 }
1053
1054 if let Some(join) = self.line_join {
1055 dict.set("LJ", Object::Integer(join as i64));
1056 }
1057
1058 if let Some(limit) = self.miter_limit {
1059 dict.set("ML", Object::Real(limit));
1060 }
1061
1062 if let Some(mode) = &self.blend_mode {
1064 dict.set("BM", Object::Name(mode.pdf_name().to_string()));
1065 }
1066
1067 if let Some(alpha) = self.alpha_stroke {
1068 dict.set("CA", Object::Real(alpha));
1069 }
1070
1071 if let Some(alpha) = self.alpha_fill {
1072 dict.set("ca", Object::Real(alpha));
1073 }
1074
1075 if let Some(ais) = self.alpha_is_shape {
1076 dict.set("AIS", Object::Boolean(ais));
1077 }
1078
1079 if let Some(tk) = self.text_knockout {
1080 dict.set("TK", Object::Boolean(tk));
1081 }
1082
1083 if let Some(intent) = &self.rendering_intent {
1085 dict.set("RI", Object::Name(intent.pdf_name().to_string()));
1086 }
1087
1088 if let Some(op) = self.overprint_stroke {
1089 dict.set("OP", Object::Boolean(op));
1090 }
1091
1092 if let Some(op) = self.overprint_fill {
1093 dict.set("op", Object::Boolean(op));
1094 }
1095
1096 if let Some(mode) = self.overprint_mode {
1097 dict.set("OPM", Object::Integer(mode as i64));
1098 }
1099
1100 if let Some(flatness) = self.flatness {
1101 dict.set("FL", Object::Real(flatness));
1102 }
1103
1104 if let Some(smoothness) = self.smoothness {
1105 dict.set("SM", Object::Real(smoothness));
1106 }
1107
1108 if let Some(sa) = self.stroke_adjustment {
1109 dict.set("SA", Object::Boolean(sa));
1110 }
1111
1112 dict
1113 }
1114}
1115
1116#[derive(Debug, Clone)]
1118pub struct ExtGStateManager {
1119 states: HashMap<String, ExtGState>,
1120 next_id: usize,
1121}
1122
1123impl Default for ExtGStateManager {
1124 fn default() -> Self {
1125 Self::new()
1126 }
1127}
1128
1129impl ExtGStateManager {
1130 pub fn new() -> Self {
1132 Self {
1133 states: HashMap::new(),
1134 next_id: 1,
1135 }
1136 }
1137
1138 pub fn add_state(&mut self, state: ExtGState) -> Result<String> {
1140 if state.is_empty() {
1141 return Err(PdfError::InvalidStructure(
1142 "ExtGState cannot be empty".to_string(),
1143 ));
1144 }
1145
1146 let name = format!("GS{}", self.next_id);
1147 self.states.insert(name.clone(), state);
1148 self.next_id += 1;
1149 Ok(name)
1150 }
1151
1152 pub fn get_state(&self, name: &str) -> Option<&ExtGState> {
1154 self.states.get(name)
1155 }
1156
1157 pub fn states(&self) -> &HashMap<String, ExtGState> {
1159 &self.states
1160 }
1161
1162 pub fn to_resource_dictionary(&self) -> Result<String> {
1164 if self.states.is_empty() {
1165 return Ok(String::new());
1166 }
1167
1168 let mut dict = String::from("/ExtGState <<");
1169
1170 for (name, state) in &self.states {
1171 let state_dict = state.to_pdf_dictionary()?;
1172 write!(&mut dict, " /{name} {state_dict}").map_err(|_| {
1173 PdfError::InvalidStructure("Failed to write ExtGState resource".to_string())
1174 })?;
1175 }
1176
1177 dict.push_str(" >>");
1178 Ok(dict)
1179 }
1180
1181 pub fn clear(&mut self) {
1183 self.states.clear();
1184 self.next_id = 1;
1185 }
1186
1187 pub fn count(&self) -> usize {
1189 self.states.len()
1190 }
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195 use super::*;
1196
1197 #[test]
1198 fn test_rendering_intent_pdf_names() {
1199 assert_eq!(
1200 RenderingIntent::AbsoluteColorimetric.pdf_name(),
1201 "AbsoluteColorimetric"
1202 );
1203 assert_eq!(
1204 RenderingIntent::RelativeColorimetric.pdf_name(),
1205 "RelativeColorimetric"
1206 );
1207 assert_eq!(RenderingIntent::Saturation.pdf_name(), "Saturation");
1208 assert_eq!(RenderingIntent::Perceptual.pdf_name(), "Perceptual");
1209 }
1210
1211 #[test]
1212 fn test_blend_mode_pdf_names() {
1213 assert_eq!(BlendMode::Normal.pdf_name(), "Normal");
1214 assert_eq!(BlendMode::Multiply.pdf_name(), "Multiply");
1215 assert_eq!(BlendMode::Screen.pdf_name(), "Screen");
1216 assert_eq!(BlendMode::Overlay.pdf_name(), "Overlay");
1217 }
1218
1219 #[test]
1220 fn test_line_dash_pattern_creation() {
1221 let solid = LineDashPattern::solid();
1222 assert!(solid.array.is_empty());
1223 assert_eq!(solid.phase, 0.0);
1224
1225 let dashed = LineDashPattern::dashed(5.0, 3.0);
1226 assert_eq!(dashed.array, vec![5.0, 3.0]);
1227 assert_eq!(dashed.phase, 0.0);
1228
1229 let dotted = LineDashPattern::dotted(1.0, 2.0);
1230 assert_eq!(dotted.array, vec![1.0, 2.0]);
1231 }
1232
1233 #[test]
1234 fn test_line_dash_pattern_pdf_string() {
1235 let solid = LineDashPattern::solid();
1236 assert_eq!(solid.to_pdf_string(), "[] 0");
1237
1238 let dashed = LineDashPattern::dashed(5.0, 3.0);
1239 assert_eq!(dashed.to_pdf_string(), "[5.00 3.00] 0.00");
1240
1241 let custom = LineDashPattern::new(vec![10.0, 5.0, 2.0, 5.0], 2.5);
1242 assert_eq!(custom.to_pdf_string(), "[10.00 5.00 2.00 5.00] 2.50");
1243 }
1244
1245 #[test]
1246 fn test_extgstate_font() {
1247 let font = ExtGStateFont::new(Font::Helvetica, 12.0);
1248 assert_eq!(font.font, Font::Helvetica);
1249 assert_eq!(font.size, 12.0);
1250 }
1251
1252 #[test]
1253 fn test_extgstate_creation() {
1254 let state = ExtGState::new();
1255 assert!(state.is_empty());
1256 assert!(!state.uses_transparency());
1257 }
1258
1259 #[test]
1260 fn test_extgstate_line_parameters() {
1261 let state = ExtGState::new()
1262 .with_line_width(2.5)
1263 .with_line_cap(LineCap::Round)
1264 .with_line_join(LineJoin::Bevel)
1265 .with_miter_limit(4.0);
1266
1267 assert_eq!(state.line_width, Some(2.5));
1268 assert_eq!(state.line_cap, Some(LineCap::Round));
1269 assert_eq!(state.line_join, Some(LineJoin::Bevel));
1270 assert_eq!(state.miter_limit, Some(4.0));
1271 assert!(!state.is_empty());
1272 }
1273
1274 #[test]
1275 fn test_extgstate_transparency() {
1276 let state = ExtGState::new()
1277 .with_alpha_stroke(0.8)
1278 .with_alpha_fill(0.6)
1279 .with_blend_mode(BlendMode::Multiply);
1280
1281 assert_eq!(state.alpha_stroke, Some(0.8));
1282 assert_eq!(state.alpha_fill, Some(0.6));
1283 assert_eq!(state.blend_mode, Some(BlendMode::Multiply));
1284 assert!(state.uses_transparency());
1285 }
1286
1287 #[test]
1288 fn test_extgstate_alpha_clamping() {
1289 let state = ExtGState::new()
1290 .with_alpha_stroke(1.5) .with_alpha_fill(-0.1); assert_eq!(state.alpha_stroke, Some(1.0));
1294 assert_eq!(state.alpha_fill, Some(0.0));
1295 }
1296
1297 #[test]
1298 fn test_extgstate_combined_alpha() {
1299 let state = ExtGState::new().with_alpha(0.5);
1300
1301 assert_eq!(state.alpha_stroke, Some(0.5));
1302 assert_eq!(state.alpha_fill, Some(0.5));
1303 }
1304
1305 #[test]
1306 fn test_extgstate_rendering_intent() {
1307 let state = ExtGState::new().with_rendering_intent(RenderingIntent::Perceptual);
1308
1309 assert_eq!(state.rendering_intent, Some(RenderingIntent::Perceptual));
1310 }
1311
1312 #[test]
1313 fn test_extgstate_overprint() {
1314 let state = ExtGState::new()
1315 .with_overprint_stroke(true)
1316 .with_overprint_fill(false)
1317 .with_overprint_mode(1);
1318
1319 assert_eq!(state.overprint_stroke, Some(true));
1320 assert_eq!(state.overprint_fill, Some(false));
1321 assert_eq!(state.overprint_mode, Some(1));
1322 }
1323
1324 #[test]
1325 fn test_extgstate_font_setting() {
1326 let state = ExtGState::new().with_font(Font::HelveticaBold, 14.0);
1327
1328 assert!(state.font.is_some());
1329 let font = state.font.unwrap();
1330 assert_eq!(font.font, Font::HelveticaBold);
1331 assert_eq!(font.size, 14.0);
1332 }
1333
1334 #[test]
1335 fn test_extgstate_tolerance_parameters() {
1336 let state = ExtGState::new()
1337 .with_flatness(1.5)
1338 .with_smoothness(0.8)
1339 .with_stroke_adjustment(true);
1340
1341 assert_eq!(state.flatness, Some(1.5));
1342 assert_eq!(state.smoothness, Some(0.8));
1343 assert_eq!(state.stroke_adjustment, Some(true));
1344 }
1345
1346 #[test]
1347 fn test_extgstate_pdf_dictionary_generation() {
1348 let state = ExtGState::new()
1349 .with_line_width(2.0)
1350 .with_line_cap(LineCap::Round)
1351 .with_alpha(0.5)
1352 .with_blend_mode(BlendMode::Multiply);
1353
1354 let dict = state.to_pdf_dictionary().unwrap();
1355 assert!(dict.contains("/Type /ExtGState"));
1356 assert!(dict.contains("/LW 2.000"));
1357 assert!(dict.contains("/LC 1"));
1358 assert!(dict.contains("/CA 0.500"));
1359 assert!(dict.contains("/ca 0.500"));
1360 assert!(dict.contains("/BM /Multiply"));
1361 }
1362
1363 #[test]
1364 fn test_extgstate_manager_creation() {
1365 let manager = ExtGStateManager::new();
1366 assert_eq!(manager.count(), 0);
1367 assert!(manager.states().is_empty());
1368 }
1369
1370 #[test]
1371 fn test_extgstate_manager_add_state() {
1372 let mut manager = ExtGStateManager::new();
1373 let state = ExtGState::new().with_line_width(2.0);
1374
1375 let name = manager.add_state(state).unwrap();
1376 assert_eq!(name, "GS1");
1377 assert_eq!(manager.count(), 1);
1378
1379 let retrieved = manager.get_state(&name).unwrap();
1380 assert_eq!(retrieved.line_width, Some(2.0));
1381 }
1382
1383 #[test]
1384 fn test_extgstate_manager_empty_state_rejection() {
1385 let mut manager = ExtGStateManager::new();
1386 let empty_state = ExtGState::new();
1387
1388 let result = manager.add_state(empty_state);
1389 assert!(result.is_err());
1390 assert_eq!(manager.count(), 0);
1391 }
1392
1393 #[test]
1394 fn test_extgstate_manager_multiple_states() {
1395 let mut manager = ExtGStateManager::new();
1396
1397 let state1 = ExtGState::new().with_line_width(1.0);
1398 let state2 = ExtGState::new().with_alpha(0.5);
1399
1400 let name1 = manager.add_state(state1).unwrap();
1401 let name2 = manager.add_state(state2).unwrap();
1402
1403 assert_eq!(name1, "GS1");
1404 assert_eq!(name2, "GS2");
1405 assert_eq!(manager.count(), 2);
1406 }
1407
1408 #[test]
1409 fn test_extgstate_manager_resource_dictionary() {
1410 let mut manager = ExtGStateManager::new();
1411
1412 let state = ExtGState::new().with_line_width(2.0);
1413 manager.add_state(state).unwrap();
1414
1415 let dict = manager.to_resource_dictionary().unwrap();
1416 assert!(dict.contains("/ExtGState"));
1417 assert!(dict.contains("/GS1"));
1418 assert!(dict.contains("/LW 2.000"));
1419 }
1420
1421 #[test]
1422 fn test_extgstate_manager_clear() {
1423 let mut manager = ExtGStateManager::new();
1424
1425 let state = ExtGState::new().with_line_width(1.0);
1426 manager.add_state(state).unwrap();
1427 assert_eq!(manager.count(), 1);
1428
1429 manager.clear();
1430 assert_eq!(manager.count(), 0);
1431 assert!(manager.states().is_empty());
1432 }
1433
1434 #[test]
1435 fn test_extgstate_value_validation() {
1436 let state = ExtGState::new().with_line_width(-1.0);
1438 assert_eq!(state.line_width, Some(0.0));
1439
1440 let state = ExtGState::new().with_miter_limit(0.5);
1442 assert_eq!(state.miter_limit, Some(1.0));
1443
1444 let state = ExtGState::new().with_flatness(150.0);
1446 assert_eq!(state.flatness, Some(100.0));
1447
1448 let state = ExtGState::new().with_smoothness(1.5);
1450 assert_eq!(state.smoothness, Some(1.0));
1451
1452 let state = ExtGState::new().with_font(Font::Helvetica, -5.0);
1454 assert_eq!(state.font.unwrap().size, 0.0);
1455 }
1456
1457 #[test]
1458 fn test_line_dash_patterns() {
1459 let state = ExtGState::new().with_dash_pattern(LineDashPattern::dashed(10.0, 5.0));
1460
1461 let dict = state.to_pdf_dictionary().unwrap();
1462 assert!(dict.contains("/D [10.00 5.00] 0.00"));
1463 }
1464
1465 #[test]
1466 fn test_complex_extgstate() {
1467 let dash_pattern = LineDashPattern::new(vec![3.0, 2.0, 1.0, 2.0], 1.0);
1468
1469 let state = ExtGState::new()
1470 .with_line_width(1.5)
1471 .with_line_cap(LineCap::Square)
1472 .with_line_join(LineJoin::Round)
1473 .with_miter_limit(10.0)
1474 .with_dash_pattern(dash_pattern)
1475 .with_rendering_intent(RenderingIntent::Saturation)
1476 .with_overprint_stroke(true)
1477 .with_overprint_fill(false)
1478 .with_font(Font::TimesBold, 18.0)
1479 .with_flatness(0.5)
1480 .with_smoothness(0.1)
1481 .with_stroke_adjustment(false)
1482 .with_blend_mode(BlendMode::SoftLight)
1483 .with_alpha_stroke(0.8)
1484 .with_alpha_fill(0.6)
1485 .with_alpha_is_shape(true)
1486 .with_text_knockout(false);
1487
1488 assert!(!state.is_empty());
1489 assert!(state.uses_transparency());
1490
1491 let dict = state.to_pdf_dictionary().unwrap();
1492 assert!(dict.contains("/Type /ExtGState"));
1493 assert!(dict.contains("/LW 1.500"));
1494 assert!(dict.contains("/LC 2"));
1495 assert!(dict.contains("/LJ 1"));
1496 assert!(dict.contains("/ML 10.000"));
1497 assert!(dict.contains("/D [3.00 2.00 1.00 2.00] 1.00"));
1498 assert!(dict.contains("/RI /Saturation"));
1499 assert!(dict.contains("/OP true"));
1500 assert!(dict.contains("/op false"));
1501 assert!(dict.contains("/Font [/Times-Bold 18.000]"));
1502 assert!(dict.contains("/FL 0.500"));
1503 assert!(dict.contains("/SM 0.100"));
1504 assert!(dict.contains("/SA false"));
1505 assert!(dict.contains("/BM /SoftLight"));
1506 assert!(dict.contains("/CA 0.800"));
1507 assert!(dict.contains("/ca 0.600"));
1508 assert!(dict.contains("/AIS true"));
1509 assert!(dict.contains("/TK false"));
1510 }
1511
1512 #[test]
1513 fn test_transfer_function_identity() {
1514 let tf = TransferFunction::identity();
1515 assert_eq!(tf.to_pdf_string(), "/Identity");
1516 }
1517
1518 #[test]
1519 fn test_transfer_function_gamma() {
1520 let tf = TransferFunction::gamma(2.2);
1521 let pdf = tf.to_pdf_string();
1522 assert!(pdf.contains("/FunctionType 2"));
1523 assert!(pdf.contains("/N 2.200"));
1524 assert!(pdf.contains("/Domain [0.000 1.000]"));
1525 assert!(pdf.contains("/Range [0.000 1.000]"));
1526 assert!(pdf.contains("/C0 [0.000]"));
1527 assert!(pdf.contains("/C1 [1.000]"));
1528 }
1529
1530 #[test]
1531 fn test_transfer_function_linear() {
1532 let tf = TransferFunction::linear(0.8, 0.1);
1533 let pdf = tf.to_pdf_string();
1534 assert!(pdf.contains("/FunctionType 2"));
1535 assert!(pdf.contains("/N 1.000"));
1536 assert!(pdf.contains("/C0 [0.100]")); assert!(pdf.contains("/C1 [0.900]")); }
1539
1540 #[test]
1541 fn test_extgstate_with_transfer_functions() {
1542 let state = ExtGState::new()
1543 .with_gamma_correction(1.8)
1544 .with_transfer_function_2(TransferFunction::identity())
1545 .with_black_generation(TransferFunction::linear(1.0, 0.0))
1546 .with_undercolor_removal(TransferFunction::gamma(2.2));
1547
1548 assert!(!state.is_empty());
1549
1550 let dict = state.to_pdf_dictionary().unwrap();
1551 assert!(dict.contains("/TR"));
1552 assert!(dict.contains("/TR2 /Identity"));
1553 assert!(dict.contains("/BG"));
1554 assert!(dict.contains("/UCR"));
1555 assert!(dict.contains("/N 1.800")); assert!(dict.contains("/N 2.200")); }
1558
1559 #[test]
1560 fn test_transfer_function_separate() {
1561 let c_func = TransferFunctionData {
1562 function_type: 2,
1563 domain: vec![0.0, 1.0],
1564 range: vec![0.0, 1.0],
1565 params: TransferFunctionParams::Exponential {
1566 c0: vec![0.0],
1567 c1: vec![1.0],
1568 n: 1.5,
1569 },
1570 };
1571
1572 let m_func = c_func.clone();
1573 let y_func = c_func.clone();
1574 let k_func = Some(TransferFunctionData {
1575 function_type: 2,
1576 domain: vec![0.0, 1.0],
1577 range: vec![0.0, 1.0],
1578 params: TransferFunctionParams::Exponential {
1579 c0: vec![0.1],
1580 c1: vec![0.9],
1581 n: 2.0,
1582 },
1583 });
1584
1585 let tf = TransferFunction::Separate {
1586 c_or_r: c_func,
1587 m_or_g: m_func,
1588 y_or_b: y_func,
1589 k: k_func,
1590 };
1591
1592 let pdf = tf.to_pdf_string();
1593 assert!(pdf.starts_with('['));
1594 assert!(pdf.ends_with(']'));
1595 assert!(pdf.contains("/FunctionType 2"));
1596 assert_eq!(pdf.matches("/FunctionType 2").count(), 4);
1598 }
1599
1600 #[test]
1608 fn nan_dash_array_component_sanitised_at_emission() {
1609 let pattern = LineDashPattern {
1610 array: vec![5.0, f64::NAN, 3.0],
1611 phase: 0.0,
1612 };
1613 let s = pattern.to_pdf_string();
1614 assert!(
1615 !s.contains("NaN") && !s.contains("inf"),
1616 "non-finite tokens must not appear in `d` operator, got: {s:?}"
1617 );
1618 assert_eq!(
1619 s, "[5.00 0.00 3.00] 0.00",
1620 "NaN dash component must clamp to 0.00, got: {s:?}"
1621 );
1622 }
1623
1624 #[test]
1625 fn pos_inf_dash_phase_sanitised_at_emission() {
1626 let pattern = LineDashPattern {
1627 array: vec![5.0, 3.0],
1628 phase: f64::INFINITY,
1629 };
1630 let s = pattern.to_pdf_string();
1631 assert!(
1632 !s.contains("inf") && !s.contains("NaN"),
1633 "non-finite phase must not appear in `d` operator, got: {s:?}"
1634 );
1635 assert_eq!(
1636 s, "[5.00 3.00] 0.00",
1637 "+inf phase must clamp to 0.00, got: {s:?}"
1638 );
1639 }
1640}