1use crate::error::{PdfError, Result};
10use crate::graphics::Color;
11use crate::objects::{Dictionary, Object};
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum ShadingType {
17 FunctionBased = 1,
19 Axial = 2,
21 Radial = 3,
23 FreeFormGouraud = 4,
25 LatticeFormGouraud = 5,
27 CoonsPatch = 6,
29 TensorProductPatch = 7,
31}
32
33#[derive(Debug, Clone, PartialEq)]
35pub struct ColorStop {
36 pub position: f64,
38 pub color: Color,
40}
41
42impl ColorStop {
43 pub fn new(position: f64, color: Color) -> Self {
45 Self {
46 position: position.clamp(0.0, 1.0),
47 color,
48 }
49 }
50}
51
52fn resolve_color_space(stops: &[ColorStop]) -> &'static str {
60 match stops.first() {
61 Some(first) => {
62 let name = first.color.color_space_name();
63 if stops.iter().all(|s| s.color.color_space_name() == name) {
64 name
65 } else {
66 "DeviceRGB"
67 }
68 }
69 None => "DeviceRGB",
70 }
71}
72
73fn color_components(color: &Color, space: &str) -> Vec<f64> {
75 match space {
76 "DeviceGray" => vec![match color {
77 Color::Gray(g) => *g,
78 other => {
82 unreachable!("color_components(DeviceGray) called with non-Gray color: {other:?}")
83 }
84 }],
85 "DeviceCMYK" => {
86 let (c, m, y, k) = color.cmyk_components();
87 vec![c, m, y, k]
88 }
89 _ => match color.to_rgb() {
94 Color::Rgb(r, g, b) => vec![r, g, b],
95 _ => unreachable!("to_rgb always yields Color::Rgb"),
96 },
97 }
98}
99
100fn type2_function(c0: &Color, c1: &Color, space: &str) -> Dictionary {
106 let mut dict = Dictionary::new();
107 dict.set("FunctionType", Object::Integer(2));
108 dict.set(
109 "Domain",
110 Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
111 );
112 dict.set(
113 "C0",
114 Object::Array(
115 color_components(c0, space)
116 .into_iter()
117 .map(Object::Real)
118 .collect(),
119 ),
120 );
121 dict.set(
122 "C1",
123 Object::Array(
124 color_components(c1, space)
125 .into_iter()
126 .map(Object::Real)
127 .collect(),
128 ),
129 );
130 dict.set("N", Object::Real(1.0));
131 dict
132}
133
134fn build_color_function(stops: &[ColorStop], space: &str) -> Result<Dictionary> {
142 match stops {
143 [] => Err(PdfError::InvalidStructure(
144 "Shading must have at least one color stop".to_string(),
145 )),
146 [only] => Ok(type2_function(&only.color, &only.color, space)),
147 [a, b] => Ok(type2_function(&a.color, &b.color, space)),
148 _ => {
149 let subfunctions: Vec<Object> = stops
150 .windows(2)
151 .map(|w| Object::Dictionary(type2_function(&w[0].color, &w[1].color, space)))
152 .collect();
153
154 let bounds: Vec<Object> = stops[1..stops.len() - 1]
156 .iter()
157 .map(|s| Object::Real(s.position))
158 .collect();
159
160 let encode: Vec<Object> = (0..subfunctions.len())
162 .flat_map(|_| [Object::Real(0.0), Object::Real(1.0)])
163 .collect();
164
165 let mut dict = Dictionary::new();
166 dict.set("FunctionType", Object::Integer(3));
167 dict.set(
168 "Domain",
169 Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
170 );
171 dict.set("Functions", Object::Array(subfunctions));
172 dict.set("Bounds", Object::Array(bounds));
173 dict.set("Encode", Object::Array(encode));
174 Ok(dict)
175 }
176 }
177}
178
179fn assemble_gradient_dict(
184 shading_type: ShadingType,
185 coords: Vec<Object>,
186 stops: &[ColorStop],
187 extend_start: bool,
188 extend_end: bool,
189) -> Result<Dictionary> {
190 let space = resolve_color_space(stops);
191 let function = build_color_function(stops, space)?;
192
193 let mut dict = Dictionary::new();
194 dict.set("ShadingType", Object::Integer(shading_type as i64));
195 dict.set("ColorSpace", Object::Name(space.to_string()));
196 dict.set("Coords", Object::Array(coords));
197 dict.set(
198 "Domain",
199 Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
200 );
201 dict.set("Function", Object::Dictionary(function));
202 dict.set(
203 "Extend",
204 Object::Array(vec![
205 Object::Boolean(extend_start),
206 Object::Boolean(extend_end),
207 ]),
208 );
209 Ok(dict)
210}
211
212struct BitWriter {
216 buffer: Vec<u8>,
217 current_byte: u8,
218 bits_filled: u8,
219}
220
221impl BitWriter {
222 fn new() -> Self {
223 Self {
224 buffer: Vec::new(),
225 current_byte: 0,
226 bits_filled: 0,
227 }
228 }
229
230 fn write_bits(&mut self, value: u64, bits: u8) {
232 for i in (0..bits).rev() {
233 let bit = ((value >> i) & 1) as u8;
234 self.current_byte = (self.current_byte << 1) | bit;
235 self.bits_filled += 1;
236 if self.bits_filled == 8 {
237 self.buffer.push(self.current_byte);
238 self.current_byte = 0;
239 self.bits_filled = 0;
240 }
241 }
242 }
243
244 fn align_to_byte(&mut self) {
246 if self.bits_filled > 0 {
247 self.current_byte <<= 8 - self.bits_filled;
248 self.buffer.push(self.current_byte);
249 self.current_byte = 0;
250 self.bits_filled = 0;
251 }
252 }
253
254 fn into_bytes(self) -> Vec<u8> {
255 self.buffer
256 }
257}
258
259fn encode_value(value: f64, min: f64, max: f64, bits: u8) -> u64 {
264 let span = max - min;
265 let frac = if span == 0.0 {
266 0.0
267 } else {
268 ((value.clamp(min, max) - min) / span).clamp(0.0, 1.0)
269 };
270 let max_int = (1u64 << bits) - 1;
271 (frac * max_int as f64).round() as u64
272}
273
274#[derive(Debug, Clone, PartialEq)]
278pub struct GouraudVertex {
279 pub flag: u8,
281 pub x: f64,
283 pub y: f64,
285 pub color: Color,
287}
288
289fn pack_vertex(
294 vertex: &GouraudVertex,
295 bits_per_flag: u8,
296 bits_per_coordinate: u8,
297 bits_per_component: u8,
298 decode: &[f64],
299 color_space: &str,
300) -> Vec<u8> {
301 let mut w = BitWriter::new();
302 w.write_bits(vertex.flag as u64, bits_per_flag);
303 w.write_bits(
304 encode_value(vertex.x, decode[0], decode[1], bits_per_coordinate),
305 bits_per_coordinate,
306 );
307 w.write_bits(
308 encode_value(vertex.y, decode[2], decode[3], bits_per_coordinate),
309 bits_per_coordinate,
310 );
311 for (i, comp) in color_components(&vertex.color, color_space)
312 .into_iter()
313 .enumerate()
314 {
315 let lo = decode[4 + 2 * i];
316 let hi = decode[4 + 2 * i + 1];
317 w.write_bits(
318 encode_value(comp, lo, hi, bits_per_component),
319 bits_per_component,
320 );
321 }
322 w.align_to_byte();
323 w.into_bytes()
324}
325
326fn n_components(color_space: &str) -> usize {
329 match color_space {
330 "DeviceGray" => 1,
331 "DeviceCMYK" => 4,
332 _ => 3,
334 }
335}
336
337#[derive(Debug, Clone)]
347#[non_exhaustive]
348pub struct FreeFormGouraudShading {
349 pub name: String,
351 pub color_space: String,
353 pub bits_per_coordinate: u8,
355 pub bits_per_component: u8,
357 pub bits_per_flag: u8,
359 pub decode: Vec<f64>,
361 pub vertices: Vec<GouraudVertex>,
363}
364
365impl FreeFormGouraudShading {
366 pub fn new(
370 name: impl Into<String>,
371 color_space: impl Into<String>,
372 decode: Vec<f64>,
373 vertices: Vec<GouraudVertex>,
374 ) -> Self {
375 Self {
376 name: name.into(),
377 color_space: color_space.into(),
378 bits_per_coordinate: 16,
379 bits_per_component: 8,
380 bits_per_flag: 8,
381 decode,
382 vertices,
383 }
384 }
385
386 pub fn with_bits(
388 mut self,
389 bits_per_coordinate: u8,
390 bits_per_component: u8,
391 bits_per_flag: u8,
392 ) -> Self {
393 self.bits_per_coordinate = bits_per_coordinate;
394 self.bits_per_component = bits_per_component;
395 self.bits_per_flag = bits_per_flag;
396 self
397 }
398
399 pub fn validate(&self) -> Result<()> {
403 if !matches!(self.bits_per_coordinate, 1 | 2 | 4 | 8 | 12 | 16 | 24 | 32) {
404 return Err(PdfError::InvalidStructure(format!(
405 "BitsPerCoordinate must be 1,2,4,8,12,16,24 or 32, got {}",
406 self.bits_per_coordinate
407 )));
408 }
409 if !matches!(self.bits_per_component, 1 | 2 | 4 | 8 | 12 | 16) {
410 return Err(PdfError::InvalidStructure(format!(
411 "BitsPerComponent must be 1,2,4,8,12 or 16, got {}",
412 self.bits_per_component
413 )));
414 }
415 if !matches!(self.bits_per_flag, 2 | 4 | 8) {
416 return Err(PdfError::InvalidStructure(format!(
417 "BitsPerFlag must be 2, 4 or 8, got {}",
418 self.bits_per_flag
419 )));
420 }
421 let expected = 4 + 2 * n_components(&self.color_space);
422 if self.decode.len() != expected {
423 return Err(PdfError::InvalidStructure(format!(
424 "Decode must have {} entries for {}, got {}",
425 expected,
426 self.color_space,
427 self.decode.len()
428 )));
429 }
430 for pair in self.decode.chunks_exact(2) {
434 if pair[0] > pair[1] {
435 return Err(PdfError::InvalidStructure(format!(
436 "Decode ranges must be non-decreasing, got [{}, {}]",
437 pair[0], pair[1]
438 )));
439 }
440 }
441 if self.vertices.is_empty() {
442 return Err(PdfError::InvalidStructure(
443 "Mesh shading must have at least one vertex".to_string(),
444 ));
445 }
446 if self.vertices[0].flag != 0 {
447 return Err(PdfError::InvalidStructure(
448 "First mesh vertex must have edge flag 0".to_string(),
449 ));
450 }
451 let gray_space = self.color_space == "DeviceGray";
456 for (i, v) in self.vertices.iter().enumerate() {
457 if v.flag > 2 {
458 return Err(PdfError::InvalidStructure(format!(
459 "Vertex {i} edge flag must be 0, 1 or 2, got {}",
460 v.flag
461 )));
462 }
463 if gray_space && !matches!(v.color, Color::Gray(_)) {
464 return Err(PdfError::InvalidStructure(format!(
465 "Vertex {i} color must be Color::Gray to match DeviceGray, got {:?}",
466 v.color
467 )));
468 }
469 }
470 Ok(())
471 }
472
473 pub fn to_pdf_object(&self) -> Result<Object> {
478 self.validate()?;
479
480 let mut dict = Dictionary::new();
481 dict.set(
482 "ShadingType",
483 Object::Integer(ShadingType::FreeFormGouraud as i64),
484 );
485 dict.set("ColorSpace", Object::Name(self.color_space.clone()));
486 dict.set(
487 "BitsPerCoordinate",
488 Object::Integer(self.bits_per_coordinate as i64),
489 );
490 dict.set(
491 "BitsPerComponent",
492 Object::Integer(self.bits_per_component as i64),
493 );
494 dict.set("BitsPerFlag", Object::Integer(self.bits_per_flag as i64));
495 dict.set(
496 "Decode",
497 Object::Array(self.decode.iter().map(|&d| Object::Real(d)).collect()),
498 );
499
500 let mut data = Vec::new();
501 for v in &self.vertices {
502 data.extend(pack_vertex(
503 v,
504 self.bits_per_flag,
505 self.bits_per_coordinate,
506 self.bits_per_component,
507 &self.decode,
508 &self.color_space,
509 ));
510 }
511
512 Ok(Object::Stream(dict, data))
513 }
514}
515
516fn postscript_type4_function(code: &str, domain: &[f64], range: &[f64]) -> (Dictionary, Vec<u8>) {
522 let mut dict = Dictionary::new();
523 dict.set("FunctionType", Object::Integer(4));
524 dict.set(
525 "Domain",
526 Object::Array(domain.iter().map(|&d| Object::Real(d)).collect()),
527 );
528 dict.set(
529 "Range",
530 Object::Array(range.iter().map(|&r| Object::Real(r)).collect()),
531 );
532 (dict, code.as_bytes().to_vec())
533}
534
535fn ramp2_ps(start: &Color, end: &Color, space: &str) -> String {
539 let s = color_components(start, space);
540 let e = color_components(end, space);
541 let n = s.len();
542 let mut parts = Vec::with_capacity(n);
543 for j in 0..n {
544 let block = format!("{} mul {} add", e[j] - s[j], s[j]);
545 if j + 1 < n {
546 parts.push(format!("dup {block} exch"));
549 } else {
550 parts.push(block);
551 }
552 }
553 parts.join(" ")
554}
555
556fn remap_local_ps(lo: f64, hi: f64) -> String {
559 format!("{} sub {} div", lo, hi - lo)
560}
561
562fn build_color_ramp_ps(stops: &[ColorStop], space: &str) -> String {
567 match stops {
568 [] => String::new(),
569 [only] => {
570 let mut s = String::from("pop");
571 for c in color_components(&only.color, space) {
572 s.push_str(&format!(" {c}"));
573 }
574 s
575 }
576 [a, b] => ramp2_ps(&a.color, &b.color, space),
577 _ => build_ramp_nested_ps(stops, space),
578 }
579}
580
581fn build_ramp_nested_ps(stops: &[ColorStop], space: &str) -> String {
585 debug_assert!(stops.len() >= 2);
586 let lo = stops[0].position;
587 let hi = stops[1].position;
588 let seg0 = format!(
589 "{} {}",
590 remap_local_ps(lo, hi),
591 ramp2_ps(&stops[0].color, &stops[1].color, space)
592 );
593 if stops.len() == 2 {
594 return seg0;
595 }
596 let split = stops[1].position;
597 let rest = build_ramp_nested_ps(&stops[1..], space);
598 format!("dup {split} lt {{ {seg0} }} {{ {rest} }} ifelse")
599}
600
601fn build_conic_angle_prologue(center: Point) -> String {
607 format!("{} sub exch {} sub atan 360 div", center.y, center.x)
609}
610
611#[derive(Debug, Clone)]
620#[non_exhaustive]
621pub struct ConicShading {
622 pub name: String,
624 pub center: Point,
626 pub domain: [f64; 4],
628 pub matrix: Option<[f64; 6]>,
630 pub color_stops: Vec<ColorStop>,
632}
633
634impl ConicShading {
635 pub fn new(
637 name: impl Into<String>,
638 center: Point,
639 domain: [f64; 4],
640 color_stops: Vec<ColorStop>,
641 ) -> Self {
642 Self {
643 name: name.into(),
644 center,
645 domain,
646 matrix: None,
647 color_stops,
648 }
649 }
650
651 pub fn with_matrix(mut self, matrix: [f64; 6]) -> Self {
653 self.matrix = Some(matrix);
654 self
655 }
656
657 pub fn validate(&self) -> Result<()> {
659 if self.color_stops.is_empty() {
660 return Err(PdfError::InvalidStructure(
661 "Conic shading must have at least one color stop".to_string(),
662 ));
663 }
664 if self.domain[0] >= self.domain[1] || self.domain[2] >= self.domain[3] {
665 return Err(PdfError::InvalidStructure(
666 "Invalid domain: min values must be less than max values".to_string(),
667 ));
668 }
669 for window in self.color_stops.windows(2) {
672 if window[0].position >= window[1].position {
673 return Err(PdfError::InvalidStructure(
674 "Color stops must be in strictly ascending order".to_string(),
675 ));
676 }
677 }
678 if self.color_stops.len() >= 2 {
684 let first = self.color_stops[0].position;
685 let last = self.color_stops[self.color_stops.len() - 1].position;
686 if first != 0.0 || last != 1.0 {
687 return Err(PdfError::InvalidStructure(format!(
688 "Conic color stops must span [0.0, 1.0]; first={first}, last={last}"
689 )));
690 }
691 }
692 Ok(())
693 }
694
695 pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
700 self.validate()?;
701 let space = resolve_color_space(&self.color_stops);
702 let code = format!(
703 "{{ {} {} }}",
704 build_conic_angle_prologue(self.center),
705 build_color_ramp_ps(&self.color_stops, space)
706 );
707 let range: Vec<f64> = (0..n_components(space)).flat_map(|_| [0.0, 1.0]).collect();
708 let (fdict, fbytes) = postscript_type4_function(&code, &self.domain, &range);
709
710 let mut dict = Dictionary::new();
711 dict.set(
712 "ShadingType",
713 Object::Integer(ShadingType::FunctionBased as i64),
714 );
715 dict.set("ColorSpace", Object::Name(space.to_string()));
716 dict.set(
717 "Domain",
718 Object::Array(self.domain.iter().map(|&d| Object::Real(d)).collect()),
719 );
720 dict.set("Function", Object::Stream(fdict, fbytes));
721 if let Some(matrix) = self.matrix {
722 dict.set(
723 "Matrix",
724 Object::Array(matrix.iter().map(|&v| Object::Real(v)).collect()),
725 );
726 }
727 Ok(dict)
728 }
729}
730
731#[derive(Debug, Clone)]
737pub(crate) enum AdvancedShading {
738 Mesh(FreeFormGouraudShading),
740 Conic(ConicShading),
743}
744
745impl AdvancedShading {
746 pub(crate) fn to_pdf_object(&self) -> Result<Object> {
749 match self {
750 AdvancedShading::Mesh(m) => m.to_pdf_object(),
751 AdvancedShading::Conic(c) => Ok(Object::Dictionary(c.to_pdf_dictionary()?)),
752 }
753 }
754}
755
756#[derive(Debug, Clone, Copy, PartialEq)]
758pub struct Point {
759 pub x: f64,
760 pub y: f64,
761}
762
763impl Point {
764 pub fn new(x: f64, y: f64) -> Self {
766 Self { x, y }
767 }
768}
769
770#[derive(Debug, Clone)]
772pub struct AxialShading {
773 pub name: String,
775 pub start_point: Point,
777 pub end_point: Point,
779 pub color_stops: Vec<ColorStop>,
781 pub extend_start: bool,
783 pub extend_end: bool,
785}
786
787impl AxialShading {
788 pub fn new(
790 name: String,
791 start_point: Point,
792 end_point: Point,
793 color_stops: Vec<ColorStop>,
794 ) -> Self {
795 Self {
796 name,
797 start_point,
798 end_point,
799 color_stops,
800 extend_start: false,
801 extend_end: false,
802 }
803 }
804
805 pub fn with_extend(mut self, extend_start: bool, extend_end: bool) -> Self {
807 self.extend_start = extend_start;
808 self.extend_end = extend_end;
809 self
810 }
811
812 pub fn linear_gradient(
814 name: String,
815 start_point: Point,
816 end_point: Point,
817 start_color: Color,
818 end_color: Color,
819 ) -> Self {
820 let color_stops = vec![
821 ColorStop::new(0.0, start_color),
822 ColorStop::new(1.0, end_color),
823 ];
824
825 Self::new(name, start_point, end_point, color_stops)
826 }
827
828 pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
834 let coords = vec![
835 Object::Real(self.start_point.x),
836 Object::Real(self.start_point.y),
837 Object::Real(self.end_point.x),
838 Object::Real(self.end_point.y),
839 ];
840 assemble_gradient_dict(
841 ShadingType::Axial,
842 coords,
843 &self.color_stops,
844 self.extend_start,
845 self.extend_end,
846 )
847 }
848
849 pub fn validate(&self) -> Result<()> {
851 if self.color_stops.is_empty() {
852 return Err(PdfError::InvalidStructure(
853 "Axial shading must have at least one color stop".to_string(),
854 ));
855 }
856
857 for window in self.color_stops.windows(2) {
859 if window[0].position > window[1].position {
860 return Err(PdfError::InvalidStructure(
861 "Color stops must be in ascending order".to_string(),
862 ));
863 }
864 }
865
866 if (self.start_point.x - self.end_point.x).abs() < f64::EPSILON
868 && (self.start_point.y - self.end_point.y).abs() < f64::EPSILON
869 {
870 return Err(PdfError::InvalidStructure(
871 "Start and end points cannot be the same".to_string(),
872 ));
873 }
874
875 Ok(())
876 }
877}
878
879#[derive(Debug, Clone)]
881pub struct RadialShading {
882 pub name: String,
884 pub start_center: Point,
886 pub start_radius: f64,
888 pub end_center: Point,
890 pub end_radius: f64,
892 pub color_stops: Vec<ColorStop>,
894 pub extend_start: bool,
896 pub extend_end: bool,
898}
899
900impl RadialShading {
901 pub fn new(
903 name: String,
904 start_center: Point,
905 start_radius: f64,
906 end_center: Point,
907 end_radius: f64,
908 color_stops: Vec<ColorStop>,
909 ) -> Self {
910 Self {
911 name,
912 start_center,
913 start_radius: start_radius.max(0.0),
914 end_center,
915 end_radius: end_radius.max(0.0),
916 color_stops,
917 extend_start: false,
918 extend_end: false,
919 }
920 }
921
922 pub fn with_extend(mut self, extend_start: bool, extend_end: bool) -> Self {
924 self.extend_start = extend_start;
925 self.extend_end = extend_end;
926 self
927 }
928
929 pub fn radial_gradient(
931 name: String,
932 center: Point,
933 start_radius: f64,
934 end_radius: f64,
935 start_color: Color,
936 end_color: Color,
937 ) -> Self {
938 let color_stops = vec![
939 ColorStop::new(0.0, start_color),
940 ColorStop::new(1.0, end_color),
941 ];
942
943 Self::new(name, center, start_radius, center, end_radius, color_stops)
944 }
945
946 pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
952 let coords = vec![
953 Object::Real(self.start_center.x),
954 Object::Real(self.start_center.y),
955 Object::Real(self.start_radius),
956 Object::Real(self.end_center.x),
957 Object::Real(self.end_center.y),
958 Object::Real(self.end_radius),
959 ];
960 assemble_gradient_dict(
961 ShadingType::Radial,
962 coords,
963 &self.color_stops,
964 self.extend_start,
965 self.extend_end,
966 )
967 }
968
969 pub fn validate(&self) -> Result<()> {
971 if self.color_stops.is_empty() {
972 return Err(PdfError::InvalidStructure(
973 "Radial shading must have at least one color stop".to_string(),
974 ));
975 }
976
977 for window in self.color_stops.windows(2) {
979 if window[0].position > window[1].position {
980 return Err(PdfError::InvalidStructure(
981 "Color stops must be in ascending order".to_string(),
982 ));
983 }
984 }
985
986 if self.start_radius < 0.0 || self.end_radius < 0.0 {
988 return Err(PdfError::InvalidStructure(
989 "Radii cannot be negative".to_string(),
990 ));
991 }
992
993 Ok(())
994 }
995}
996
997#[derive(Debug, Clone)]
999pub struct FunctionBasedShading {
1000 pub name: String,
1002 pub domain: [f64; 4],
1004 pub matrix: Option<[f64; 6]>,
1006 pub function_id: u32,
1008}
1009
1010impl FunctionBasedShading {
1011 pub fn new(name: String, domain: [f64; 4], function_id: u32) -> Self {
1013 Self {
1014 name,
1015 domain,
1016 matrix: None,
1017 function_id,
1018 }
1019 }
1020
1021 pub fn with_matrix(mut self, matrix: [f64; 6]) -> Self {
1023 self.matrix = Some(matrix);
1024 self
1025 }
1026
1027 pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
1029 let mut shading_dict = Dictionary::new();
1030
1031 shading_dict.set(
1033 "ShadingType",
1034 Object::Integer(ShadingType::FunctionBased as i64),
1035 );
1036
1037 let domain = vec![
1039 Object::Real(self.domain[0]),
1040 Object::Real(self.domain[1]),
1041 Object::Real(self.domain[2]),
1042 Object::Real(self.domain[3]),
1043 ];
1044 shading_dict.set("Domain", Object::Array(domain));
1045
1046 if let Some(matrix) = self.matrix {
1048 let matrix_objects: Vec<Object> = matrix.iter().map(|&x| Object::Real(x)).collect();
1049 shading_dict.set("Matrix", Object::Array(matrix_objects));
1050 }
1051
1052 shading_dict.set("Function", Object::Integer(self.function_id as i64));
1054
1055 Ok(shading_dict)
1056 }
1057
1058 pub fn validate(&self) -> Result<()> {
1060 if self.domain[0] >= self.domain[1] || self.domain[2] >= self.domain[3] {
1062 return Err(PdfError::InvalidStructure(
1063 "Invalid domain: min values must be less than max values".to_string(),
1064 ));
1065 }
1066
1067 Ok(())
1068 }
1069}
1070
1071#[derive(Debug, Clone)]
1073pub struct ShadingPattern {
1074 pub name: String,
1076 pub shading: ShadingDefinition,
1078 pub matrix: Option<[f64; 6]>,
1080}
1081
1082#[derive(Debug, Clone)]
1084pub enum ShadingDefinition {
1085 Axial(AxialShading),
1087 Radial(RadialShading),
1089 FunctionBased(FunctionBasedShading),
1091}
1092
1093impl ShadingDefinition {
1094 pub fn name(&self) -> &str {
1096 match self {
1097 ShadingDefinition::Axial(shading) => &shading.name,
1098 ShadingDefinition::Radial(shading) => &shading.name,
1099 ShadingDefinition::FunctionBased(shading) => &shading.name,
1100 }
1101 }
1102
1103 pub fn validate(&self) -> Result<()> {
1105 match self {
1106 ShadingDefinition::Axial(shading) => shading.validate(),
1107 ShadingDefinition::Radial(shading) => shading.validate(),
1108 ShadingDefinition::FunctionBased(shading) => shading.validate(),
1109 }
1110 }
1111
1112 pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
1114 match self {
1115 ShadingDefinition::Axial(shading) => shading.to_pdf_dictionary(),
1116 ShadingDefinition::Radial(shading) => shading.to_pdf_dictionary(),
1117 ShadingDefinition::FunctionBased(shading) => shading.to_pdf_dictionary(),
1118 }
1119 }
1120}
1121
1122impl ShadingPattern {
1123 pub fn new(name: String, shading: ShadingDefinition) -> Self {
1125 Self {
1126 name,
1127 shading,
1128 matrix: None,
1129 }
1130 }
1131
1132 pub fn with_matrix(mut self, matrix: [f64; 6]) -> Self {
1134 self.matrix = Some(matrix);
1135 self
1136 }
1137
1138 pub fn to_pdf_pattern_dictionary(&self) -> Result<Dictionary> {
1150 let mut pattern_dict = Dictionary::new();
1151
1152 pattern_dict.set("Type", Object::Name("Pattern".to_string()));
1154 pattern_dict.set("PatternType", Object::Integer(2)); pattern_dict.set(
1161 "Shading",
1162 Object::Dictionary(self.shading.to_pdf_dictionary()?),
1163 );
1164
1165 if let Some(matrix) = self.matrix {
1167 let matrix_objects: Vec<Object> = matrix.iter().map(|&x| Object::Real(x)).collect();
1168 pattern_dict.set("Matrix", Object::Array(matrix_objects));
1169 }
1170
1171 Ok(pattern_dict)
1172 }
1173
1174 pub fn validate(&self) -> Result<()> {
1176 self.shading.validate()
1177 }
1178}
1179
1180#[derive(Debug, Clone)]
1182pub struct ShadingManager {
1183 shadings: HashMap<String, ShadingDefinition>,
1185 patterns: HashMap<String, ShadingPattern>,
1187 next_id: usize,
1189}
1190
1191impl Default for ShadingManager {
1192 fn default() -> Self {
1193 Self::new()
1194 }
1195}
1196
1197impl ShadingManager {
1198 pub fn new() -> Self {
1200 Self {
1201 shadings: HashMap::new(),
1202 patterns: HashMap::new(),
1203 next_id: 1,
1204 }
1205 }
1206
1207 pub fn add_shading(&mut self, mut shading: ShadingDefinition) -> Result<String> {
1209 shading.validate()?;
1211
1212 let name = shading.name().to_string();
1213
1214 let final_name = if name.is_empty() || self.shadings.contains_key(&name) {
1216 let auto_name = format!("Sh{}", self.next_id);
1217 self.next_id += 1;
1218
1219 match &mut shading {
1221 ShadingDefinition::Axial(s) => s.name = auto_name.clone(),
1222 ShadingDefinition::Radial(s) => s.name = auto_name.clone(),
1223 ShadingDefinition::FunctionBased(s) => s.name = auto_name.clone(),
1224 }
1225
1226 auto_name
1227 } else {
1228 name
1229 };
1230
1231 self.shadings.insert(final_name.clone(), shading);
1232 Ok(final_name)
1233 }
1234
1235 pub fn add_shading_pattern(&mut self, mut pattern: ShadingPattern) -> Result<String> {
1237 pattern.validate()?;
1239
1240 if pattern.name.is_empty() || self.patterns.contains_key(&pattern.name) {
1242 pattern.name = format!("SP{}", self.next_id);
1243 self.next_id += 1;
1244 }
1245
1246 let name = pattern.name.clone();
1247 self.patterns.insert(name.clone(), pattern);
1248 Ok(name)
1249 }
1250
1251 pub fn get_shading(&self, name: &str) -> Option<&ShadingDefinition> {
1253 self.shadings.get(name)
1254 }
1255
1256 pub fn get_pattern(&self, name: &str) -> Option<&ShadingPattern> {
1258 self.patterns.get(name)
1259 }
1260
1261 pub fn shadings(&self) -> &HashMap<String, ShadingDefinition> {
1263 &self.shadings
1264 }
1265
1266 pub fn patterns(&self) -> &HashMap<String, ShadingPattern> {
1268 &self.patterns
1269 }
1270
1271 pub fn clear(&mut self) {
1273 self.shadings.clear();
1274 self.patterns.clear();
1275 self.next_id = 1;
1276 }
1277
1278 pub fn shading_count(&self) -> usize {
1280 self.shadings.len()
1281 }
1282
1283 pub fn pattern_count(&self) -> usize {
1285 self.patterns.len()
1286 }
1287
1288 pub fn total_count(&self) -> usize {
1290 self.shading_count() + self.pattern_count()
1291 }
1292
1293 pub fn create_linear_gradient(
1295 &mut self,
1296 start_point: Point,
1297 end_point: Point,
1298 start_color: Color,
1299 end_color: Color,
1300 ) -> Result<String> {
1301 let shading = ShadingDefinition::Axial(AxialShading::linear_gradient(
1302 String::new(), start_point,
1304 end_point,
1305 start_color,
1306 end_color,
1307 ));
1308
1309 self.add_shading(shading)
1310 }
1311
1312 pub fn create_radial_gradient(
1314 &mut self,
1315 center: Point,
1316 start_radius: f64,
1317 end_radius: f64,
1318 start_color: Color,
1319 end_color: Color,
1320 ) -> Result<String> {
1321 let shading = ShadingDefinition::Radial(RadialShading::radial_gradient(
1322 String::new(), center,
1324 start_radius,
1325 end_radius,
1326 start_color,
1327 end_color,
1328 ));
1329
1330 self.add_shading(shading)
1331 }
1332
1333 pub fn to_resource_dictionary(&self) -> Result<String> {
1335 if self.shadings.is_empty() && self.patterns.is_empty() {
1336 return Ok(String::new());
1337 }
1338
1339 let mut dict = String::new();
1340
1341 if !self.shadings.is_empty() {
1343 dict.push_str("/Shading <<");
1344 for name in self.shadings.keys() {
1345 dict.push_str(&format!(" /{} {} 0 R", name, self.next_id));
1346 }
1347 dict.push_str(" >>");
1348 }
1349
1350 if !self.patterns.is_empty() {
1352 if !dict.is_empty() {
1353 dict.push('\n');
1354 }
1355 dict.push_str("/Pattern <<");
1356 for name in self.patterns.keys() {
1357 dict.push_str(&format!(" /{} {} 0 R", name, self.next_id));
1358 }
1359 dict.push_str(" >>");
1360 }
1361
1362 Ok(dict)
1363 }
1364}
1365
1366#[cfg(test)]
1367mod tests {
1368 use super::*;
1369
1370 #[test]
1373 fn test_bitwriter_single_value_byte_aligned() {
1374 let mut w = BitWriter::new();
1376 w.write_bits(0b1011, 4);
1377 w.align_to_byte();
1378 assert_eq!(w.into_bytes(), vec![0xB0]);
1379 }
1380
1381 #[test]
1382 fn test_bitwriter_value_spans_two_bytes() {
1383 let mut w = BitWriter::new();
1385 w.write_bits(0b1_1111_1111, 9);
1386 w.align_to_byte();
1387 assert_eq!(w.into_bytes(), vec![0xFF, 0x80]);
1388 }
1389
1390 #[test]
1391 fn test_bitwriter_accumulates_across_writes() {
1392 let mut w = BitWriter::new();
1394 w.write_bits(0b10, 2);
1395 w.write_bits(0b110, 3);
1396 w.align_to_byte();
1397 assert_eq!(w.into_bytes(), vec![0xB0]);
1398 }
1399
1400 #[test]
1401 fn test_encode_value_maps_real_to_packed_integer() {
1402 assert_eq!(encode_value(50.0, 0.0, 100.0, 8), 128);
1404 }
1405
1406 #[test]
1407 fn test_encode_value_clamps_out_of_range() {
1408 assert_eq!(encode_value(150.0, 0.0, 100.0, 8), 255);
1409 assert_eq!(encode_value(-10.0, 0.0, 100.0, 8), 0);
1410 assert_eq!(encode_value(100.0, 0.0, 100.0, 8), 255);
1411 }
1412
1413 #[test]
1414 fn test_encode_value_16_bit_precision() {
1415 assert_eq!(encode_value(0.5, 0.0, 1.0, 16), 32768);
1417 }
1418
1419 #[test]
1420 fn test_gouraud_vertex_pack_byte_aligned() {
1421 let v = GouraudVertex {
1423 flag: 0,
1424 x: 10.0,
1425 y: 20.0,
1426 color: Color::Rgb(1.0, 0.0, 0.0),
1427 };
1428 let decode = [0.0, 100.0, 0.0, 100.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0];
1429 let bytes = pack_vertex(&v, 8, 16, 8, &decode, "DeviceRGB");
1430 assert_eq!(bytes, vec![0x00, 0x19, 0x9A, 0x33, 0x33, 0xFF, 0x00, 0x00]);
1432 }
1433
1434 #[test]
1435 fn test_gouraud_vertex_pack_with_padding() {
1436 let v = GouraudVertex {
1438 flag: 1,
1439 x: 50.0,
1440 y: 25.0,
1441 color: Color::Gray(0.5),
1442 };
1443 let decode = [0.0, 100.0, 0.0, 100.0, 0.0, 1.0];
1444 let bytes = pack_vertex(&v, 2, 8, 8, &decode, "DeviceGray");
1445 assert_eq!(bytes, vec![0x60, 0x10, 0x20, 0x00]);
1447 }
1448
1449 fn sample_rgb_mesh() -> FreeFormGouraudShading {
1451 FreeFormGouraudShading::new(
1452 "M".to_string(),
1453 "DeviceRGB".to_string(),
1454 vec![0.0, 100.0, 0.0, 100.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
1455 vec![
1456 GouraudVertex {
1457 flag: 0,
1458 x: 10.0,
1459 y: 20.0,
1460 color: Color::Rgb(1.0, 0.0, 0.0),
1461 },
1462 GouraudVertex {
1463 flag: 1,
1464 x: 50.0,
1465 y: 50.0,
1466 color: Color::Rgb(0.0, 1.0, 0.0),
1467 },
1468 GouraudVertex {
1469 flag: 1,
1470 x: 90.0,
1471 y: 10.0,
1472 color: Color::Rgb(0.0, 0.0, 1.0),
1473 },
1474 ],
1475 )
1476 }
1477
1478 #[test]
1479 fn test_freeform_gouraud_creation_defaults() {
1480 let mesh = sample_rgb_mesh();
1481 assert!(mesh.validate().is_ok());
1482 assert_eq!(mesh.name, "M");
1483 assert_eq!(mesh.color_space, "DeviceRGB");
1484 assert_eq!(mesh.bits_per_coordinate, 16);
1486 assert_eq!(mesh.bits_per_component, 8);
1487 assert_eq!(mesh.bits_per_flag, 8);
1488 assert_eq!(mesh.vertices.len(), 3);
1489 }
1490
1491 #[test]
1492 fn test_freeform_gouraud_validate_rejects_invalid_bits() {
1493 let mut m = sample_rgb_mesh();
1494 m.bits_per_coordinate = 5; assert!(m.validate().is_err());
1496
1497 let mut m = sample_rgb_mesh();
1498 m.bits_per_component = 3; assert!(m.validate().is_err());
1500
1501 let mut m = sample_rgb_mesh();
1502 m.bits_per_flag = 3; assert!(m.validate().is_err());
1504 }
1505
1506 #[test]
1507 fn test_freeform_gouraud_validate_rejects_decode_length_mismatch() {
1508 let mut m = sample_rgb_mesh();
1509 m.decode = vec![0.0, 100.0, 0.0, 100.0, 0.0, 1.0, 0.0, 1.0];
1511 assert!(m.validate().is_err());
1512 }
1513
1514 #[test]
1515 fn test_freeform_gouraud_validate_rejects_empty_vertices() {
1516 let mut m = sample_rgb_mesh();
1517 m.vertices.clear();
1518 assert!(m.validate().is_err());
1519 }
1520
1521 #[test]
1522 fn test_freeform_gouraud_validate_rejects_nonzero_first_flag() {
1523 let mut m = sample_rgb_mesh();
1524 m.vertices[0].flag = 1;
1525 assert!(m.validate().is_err());
1526 }
1527
1528 #[test]
1529 fn test_freeform_gouraud_to_pdf_object_dict_keys() {
1530 let obj = sample_rgb_mesh().to_pdf_object().unwrap();
1531 let dict = match &obj {
1532 Object::Stream(d, _) => d,
1533 other => panic!("mesh must emit a Stream, got {other:?}"),
1534 };
1535 assert_eq!(dict.get("ShadingType"), Some(&Object::Integer(4)));
1536 assert_eq!(
1537 dict.get("ColorSpace"),
1538 Some(&Object::Name("DeviceRGB".to_string()))
1539 );
1540 assert_eq!(dict.get("BitsPerCoordinate"), Some(&Object::Integer(16)));
1541 assert_eq!(dict.get("BitsPerComponent"), Some(&Object::Integer(8)));
1542 assert_eq!(dict.get("BitsPerFlag"), Some(&Object::Integer(8)));
1543 assert_eq!(
1544 dict.get("Decode"),
1545 Some(&Object::Array(
1546 vec![0.0, 100.0, 0.0, 100.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0]
1547 .into_iter()
1548 .map(Object::Real)
1549 .collect()
1550 ))
1551 );
1552 }
1553
1554 #[test]
1555 fn test_freeform_gouraud_stream_body_exact_bytes() {
1556 let obj = sample_rgb_mesh().to_pdf_object().unwrap();
1557 let data = match &obj {
1558 Object::Stream(_, d) => d,
1559 other => panic!("mesh must emit a Stream, got {other:?}"),
1560 };
1561 assert_eq!(
1562 *data,
1563 vec![
1564 0x00, 0x19, 0x9A, 0x33, 0x33, 0xFF, 0x00, 0x00, 0x01, 0x80, 0x00, 0x80, 0x00, 0x00, 0xFF, 0x00, 0x01, 0xE6, 0x66, 0x19, 0x9A, 0x00, 0x00, 0xFF,
1570 ]
1571 );
1572 }
1573
1574 #[test]
1577 fn test_postscript_type4_function_shape() {
1578 let (dict, code) = postscript_type4_function(
1581 "{ 1 }",
1582 &[0.0, 1.0, 0.0, 1.0],
1583 &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
1584 );
1585 assert_eq!(dict.get("FunctionType"), Some(&Object::Integer(4)));
1586 assert_eq!(
1587 dict.get("Domain"),
1588 Some(&Object::Array(
1589 vec![0.0, 1.0, 0.0, 1.0]
1590 .into_iter()
1591 .map(Object::Real)
1592 .collect()
1593 ))
1594 );
1595 assert_eq!(
1596 dict.get("Range"),
1597 Some(&Object::Array(
1598 vec![0.0, 1.0, 0.0, 1.0, 0.0, 1.0]
1599 .into_iter()
1600 .map(Object::Real)
1601 .collect()
1602 ))
1603 );
1604 assert_eq!(code, b"{ 1 }");
1605 }
1606
1607 #[test]
1608 fn test_color_ramp_ps_two_stops_no_branching() {
1609 let stops = vec![
1612 ColorStop::new(0.0, Color::Rgb(1.0, 0.0, 0.0)),
1613 ColorStop::new(1.0, Color::Rgb(0.0, 0.0, 1.0)),
1614 ];
1615 let ps = build_color_ramp_ps(&stops, "DeviceRGB");
1616 assert_eq!(ps, "dup -1 mul 1 add exch dup 0 mul 0 add exch 1 mul 0 add");
1617 assert!(!ps.contains("ifelse"));
1618 }
1619
1620 #[test]
1621 fn test_color_ramp_ps_three_stops_has_bound_check() {
1622 let stops = vec![
1625 ColorStop::new(0.0, Color::red()),
1626 ColorStop::new(0.5, Color::green()),
1627 ColorStop::new(1.0, Color::blue()),
1628 ];
1629 let ps = build_color_ramp_ps(&stops, "DeviceRGB");
1630 assert_eq!(ps.matches("ifelse").count(), 1, "one split for three stops");
1631 assert!(ps.contains("0.5"), "interior bound present");
1632 assert_eq!(ps.matches("mul").count(), 6, "two RGB segments");
1633 }
1634
1635 #[test]
1636 fn test_conic_angle_prologue_exact_ps() {
1637 let ps = build_conic_angle_prologue(Point::new(50.0, 50.0));
1639 assert_eq!(ps, "50 sub exch 50 sub atan 360 div");
1640 }
1641
1642 #[test]
1643 fn test_conic_shading_emits_type1_with_ps_function_and_colorspace() {
1644 let stops = vec![
1645 ColorStop::new(0.0, Color::red()),
1646 ColorStop::new(1.0, Color::blue()),
1647 ];
1648 let conic = ConicShading::new(
1649 "C".to_string(),
1650 Point::new(50.0, 50.0),
1651 [0.0, 100.0, 0.0, 100.0],
1652 stops.clone(),
1653 );
1654 let dict = conic.to_pdf_dictionary().unwrap();
1655
1656 assert_eq!(dict.get("ShadingType"), Some(&Object::Integer(1)));
1658 assert_eq!(
1659 dict.get("ColorSpace"),
1660 Some(&Object::Name("DeviceRGB".to_string()))
1661 );
1662 assert_eq!(
1663 dict.get("Domain"),
1664 Some(&Object::Array(
1665 vec![0.0, 100.0, 0.0, 100.0]
1666 .into_iter()
1667 .map(Object::Real)
1668 .collect()
1669 ))
1670 );
1671
1672 let (fdict, fcode) = match dict.get("Function") {
1674 Some(Object::Stream(d, c)) => (d, c),
1675 other => panic!("Function must be a Type 4 stream, got {other:?}"),
1676 };
1677 assert_eq!(fdict.get("FunctionType"), Some(&Object::Integer(4)));
1678 assert_eq!(
1680 fdict.get("Domain"),
1681 Some(&Object::Array(
1682 vec![0.0, 100.0, 0.0, 100.0]
1683 .into_iter()
1684 .map(Object::Real)
1685 .collect()
1686 ))
1687 );
1688 assert_eq!(
1690 fdict.get("Range"),
1691 Some(&Object::Array(
1692 vec![0.0, 1.0, 0.0, 1.0, 0.0, 1.0]
1693 .into_iter()
1694 .map(Object::Real)
1695 .collect()
1696 ))
1697 );
1698 let expected = format!(
1700 "{{ {} {} }}",
1701 build_conic_angle_prologue(Point::new(50.0, 50.0)),
1702 build_color_ramp_ps(&stops, "DeviceRGB")
1703 );
1704 assert_eq!(fcode, &expected.into_bytes());
1705 }
1706
1707 #[test]
1708 fn test_conic_shading_validate_rejects_bad_domain_and_empty_stops() {
1709 let stops = vec![
1710 ColorStop::new(0.0, Color::red()),
1711 ColorStop::new(1.0, Color::blue()),
1712 ];
1713 let bad_domain = ConicShading::new(
1714 "C".to_string(),
1715 Point::new(0.0, 0.0),
1716 [1.0, 0.0, 0.0, 1.0], stops,
1718 );
1719 assert!(bad_domain.validate().is_err());
1720
1721 let empty = ConicShading::new(
1722 "C".to_string(),
1723 Point::new(0.0, 0.0),
1724 [0.0, 1.0, 0.0, 1.0],
1725 vec![],
1726 );
1727 assert!(empty.validate().is_err());
1728 }
1729
1730 #[test]
1734 fn test_freeform_gouraud_validate_rejects_color_space_mismatch() {
1735 let mut m = sample_rgb_mesh();
1738 m.color_space = "DeviceGray".to_string();
1739 m.decode = vec![0.0, 100.0, 0.0, 100.0, 0.0, 1.0]; assert!(m.validate().is_err());
1741 }
1742
1743 #[test]
1744 fn test_freeform_gouraud_validate_rejects_inverted_decode() {
1745 let mut m = sample_rgb_mesh();
1747 m.decode[0] = 100.0;
1748 m.decode[1] = 0.0;
1749 assert!(m.validate().is_err());
1750 }
1751
1752 #[test]
1753 fn test_freeform_gouraud_validate_rejects_out_of_range_flag() {
1754 let mut m = sample_rgb_mesh();
1757 m.vertices[1].flag = 3;
1758 assert!(m.validate().is_err());
1759 }
1760
1761 #[test]
1762 fn test_freeform_gouraud_cmyk_mesh_validates_and_packs() {
1763 let mesh = FreeFormGouraudShading::new(
1766 "Cmyk".to_string(),
1767 "DeviceCMYK".to_string(),
1768 vec![
1769 0.0, 100.0, 0.0, 100.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0,
1770 ],
1771 vec![GouraudVertex {
1772 flag: 0,
1773 x: 0.0,
1774 y: 0.0,
1775 color: Color::Cmyk(1.0, 0.0, 0.0, 0.0),
1776 }],
1777 )
1778 .with_bits(8, 8, 8);
1779 assert!(mesh.validate().is_ok());
1780 let bytes = pack_vertex(&mesh.vertices[0], 8, 8, 8, &mesh.decode, "DeviceCMYK");
1781 assert_eq!(bytes, vec![0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00]);
1783 }
1784
1785 #[test]
1786 fn test_conic_shading_validate_requires_full_range_endpoints() {
1787 let conic = ConicShading::new(
1790 "C".to_string(),
1791 Point::new(0.0, 0.0),
1792 [0.0, 1.0, 0.0, 1.0],
1793 vec![
1794 ColorStop::new(0.25, Color::red()),
1795 ColorStop::new(0.75, Color::blue()),
1796 ],
1797 );
1798 assert!(conic.validate().is_err());
1799 }
1800
1801 #[test]
1802 fn test_conic_shading_validate_rejects_equal_positions() {
1803 let conic = ConicShading::new(
1805 "C".to_string(),
1806 Point::new(0.0, 0.0),
1807 [0.0, 1.0, 0.0, 1.0],
1808 vec![
1809 ColorStop::new(0.0, Color::red()),
1810 ColorStop::new(0.5, Color::green()),
1811 ColorStop::new(0.5, Color::blue()),
1812 ColorStop::new(1.0, Color::red()),
1813 ],
1814 );
1815 assert!(conic.validate().is_err());
1816 }
1817
1818 #[test]
1819 fn test_conic_shading_three_stops_no_zero_div_and_one_ifelse() {
1820 let conic = ConicShading::new(
1823 "C".to_string(),
1824 Point::new(50.0, 50.0),
1825 [0.0, 100.0, 0.0, 100.0],
1826 vec![
1827 ColorStop::new(0.0, Color::red()),
1828 ColorStop::new(0.5, Color::green()),
1829 ColorStop::new(1.0, Color::blue()),
1830 ],
1831 );
1832 let dict = conic.to_pdf_dictionary().unwrap();
1833 let code = match dict.get("Function") {
1834 Some(Object::Stream(_, c)) => String::from_utf8(c.clone()).unwrap(),
1835 other => panic!("Function must be a stream, got {other:?}"),
1836 };
1837 assert!(!code.contains(" 0 div"), "no zero-width segment:\n{code}");
1840 assert_eq!(code.matches("ifelse").count(), 1);
1841 }
1842
1843 #[test]
1844 fn test_color_stop_creation() {
1845 let stop = ColorStop::new(0.5, Color::red());
1846 assert_eq!(stop.position, 0.5);
1847 assert_eq!(stop.color, Color::red());
1848
1849 let stop_clamped = ColorStop::new(1.5, Color::blue());
1851 assert_eq!(stop_clamped.position, 1.0);
1852 }
1853
1854 #[test]
1855 fn test_point_creation() {
1856 let point = Point::new(10.0, 20.0);
1857 assert_eq!(point.x, 10.0);
1858 assert_eq!(point.y, 20.0);
1859 }
1860
1861 #[test]
1862 fn test_axial_shading_creation() {
1863 let start = Point::new(0.0, 0.0);
1864 let end = Point::new(100.0, 100.0);
1865 let stops = vec![
1866 ColorStop::new(0.0, Color::red()),
1867 ColorStop::new(1.0, Color::blue()),
1868 ];
1869
1870 let shading = AxialShading::new("TestGradient".to_string(), start, end, stops);
1871 assert_eq!(shading.name, "TestGradient");
1872 assert_eq!(shading.start_point, start);
1873 assert_eq!(shading.end_point, end);
1874 assert_eq!(shading.color_stops.len(), 2);
1875 assert!(!shading.extend_start);
1876 assert!(!shading.extend_end);
1877 }
1878
1879 #[test]
1880 fn test_axial_shading_linear_gradient() {
1881 let start = Point::new(0.0, 0.0);
1882 let end = Point::new(100.0, 0.0);
1883 let shading = AxialShading::linear_gradient(
1884 "LinearGrad".to_string(),
1885 start,
1886 end,
1887 Color::red(),
1888 Color::blue(),
1889 );
1890
1891 assert_eq!(shading.color_stops.len(), 2);
1892 assert_eq!(shading.color_stops[0].position, 0.0);
1893 assert_eq!(shading.color_stops[1].position, 1.0);
1894 }
1895
1896 #[test]
1897 fn test_axial_shading_with_extend() {
1898 let start = Point::new(0.0, 0.0);
1899 let end = Point::new(100.0, 0.0);
1900 let shading = AxialShading::linear_gradient(
1901 "ExtendedGrad".to_string(),
1902 start,
1903 end,
1904 Color::red(),
1905 Color::blue(),
1906 )
1907 .with_extend(true, true);
1908
1909 assert!(shading.extend_start);
1910 assert!(shading.extend_end);
1911 }
1912
1913 #[test]
1914 fn test_axial_shading_validation_valid() {
1915 let start = Point::new(0.0, 0.0);
1916 let end = Point::new(100.0, 0.0);
1917 let shading = AxialShading::linear_gradient(
1918 "ValidGrad".to_string(),
1919 start,
1920 end,
1921 Color::red(),
1922 Color::blue(),
1923 );
1924
1925 assert!(shading.validate().is_ok());
1926 }
1927
1928 #[test]
1929 fn test_axial_shading_validation_no_stops() {
1930 let start = Point::new(0.0, 0.0);
1931 let end = Point::new(100.0, 0.0);
1932 let shading = AxialShading::new("EmptyGrad".to_string(), start, end, Vec::new());
1933
1934 assert!(shading.validate().is_err());
1935 }
1936
1937 #[test]
1938 fn test_axial_shading_validation_same_points() {
1939 let point = Point::new(50.0, 50.0);
1940 let shading = AxialShading::linear_gradient(
1941 "SamePointGrad".to_string(),
1942 point,
1943 point,
1944 Color::red(),
1945 Color::blue(),
1946 );
1947
1948 assert!(shading.validate().is_err());
1949 }
1950
1951 #[test]
1952 fn test_radial_shading_creation() {
1953 let center = Point::new(50.0, 50.0);
1954 let stops = vec![
1955 ColorStop::new(0.0, Color::red()),
1956 ColorStop::new(1.0, Color::blue()),
1957 ];
1958
1959 let shading =
1960 RadialShading::new("RadialGrad".to_string(), center, 10.0, center, 50.0, stops);
1961
1962 assert_eq!(shading.name, "RadialGrad");
1963 assert_eq!(shading.start_center, center);
1964 assert_eq!(shading.start_radius, 10.0);
1965 assert_eq!(shading.end_radius, 50.0);
1966 }
1967
1968 #[test]
1969 fn test_radial_shading_gradient() {
1970 let center = Point::new(50.0, 50.0);
1971 let shading = RadialShading::radial_gradient(
1972 "SimpleRadial".to_string(),
1973 center,
1974 0.0,
1975 25.0,
1976 Color::white(),
1977 Color::black(),
1978 );
1979
1980 assert_eq!(shading.color_stops.len(), 2);
1981 assert_eq!(shading.start_radius, 0.0);
1982 assert_eq!(shading.end_radius, 25.0);
1983 }
1984
1985 #[test]
1986 fn test_radial_shading_radius_clamping() {
1987 let center = Point::new(50.0, 50.0);
1988 let stops = vec![ColorStop::new(0.0, Color::red())];
1989
1990 let shading = RadialShading::new(
1991 "ClampedRadial".to_string(),
1992 center,
1993 -5.0, center,
1995 10.0,
1996 stops,
1997 );
1998
1999 assert_eq!(shading.start_radius, 0.0);
2000 }
2001
2002 #[test]
2003 fn test_radial_shading_validation_valid() {
2004 let center = Point::new(50.0, 50.0);
2005 let shading = RadialShading::radial_gradient(
2006 "ValidRadial".to_string(),
2007 center,
2008 0.0,
2009 25.0,
2010 Color::red(),
2011 Color::blue(),
2012 );
2013
2014 assert!(shading.validate().is_ok());
2015 }
2016
2017 #[test]
2018 fn test_function_based_shading_creation() {
2019 let domain = [0.0, 1.0, 0.0, 1.0];
2020 let shading = FunctionBasedShading::new("FuncShading".to_string(), domain, 1);
2021
2022 assert_eq!(shading.name, "FuncShading");
2023 assert_eq!(shading.domain, domain);
2024 assert_eq!(shading.function_id, 1);
2025 assert!(shading.matrix.is_none());
2026 }
2027
2028 #[test]
2029 fn test_function_based_shading_with_matrix() {
2030 let domain = [0.0, 1.0, 0.0, 1.0];
2031 let matrix = [2.0, 0.0, 0.0, 2.0, 10.0, 20.0];
2032 let shading =
2033 FunctionBasedShading::new("FuncShading".to_string(), domain, 1).with_matrix(matrix);
2034
2035 assert_eq!(shading.matrix, Some(matrix));
2036 }
2037
2038 #[test]
2039 fn test_function_based_shading_validation_valid() {
2040 let domain = [0.0, 1.0, 0.0, 1.0];
2041 let shading = FunctionBasedShading::new("ValidFunc".to_string(), domain, 1);
2042
2043 assert!(shading.validate().is_ok());
2044 }
2045
2046 #[test]
2047 fn test_function_based_shading_validation_invalid_domain() {
2048 let domain = [1.0, 0.0, 0.0, 1.0]; let shading = FunctionBasedShading::new("InvalidFunc".to_string(), domain, 1);
2050
2051 assert!(shading.validate().is_err());
2052 }
2053
2054 #[test]
2055 fn test_shading_pattern_creation() {
2056 let start = Point::new(0.0, 0.0);
2057 let end = Point::new(100.0, 0.0);
2058 let axial = AxialShading::linear_gradient(
2059 "PatternGrad".to_string(),
2060 start,
2061 end,
2062 Color::red(),
2063 Color::blue(),
2064 );
2065 let shading = ShadingDefinition::Axial(axial);
2066 let pattern = ShadingPattern::new("Pattern1".to_string(), shading);
2067
2068 assert_eq!(pattern.name, "Pattern1");
2069 assert!(pattern.matrix.is_none());
2070 }
2071
2072 #[test]
2073 fn test_shading_pattern_with_matrix() {
2074 let start = Point::new(0.0, 0.0);
2075 let end = Point::new(100.0, 0.0);
2076 let axial = AxialShading::linear_gradient(
2077 "PatternGrad".to_string(),
2078 start,
2079 end,
2080 Color::red(),
2081 Color::blue(),
2082 );
2083 let shading = ShadingDefinition::Axial(axial);
2084 let matrix = [1.0, 0.0, 0.0, 1.0, 50.0, 50.0];
2085 let pattern = ShadingPattern::new("Pattern1".to_string(), shading).with_matrix(matrix);
2086
2087 assert_eq!(pattern.matrix, Some(matrix));
2088 }
2089
2090 #[test]
2091 fn test_shading_manager_creation() {
2092 let manager = ShadingManager::new();
2093 assert_eq!(manager.shading_count(), 0);
2094 assert_eq!(manager.pattern_count(), 0);
2095 assert_eq!(manager.total_count(), 0);
2096 }
2097
2098 #[test]
2099 fn test_shading_manager_add_shading() {
2100 let mut manager = ShadingManager::new();
2101 let start = Point::new(0.0, 0.0);
2102 let end = Point::new(100.0, 0.0);
2103 let axial = AxialShading::linear_gradient(
2104 "TestGrad".to_string(),
2105 start,
2106 end,
2107 Color::red(),
2108 Color::blue(),
2109 );
2110 let shading = ShadingDefinition::Axial(axial);
2111
2112 let name = manager.add_shading(shading).unwrap();
2113 assert_eq!(name, "TestGrad");
2114 assert_eq!(manager.shading_count(), 1);
2115
2116 let retrieved = manager.get_shading(&name).unwrap();
2117 assert_eq!(retrieved.name(), "TestGrad");
2118 }
2119
2120 #[test]
2121 fn test_shading_manager_auto_naming() {
2122 let mut manager = ShadingManager::new();
2123 let start = Point::new(0.0, 0.0);
2124 let end = Point::new(100.0, 0.0);
2125 let axial = AxialShading::linear_gradient(
2126 String::new(), start,
2128 end,
2129 Color::red(),
2130 Color::blue(),
2131 );
2132 let shading = ShadingDefinition::Axial(axial);
2133
2134 let name = manager.add_shading(shading).unwrap();
2135 assert_eq!(name, "Sh1");
2136
2137 let axial2 = AxialShading::linear_gradient(
2139 String::new(),
2140 start,
2141 end,
2142 Color::green(),
2143 Color::yellow(),
2144 );
2145 let shading2 = ShadingDefinition::Axial(axial2);
2146
2147 let name2 = manager.add_shading(shading2).unwrap();
2148 assert_eq!(name2, "Sh2");
2149 }
2150
2151 #[test]
2152 fn test_shading_manager_create_gradients() {
2153 let mut manager = ShadingManager::new();
2154
2155 let linear_name = manager
2156 .create_linear_gradient(
2157 Point::new(0.0, 0.0),
2158 Point::new(100.0, 0.0),
2159 Color::red(),
2160 Color::blue(),
2161 )
2162 .unwrap();
2163
2164 let radial_name = manager
2165 .create_radial_gradient(
2166 Point::new(50.0, 50.0),
2167 0.0,
2168 25.0,
2169 Color::white(),
2170 Color::black(),
2171 )
2172 .unwrap();
2173
2174 assert_eq!(manager.shading_count(), 2);
2175 assert!(manager.get_shading(&linear_name).is_some());
2176 assert!(manager.get_shading(&radial_name).is_some());
2177 }
2178
2179 #[test]
2180 fn test_shading_manager_clear() {
2181 let mut manager = ShadingManager::new();
2182
2183 manager
2184 .create_linear_gradient(
2185 Point::new(0.0, 0.0),
2186 Point::new(100.0, 0.0),
2187 Color::red(),
2188 Color::blue(),
2189 )
2190 .unwrap();
2191
2192 assert_eq!(manager.shading_count(), 1);
2193
2194 manager.clear();
2195 assert_eq!(manager.shading_count(), 0);
2196 assert_eq!(manager.total_count(), 0);
2197 }
2198
2199 #[test]
2200 fn test_axial_shading_pdf_dictionary() {
2201 let start = Point::new(0.0, 0.0);
2202 let end = Point::new(100.0, 50.0);
2203 let shading = AxialShading::linear_gradient(
2204 "TestPDF".to_string(),
2205 start,
2206 end,
2207 Color::red(),
2208 Color::blue(),
2209 )
2210 .with_extend(true, false);
2211
2212 let dict = shading.to_pdf_dictionary().unwrap();
2213
2214 if let Some(Object::Integer(shading_type)) = dict.get("ShadingType") {
2215 assert_eq!(*shading_type, 2); }
2217
2218 if let Some(Object::Array(coords)) = dict.get("Coords") {
2219 assert_eq!(coords.len(), 4);
2220 }
2221
2222 if let Some(Object::Array(extend)) = dict.get("Extend") {
2223 assert_eq!(extend.len(), 2);
2224 if let (Object::Boolean(start_extend), Object::Boolean(end_extend)) =
2225 (&extend[0], &extend[1])
2226 {
2227 assert!(*start_extend);
2228 assert!(!(*end_extend));
2229 }
2230 }
2231 }
2232
2233 fn type2_c0_c1(func: &Dictionary) -> (Vec<f64>, Vec<f64>) {
2237 let extract = |key: &str| -> Vec<f64> {
2238 match func.get(key) {
2239 Some(Object::Array(a)) => a
2240 .iter()
2241 .map(|o| match o {
2242 Object::Real(v) => *v,
2243 Object::Integer(v) => *v as f64,
2244 _ => panic!("{key} component is not numeric"),
2245 })
2246 .collect(),
2247 other => panic!("{key} is not an array: {other:?}"),
2248 }
2249 };
2250 (extract("C0"), extract("C1"))
2251 }
2252
2253 #[test]
2254 fn test_axial_two_stops_emits_real_type2_function() {
2255 let shading = AxialShading::linear_gradient(
2258 "G".to_string(),
2259 Point::new(0.0, 0.0),
2260 Point::new(100.0, 0.0),
2261 Color::red(),
2262 Color::blue(),
2263 );
2264 let dict = shading.to_pdf_dictionary().unwrap();
2265
2266 assert_eq!(
2268 dict.get("ColorSpace"),
2269 Some(&Object::Name("DeviceRGB".to_string())),
2270 "axial shading must declare /ColorSpace"
2271 );
2272
2273 let func = match dict.get("Function") {
2275 Some(Object::Dictionary(d)) => d,
2276 other => panic!("/Function must be a dictionary, got {other:?}"),
2277 };
2278 assert_eq!(func.get("FunctionType"), Some(&Object::Integer(2)));
2279 let (c0, c1) = type2_c0_c1(func);
2280 assert_eq!(c0, vec![1.0, 0.0, 0.0], "C0 must be red");
2281 assert_eq!(c1, vec![0.0, 0.0, 1.0], "C1 must be blue");
2282 assert_eq!(func.get("N"), Some(&Object::Real(1.0)));
2283 assert_eq!(
2284 func.get("Domain"),
2285 Some(&Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]))
2286 );
2287 }
2288
2289 #[test]
2290 fn test_axial_three_stops_emits_type3_stitching() {
2291 let shading = AxialShading::new(
2294 "G".to_string(),
2295 Point::new(0.0, 0.0),
2296 Point::new(100.0, 0.0),
2297 vec![
2298 ColorStop::new(0.0, Color::red()),
2299 ColorStop::new(0.5, Color::green()),
2300 ColorStop::new(1.0, Color::blue()),
2301 ],
2302 );
2303 let dict = shading.to_pdf_dictionary().unwrap();
2304 let func = match dict.get("Function") {
2305 Some(Object::Dictionary(d)) => d,
2306 other => panic!("/Function must be a dictionary, got {other:?}"),
2307 };
2308 assert_eq!(func.get("FunctionType"), Some(&Object::Integer(3)));
2309 assert_eq!(
2310 func.get("Bounds"),
2311 Some(&Object::Array(vec![Object::Real(0.5)])),
2312 "interior stop position is the only bound"
2313 );
2314 assert_eq!(
2315 func.get("Encode"),
2316 Some(&Object::Array(vec![
2317 Object::Real(0.0),
2318 Object::Real(1.0),
2319 Object::Real(0.0),
2320 Object::Real(1.0),
2321 ]))
2322 );
2323 let subfuncs = match func.get("Functions") {
2324 Some(Object::Array(a)) => a,
2325 other => panic!("/Functions must be an array, got {other:?}"),
2326 };
2327 assert_eq!(subfuncs.len(), 2, "two segments for three stops");
2328 let f0 = match &subfuncs[0] {
2330 Object::Dictionary(d) => d,
2331 other => panic!("subfunction 0 not a dict: {other:?}"),
2332 };
2333 let (c0, c1) = type2_c0_c1(f0);
2334 assert_eq!(c0, vec![1.0, 0.0, 0.0]);
2335 assert_eq!(c1, vec![0.0, 1.0, 0.0]);
2336 }
2337
2338 #[test]
2339 fn test_axial_gray_stops_emit_devicegray_function() {
2340 let shading = AxialShading::linear_gradient(
2342 "G".to_string(),
2343 Point::new(0.0, 0.0),
2344 Point::new(10.0, 0.0),
2345 Color::black(),
2346 Color::white(),
2347 );
2348 let dict = shading.to_pdf_dictionary().unwrap();
2349 assert_eq!(
2350 dict.get("ColorSpace"),
2351 Some(&Object::Name("DeviceGray".to_string()))
2352 );
2353 let func = match dict.get("Function") {
2354 Some(Object::Dictionary(d)) => d,
2355 other => panic!("/Function must be a dictionary, got {other:?}"),
2356 };
2357 let (c0, c1) = type2_c0_c1(func);
2358 assert_eq!(c0, vec![0.0], "black");
2359 assert_eq!(c1, vec![1.0], "white");
2360 }
2361
2362 #[test]
2363 fn test_axial_cmyk_stops_emit_devicecmyk_function() {
2364 let shading = AxialShading::linear_gradient(
2366 "G".to_string(),
2367 Point::new(0.0, 0.0),
2368 Point::new(10.0, 0.0),
2369 Color::Cmyk(1.0, 0.0, 0.0, 0.0),
2370 Color::Cmyk(0.0, 1.0, 0.0, 0.0),
2371 );
2372 let dict = shading.to_pdf_dictionary().unwrap();
2373 assert_eq!(
2374 dict.get("ColorSpace"),
2375 Some(&Object::Name("DeviceCMYK".to_string()))
2376 );
2377 let func = match dict.get("Function") {
2378 Some(Object::Dictionary(d)) => d,
2379 other => panic!("/Function must be a dictionary, got {other:?}"),
2380 };
2381 let (c0, c1) = type2_c0_c1(func);
2382 assert_eq!(c0, vec![1.0, 0.0, 0.0, 0.0], "C0 = cyan, 4 components");
2383 assert_eq!(c1, vec![0.0, 1.0, 0.0, 0.0], "C1 = magenta, 4 components");
2384 }
2385
2386 #[test]
2387 fn test_axial_four_stops_type3_has_three_subfunctions_two_bounds() {
2388 let shading = AxialShading::new(
2389 "G".to_string(),
2390 Point::new(0.0, 0.0),
2391 Point::new(100.0, 0.0),
2392 vec![
2393 ColorStop::new(0.0, Color::red()),
2394 ColorStop::new(0.3, Color::green()),
2395 ColorStop::new(0.7, Color::blue()),
2396 ColorStop::new(1.0, Color::white()),
2397 ],
2398 );
2399 let dict = shading.to_pdf_dictionary().unwrap();
2400 let func = match dict.get("Function") {
2401 Some(Object::Dictionary(d)) => d,
2402 other => panic!("/Function must be a dictionary, got {other:?}"),
2403 };
2404 assert_eq!(func.get("FunctionType"), Some(&Object::Integer(3)));
2405 let subfuncs = match func.get("Functions") {
2406 Some(Object::Array(a)) => a,
2407 other => panic!("/Functions array expected, got {other:?}"),
2408 };
2409 assert_eq!(subfuncs.len(), 3, "4 stops → 3 segments");
2410 assert_eq!(
2411 func.get("Bounds"),
2412 Some(&Object::Array(vec![Object::Real(0.3), Object::Real(0.7)])),
2413 "two interior bounds at the middle stops"
2414 );
2415 assert_eq!(
2416 func.get("Encode"),
2417 Some(&Object::Array(vec![
2418 Object::Real(0.0),
2419 Object::Real(1.0),
2420 Object::Real(0.0),
2421 Object::Real(1.0),
2422 Object::Real(0.0),
2423 Object::Real(1.0),
2424 ]))
2425 );
2426 }
2427
2428 #[test]
2429 fn test_single_stop_emits_constant_type2() {
2430 let shading = AxialShading::new(
2432 "G".to_string(),
2433 Point::new(0.0, 0.0),
2434 Point::new(10.0, 0.0),
2435 vec![ColorStop::new(0.0, Color::Rgb(0.2, 0.4, 0.6))],
2436 );
2437 let func = match shading.to_pdf_dictionary().unwrap().get("Function") {
2438 Some(Object::Dictionary(d)) => d.clone(),
2439 other => panic!("/Function must be a dictionary, got {other:?}"),
2440 };
2441 assert_eq!(func.get("FunctionType"), Some(&Object::Integer(2)));
2442 let (c0, c1) = type2_c0_c1(&func);
2443 assert_eq!(c0, c1, "constant colour: C0 == C1");
2444 assert_eq!(c0, vec![0.2, 0.4, 0.6]);
2445 }
2446
2447 #[test]
2448 fn test_mixed_color_spaces_promote_to_rgb() {
2449 let shading = AxialShading::new(
2451 "G".to_string(),
2452 Point::new(0.0, 0.0),
2453 Point::new(10.0, 0.0),
2454 vec![
2455 ColorStop::new(0.0, Color::Gray(0.5)),
2456 ColorStop::new(1.0, Color::Rgb(1.0, 0.0, 0.0)),
2457 ],
2458 );
2459 let dict = shading.to_pdf_dictionary().unwrap();
2460 assert_eq!(
2461 dict.get("ColorSpace"),
2462 Some(&Object::Name("DeviceRGB".to_string()))
2463 );
2464 let func = match dict.get("Function") {
2465 Some(Object::Dictionary(d)) => d,
2466 other => panic!("/Function must be a dictionary, got {other:?}"),
2467 };
2468 let (c0, c1) = type2_c0_c1(func);
2469 assert_eq!(c0, vec![0.5, 0.5, 0.5], "gray 0.5 promoted to RGB");
2470 assert_eq!(c1, vec![1.0, 0.0, 0.0]);
2471 }
2472
2473 #[test]
2474 fn test_radial_emits_real_function_and_colorspace() {
2475 let center = Point::new(50.0, 50.0);
2476 let shading = RadialShading::radial_gradient(
2477 "R".to_string(),
2478 center,
2479 0.0,
2480 25.0,
2481 Color::cyan(),
2482 Color::magenta(),
2483 );
2484 let dict = shading.to_pdf_dictionary().unwrap();
2485 assert_eq!(
2486 dict.get("ColorSpace"),
2487 Some(&Object::Name("DeviceRGB".to_string()))
2488 );
2489 let func = match dict.get("Function") {
2490 Some(Object::Dictionary(d)) => d,
2491 other => panic!("/Function must be a dictionary, got {other:?}"),
2492 };
2493 assert_eq!(func.get("FunctionType"), Some(&Object::Integer(2)));
2494 }
2495
2496 #[test]
2497 fn test_shading_pattern_inlines_real_shading_not_placeholder() {
2498 let axial = AxialShading::linear_gradient(
2500 "P".to_string(),
2501 Point::new(0.0, 0.0),
2502 Point::new(100.0, 0.0),
2503 Color::red(),
2504 Color::blue(),
2505 );
2506 let pattern = ShadingPattern::new("SP1".to_string(), ShadingDefinition::Axial(axial));
2507 let dict = pattern.to_pdf_pattern_dictionary().unwrap();
2508 assert_eq!(dict.get("PatternType"), Some(&Object::Integer(2)));
2509 let shading = match dict.get("Shading") {
2510 Some(Object::Dictionary(d)) => d,
2511 other => panic!("/Shading must be an inline dict, got {other:?}"),
2512 };
2513 assert_eq!(shading.get("ShadingType"), Some(&Object::Integer(2)));
2514 assert!(
2515 matches!(shading.get("Function"), Some(Object::Dictionary(_))),
2516 "inlined shading must carry a real /Function"
2517 );
2518 }
2519
2520 #[test]
2521 fn test_radial_shading_pdf_dictionary() {
2522 let center = Point::new(50.0, 50.0);
2523 let shading = RadialShading::radial_gradient(
2524 "TestRadialPDF".to_string(),
2525 center,
2526 10.0,
2527 30.0,
2528 Color::yellow(),
2529 Color::red(),
2530 );
2531
2532 let dict = shading.to_pdf_dictionary().unwrap();
2533
2534 if let Some(Object::Integer(shading_type)) = dict.get("ShadingType") {
2535 assert_eq!(*shading_type, 3); }
2537
2538 if let Some(Object::Array(coords)) = dict.get("Coords") {
2539 assert_eq!(coords.len(), 6); }
2541 }
2542}