1use crate::content::color_space::{
8 ResolvedColorSpace, components_to_device_color_icc, painted_channels_for_cs,
9 resolve_color_space_obj,
10};
11use crate::content::graphics_state::PdfGraphicsState;
12use crate::error::PdfError;
13use crate::objects::{PdfDict, PdfObj};
14use crate::resolver::Resolver;
15use crate::resources::function::PdfFunction;
16use std::sync::Arc;
17
18use stet_fonts::geometry::Matrix;
19use stet_graphics::device::{
20 AxialShadingParams, ColorStop, ImageColorSpace, ImageParams, MeshShadingParams,
21 PatchShadingParams, RadialShadingParams, ShadingColorSpace, SimpleColorSpace, SpotTintFunction,
22};
23use stet_graphics::display_list::{DisplayElement, DisplayList};
24use stet_graphics::icc::IccCache;
25
26pub fn handle_shading(
30 shading_obj: &PdfObj,
31 dict: &PdfDict,
32 gstate: &PdfGraphicsState,
33 resolver: &Resolver,
34 display_list: &mut DisplayList,
35 icc_cache: &mut IccCache,
36) -> Result<(), PdfError> {
37 let shading_type =
38 dict.get_int(b"ShadingType")
39 .ok_or(PdfError::Other("shading missing ShadingType".into()))? as i32;
40
41 let bbox = parse_bbox(dict);
42 let extend = parse_extend(dict);
43
44 let resolved_cs = resolve_shading_resolved_cs(dict, resolver);
46
47 if let Some(bg_arr) = dict.get_array(b"Background") {
50 let comps: Vec<f64> = bg_arr.iter().filter_map(|o| o.as_f64()).collect();
51 let bg_color = components_to_device_color_icc(&resolved_cs, &comps, Some(icc_cache));
52 let mut params = gstate.fill_params(stet_graphics::color::FillRule::NonZeroWinding);
53 params.color = bg_color;
54 let mut path = stet_fonts::geometry::PsPath::new();
56 path.segments
57 .push(stet_fonts::geometry::PathSegment::MoveTo(-1e6, -1e6));
58 path.segments
59 .push(stet_fonts::geometry::PathSegment::LineTo(1e6, -1e6));
60 path.segments
61 .push(stet_fonts::geometry::PathSegment::LineTo(1e6, 1e6));
62 path.segments
63 .push(stet_fonts::geometry::PathSegment::LineTo(-1e6, 1e6));
64 path.segments
65 .push(stet_fonts::geometry::PathSegment::ClosePath);
66 display_list.push(DisplayElement::Fill { path, params });
67 }
68
69 match shading_type {
70 1 => handle_function_based(
71 dict,
72 gstate,
73 resolver,
74 display_list,
75 &resolved_cs,
76 icc_cache,
77 ),
78 2 => handle_axial(
79 dict,
80 gstate,
81 resolver,
82 display_list,
83 bbox,
84 extend,
85 &resolved_cs,
86 icc_cache,
87 ),
88 3 => handle_radial(
89 dict,
90 gstate,
91 resolver,
92 display_list,
93 bbox,
94 extend,
95 &resolved_cs,
96 icc_cache,
97 ),
98 4 | 5 => handle_mesh(
99 shading_obj,
100 dict,
101 gstate,
102 resolver,
103 display_list,
104 shading_type,
105 &resolved_cs,
106 icc_cache,
107 ),
108 6 | 7 => handle_patches(
109 shading_obj,
110 dict,
111 gstate,
112 resolver,
113 display_list,
114 shading_type,
115 &resolved_cs,
116 icc_cache,
117 ),
118 _ => Ok(()),
119 }
120}
121
122fn handle_function_based(
123 dict: &PdfDict,
124 gstate: &PdfGraphicsState,
125 resolver: &Resolver,
126 display_list: &mut DisplayList,
127 resolved_cs: &ResolvedColorSpace,
128 icc_cache: &mut IccCache,
129) -> Result<(), PdfError> {
130 let function = parse_shading_function(dict, resolver)?;
131
132 let domain = dict
133 .get_array(b"Domain")
134 .map(|a| {
135 let v: Vec<f64> = a.iter().filter_map(|o| o.as_f64()).collect();
136 if v.len() >= 4 {
137 [v[0], v[1], v[2], v[3]]
138 } else {
139 [0.0, 1.0, 0.0, 1.0]
140 }
141 })
142 .unwrap_or([0.0, 1.0, 0.0, 1.0]);
143
144 let shading_matrix = dict
145 .get_array(b"Matrix")
146 .map(|a| {
147 let v: Vec<f64> = a.iter().filter_map(|o| o.as_f64()).collect();
148 if v.len() >= 6 {
149 Matrix::new(v[0], v[1], v[2], v[3], v[4], v[5])
150 } else {
151 Matrix::identity()
152 }
153 })
154 .unwrap_or_else(Matrix::identity);
155
156 let domain_w = domain[1] - domain[0];
157 let domain_h = domain[3] - domain[2];
158 let domain_matrix = Matrix::new(domain_w, 0.0, 0.0, domain_h, domain[0], domain[2]);
159 let combined = gstate.ctm.concat(&shading_matrix).concat(&domain_matrix);
160
161 let dev_w = (combined.a * combined.a + combined.b * combined.b).sqrt();
165 let dev_h = (combined.c * combined.c + combined.d * combined.d).sqrt();
166 let width = (dev_w.ceil() as u32).clamp(2, 2048);
167 let height = (dev_h.ceil() as u32).clamp(2, 2048);
168
169 let mut rgba = vec![255u8; (width * height * 4) as usize];
170
171 for row in 0..height {
172 for col in 0..width {
173 let x = domain[0] + (col as f64 + 0.5) / width as f64 * (domain[1] - domain[0]);
174 let y = domain[3] - (row as f64 + 0.5) / height as f64 * (domain[3] - domain[2]);
175 let components = function.evaluate(&[x, y]);
176 let color = components_to_device_color_icc(resolved_cs, &components, Some(icc_cache));
177 let idx = ((row * width + col) * 4) as usize;
178 rgba[idx] = (color.r * 255.0 + 0.5) as u8;
179 rgba[idx + 1] = (color.g * 255.0 + 0.5) as u8;
180 rgba[idx + 2] = (color.b * 255.0 + 0.5) as u8;
181 }
182 }
183
184 let image_matrix = Matrix::new(width as f64, 0.0, 0.0, -(height as f64), 0.0, height as f64);
185
186 display_list.push(DisplayElement::Image {
187 sample_data: std::sync::Arc::new(rgba),
188 params: ImageParams {
189 width,
190 height,
191 color_space: ImageColorSpace::PreconvertedRGBA,
192 bits_per_component: 8,
193 ctm: combined,
194 image_matrix,
195 interpolate: true,
196 mask_color: None,
197 alpha: 1.0,
198 blend_mode: 0,
199 overprint: false,
200 overprint_mode: 0,
201 opm_paired: false,
202 painted_channels: 0,
203 alpha_is_shape: false,
204 rendering_intent: 0,
205 },
206 });
207 Ok(())
208}
209
210#[allow(clippy::too_many_arguments)]
211fn handle_axial(
212 dict: &PdfDict,
213 gstate: &PdfGraphicsState,
214 resolver: &Resolver,
215 display_list: &mut DisplayList,
216 bbox: Option<[f64; 4]>,
217 extend: (bool, bool),
218 resolved_cs: &ResolvedColorSpace,
219 icc_cache: &mut IccCache,
220) -> Result<(), PdfError> {
221 let coords = dict
222 .get_array(b"Coords")
223 .ok_or(PdfError::Other("axial shading missing Coords".into()))?;
224 let vals: Vec<f64> = coords.iter().filter_map(|o| o.as_f64()).collect();
225 if vals.len() < 4 {
226 return Err(PdfError::Other("axial Coords needs 4 values".into()));
227 }
228
229 let function = parse_shading_function(dict, resolver)?;
230 let n_stops = function.min_samples().max(64).min(1024);
231 let color_stops = sample_function_to_stops_icc(&function, n_stops, resolved_cs, icc_cache);
232
233 let cs = resolved_cs_to_shading_cs(resolved_cs);
237
238 display_list.push(DisplayElement::AxialShading {
239 params: AxialShadingParams {
240 x0: vals[0],
241 y0: vals[1],
242 x1: vals[2],
243 y1: vals[3],
244 color_stops,
245 extend_start: extend.0,
246 extend_end: extend.1,
247 ctm: gstate.ctm,
248 bbox,
249 color_space: cs,
250 overprint: gstate.overprint,
251 overprint_mode: gstate.overprint_mode,
252 painted_channels: painted_channels_for_cs(resolved_cs),
253 alpha: gstate.fill_alpha,
254 blend_mode: gstate.blend_mode,
255 alpha_is_shape: gstate.alpha_is_shape,
256 spot_tint_blend: cs_has_spot_with_cmyk_alt(resolved_cs),
257 },
258 });
259 Ok(())
260}
261
262#[allow(clippy::too_many_arguments)]
263fn handle_radial(
264 dict: &PdfDict,
265 gstate: &PdfGraphicsState,
266 resolver: &Resolver,
267 display_list: &mut DisplayList,
268 bbox: Option<[f64; 4]>,
269 extend: (bool, bool),
270 resolved_cs: &ResolvedColorSpace,
271 icc_cache: &mut IccCache,
272) -> Result<(), PdfError> {
273 let coords = dict
274 .get_array(b"Coords")
275 .ok_or(PdfError::Other("radial shading missing Coords".into()))?;
276 let vals: Vec<f64> = coords.iter().filter_map(|o| o.as_f64()).collect();
277 if vals.len() < 6 {
278 return Err(PdfError::Other("radial Coords needs 6 values".into()));
279 }
280
281 let function = parse_shading_function(dict, resolver)?;
282 let n_stops = function.min_samples().max(64).min(1024);
283 let color_stops = sample_function_to_stops_icc(&function, n_stops, resolved_cs, icc_cache);
284
285 let cs = resolved_cs_to_shading_cs(resolved_cs);
290
291 display_list.push(DisplayElement::RadialShading {
292 params: RadialShadingParams {
293 x0: vals[0],
294 y0: vals[1],
295 r0: vals[2],
296 x1: vals[3],
297 y1: vals[4],
298 r1: vals[5],
299 color_stops,
300 extend_start: extend.0,
301 extend_end: extend.1,
302 ctm: gstate.ctm,
303 bbox,
304 color_space: cs,
305 overprint: gstate.overprint,
306 overprint_mode: gstate.overprint_mode,
307 painted_channels: painted_channels_for_cs(resolved_cs),
308 alpha: gstate.fill_alpha,
309 blend_mode: gstate.blend_mode,
310 alpha_is_shape: gstate.alpha_is_shape,
311 spot_tint_blend: cs_has_spot_with_cmyk_alt(resolved_cs),
312 },
313 });
314 Ok(())
315}
316
317fn cs_has_spot_with_cmyk_alt(cs: &ResolvedColorSpace) -> bool {
321 use stet_graphics::device::cmyk_channel_for_name;
322 match cs {
323 ResolvedColorSpace::Separation { name, alt, .. } => {
324 cmyk_channel_for_name(name) == 0
325 && matches!(alt.as_ref(), ResolvedColorSpace::DeviceCMYK)
326 }
327 ResolvedColorSpace::DeviceN { names, alt, .. } => {
328 matches!(alt.as_ref(), ResolvedColorSpace::DeviceCMYK)
329 && names.iter().any(|n| cmyk_channel_for_name(n) == 0)
330 }
331 _ => false,
332 }
333}
334
335#[allow(clippy::too_many_arguments)]
336fn handle_mesh(
337 shading_obj: &PdfObj,
338 dict: &PdfDict,
339 gstate: &PdfGraphicsState,
340 resolver: &Resolver,
341 display_list: &mut DisplayList,
342 shading_type: i32,
343 resolved_cs: &ResolvedColorSpace,
344 icc_cache: &mut IccCache,
345) -> Result<(), PdfError> {
346 let bpc = dict.get_int(b"BitsPerCoordinate").unwrap_or(8) as usize;
347 let bpco = dict.get_int(b"BitsPerComponent").unwrap_or(8) as usize;
348 let bpfl = dict.get_int(b"BitsPerFlag").unwrap_or(8) as usize;
349
350 let decode = dict
351 .get_array(b"Decode")
352 .map(|a| a.iter().filter_map(|o| o.as_f64()).collect::<Vec<_>>())
353 .unwrap_or_default();
354
355 let cs = resolved_cs_to_shading_cs(resolved_cs);
356 let cs_comps = resolved_cs.num_components();
360
361 let function = if dict.get(b"Function").is_some() {
365 parse_shading_function(dict, resolver).ok()
366 } else {
367 None
368 };
369 let n_comps = if function.is_some() {
370 let color_entries = decode.len().saturating_sub(4);
373 (color_entries / 2).max(1)
374 } else {
375 cs_comps
376 };
377
378 let data = resolver.stream_data_from_obj(shading_obj)?;
379
380 let mut triangles = match shading_type {
381 4 => {
382 stet_graphics::mesh_shading::parse_type4_mesh(&data, bpc, bpco, bpfl, &decode, n_comps)
383 }
384 5 => {
385 let vpr = dict.get_int(b"VerticesPerRow").unwrap_or(2) as usize;
386 stet_graphics::mesh_shading::parse_type5_mesh(&data, bpc, bpco, &decode, n_comps, vpr)
387 }
388 _ => return Ok(()),
389 };
390
391 let color_lut = if let Some(ref func) = function {
396 if n_comps == 1 {
397 let d_min = decode.get(4).copied().unwrap_or(0.0);
399 let d_max = decode.get(5).copied().unwrap_or(1.0);
400 let d_range = (d_max - d_min).abs().max(1e-10);
401
402 let lut_size = 256;
404 let mut lut = Vec::with_capacity(lut_size);
405 for i in 0..lut_size {
406 let t = i as f64 / (lut_size - 1) as f64;
407 let input = d_min + t * (d_max - d_min);
408 let components = func.evaluate(&[input]);
409 let color =
410 components_to_device_color_icc(resolved_cs, &components, Some(icc_cache));
411 lut.push(color);
412 }
413
414 for t in &mut triangles {
416 for v in [&mut t.v0, &mut t.v1, &mut t.v2] {
417 let raw = v.raw_components[0];
418 let normalized = ((raw - d_min) / d_range).clamp(0.0, 1.0);
419 v.raw_components = vec![normalized];
420 }
421 }
422
423 Some(std::sync::Arc::new(lut))
424 } else {
425 None
426 }
427 } else {
428 None
429 };
430
431 if let Some(ref func) = function {
434 if color_lut.is_some() {
435 let d_min = decode.get(4).copied().unwrap_or(0.0);
438 let d_max = decode.get(5).copied().unwrap_or(1.0);
439 for t in &mut triangles {
440 for v in [&mut t.v0, &mut t.v1, &mut t.v2] {
441 let input = d_min + v.raw_components[0] * (d_max - d_min);
442 let expanded = func.evaluate(&[input]);
443 let color =
444 components_to_device_color_icc(resolved_cs, &expanded, Some(icc_cache));
445 v.color = color;
446 }
447 }
448 } else {
449 for t in &mut triangles {
450 t.v0.raw_components = func.evaluate(&t.v0.raw_components);
451 t.v1.raw_components = func.evaluate(&t.v1.raw_components);
452 t.v2.raw_components = func.evaluate(&t.v2.raw_components);
453 }
454 }
455 }
456
457 if color_lut.is_none() {
458 for t in &mut triangles {
460 t.v0.color =
461 components_to_device_color_icc(resolved_cs, &t.v0.raw_components, Some(icc_cache));
462 t.v1.color =
463 components_to_device_color_icc(resolved_cs, &t.v1.raw_components, Some(icc_cache));
464 t.v2.color =
465 components_to_device_color_icc(resolved_cs, &t.v2.raw_components, Some(icc_cache));
466 }
467 }
468
469 for t in &mut triangles {
471 let (x, y) = gstate.ctm.transform_point(t.v0.x, t.v0.y);
472 t.v0.x = x;
473 t.v0.y = y;
474 let (x, y) = gstate.ctm.transform_point(t.v1.x, t.v1.y);
475 t.v1.x = x;
476 t.v1.y = y;
477 let (x, y) = gstate.ctm.transform_point(t.v2.x, t.v2.y);
478 t.v2.x = x;
479 t.v2.y = y;
480 }
481
482 let bbox = parse_bbox(dict);
483 let device_bbox = transform_bbox(&bbox, &gstate.ctm);
484
485 display_list.push(DisplayElement::MeshShading {
486 params: MeshShadingParams {
487 triangles,
488 ctm: Matrix::identity(),
489 bbox: device_bbox,
490 color_space: cs,
491 overprint: gstate.overprint,
492 overprint_mode: gstate.overprint_mode,
493 painted_channels: painted_channels_for_cs(resolved_cs),
494 color_lut,
495 alpha: gstate.fill_alpha,
496 blend_mode: gstate.blend_mode,
497 alpha_is_shape: gstate.alpha_is_shape,
498 },
499 });
500 Ok(())
501}
502
503#[allow(clippy::too_many_arguments)]
504fn handle_patches(
505 shading_obj: &PdfObj,
506 dict: &PdfDict,
507 gstate: &PdfGraphicsState,
508 resolver: &Resolver,
509 display_list: &mut DisplayList,
510 shading_type: i32,
511 resolved_cs: &ResolvedColorSpace,
512 icc_cache: &mut IccCache,
513) -> Result<(), PdfError> {
514 let bpc = dict.get_int(b"BitsPerCoordinate").unwrap_or(8) as usize;
515 let bpco = dict.get_int(b"BitsPerComponent").unwrap_or(8) as usize;
516 let bpfl = dict.get_int(b"BitsPerFlag").unwrap_or(8) as usize;
517
518 let decode = dict
519 .get_array(b"Decode")
520 .map(|a| a.iter().filter_map(|o| o.as_f64()).collect::<Vec<_>>())
521 .unwrap_or_default();
522
523 let cs = resolved_cs_to_shading_cs(resolved_cs);
524 let cs_comps = resolved_cs.num_components();
528
529 let function = if dict.get(b"Function").is_some() {
530 parse_shading_function(dict, resolver).ok()
531 } else {
532 None
533 };
534 let n_comps = if function.is_some() {
535 let color_entries = decode.len().saturating_sub(4);
536 (color_entries / 2).max(1)
537 } else {
538 cs_comps
539 };
540
541 let data = resolver.stream_data_from_obj(shading_obj)?;
542
543 let mut patches = match shading_type {
544 6 => stet_graphics::mesh_shading::parse_type6_patches(
545 &data, bpc, bpco, bpfl, &decode, n_comps,
546 ),
547 7 => stet_graphics::mesh_shading::parse_type7_patches(
548 &data, bpc, bpco, bpfl, &decode, n_comps,
549 ),
550 _ => return Ok(()),
551 };
552
553 let color_lut = if let Some(ref func) = function {
558 if n_comps == 1 {
559 let d_min = decode.get(4).copied().unwrap_or(0.0);
560 let d_max = decode.get(5).copied().unwrap_or(1.0);
561 let d_range = (d_max - d_min).abs().max(1e-10);
562
563 let lut_size = 256;
564 let mut lut = Vec::with_capacity(lut_size);
565 for i in 0..lut_size {
566 let t = i as f64 / (lut_size - 1) as f64;
567 let input = d_min + t * (d_max - d_min);
568 let components = func.evaluate(&[input]);
569 let color =
570 components_to_device_color_icc(resolved_cs, &components, Some(icc_cache));
571 lut.push(color);
572 }
573
574 for p in &mut patches {
576 for i in 0..4 {
577 let raw = p.raw_colors[i][0];
578 let normalized = ((raw - d_min) / d_range).clamp(0.0, 1.0);
579 p.raw_colors[i] = vec![normalized];
580 let idx = (normalized * 255.0).round() as usize;
582 p.colors[i] = lut[idx.min(255)].clone();
583 }
584 }
585 Some(std::sync::Arc::new(lut))
586 } else {
587 for p in &mut patches {
589 for i in 0..4 {
590 p.raw_colors[i] = func.evaluate(&p.raw_colors[i]);
591 }
592 }
593 for p in &mut patches {
594 for i in 0..4 {
595 p.colors[i] = components_to_device_color_icc(
596 resolved_cs,
597 &p.raw_colors[i],
598 Some(icc_cache),
599 );
600 }
601 }
602 None
603 }
604 } else {
605 for p in &mut patches {
607 for i in 0..4 {
608 p.colors[i] =
609 components_to_device_color_icc(resolved_cs, &p.raw_colors[i], Some(icc_cache));
610 }
611 }
612 None
613 };
614
615 for p in &mut patches {
617 for pt in &mut p.points {
618 let (x, y) = gstate.ctm.transform_point(pt.0, pt.1);
619 pt.0 = x;
620 pt.1 = y;
621 }
622 }
623
624 let bbox = parse_bbox(dict);
625 let device_bbox = transform_bbox(&bbox, &gstate.ctm);
626
627 display_list.push(DisplayElement::PatchShading {
628 params: PatchShadingParams {
629 patches,
630 ctm: Matrix::identity(),
631 bbox: device_bbox,
632 color_space: cs,
633 overprint: gstate.overprint,
634 overprint_mode: gstate.overprint_mode,
635 painted_channels: painted_channels_for_cs(resolved_cs),
636 color_lut,
637 alpha: gstate.fill_alpha,
638 blend_mode: gstate.blend_mode,
639 alpha_is_shape: gstate.alpha_is_shape,
640 },
641 });
642 Ok(())
643}
644
645fn parse_shading_function(dict: &PdfDict, resolver: &Resolver) -> Result<PdfFunction, PdfError> {
646 let fn_obj = dict
647 .get(b"Function")
648 .ok_or(PdfError::Other("shading missing Function".into()))?;
649 let fn_obj = resolver.deref(fn_obj)?;
650 if matches!(fn_obj, PdfObj::Null) {
652 return Err(PdfError::Other("shading Function is null".into()));
653 }
654 if let PdfObj::Array(arr) = &fn_obj {
655 if arr.len() == 1 {
656 return PdfFunction::parse(&arr[0], resolver);
657 }
658 if arr.len() > 1 {
662 let mut funcs = Vec::with_capacity(arr.len());
663 for item in arr {
664 funcs.push(PdfFunction::parse(item, resolver)?);
665 }
666 return Ok(PdfFunction::composite(funcs));
667 }
668 }
669 PdfFunction::parse(&fn_obj, resolver)
670}
671
672fn sample_function_to_stops_icc(
673 function: &PdfFunction,
674 n_samples: usize,
675 resolved_cs: &ResolvedColorSpace,
676 icc_cache: &mut IccCache,
677) -> Vec<ColorStop> {
678 let cmyk_tint_fn = match resolved_cs {
682 ResolvedColorSpace::Separation { alt, tint_fn, .. }
683 | ResolvedColorSpace::DeviceN { alt, tint_fn, .. }
684 if matches!(**alt, ResolvedColorSpace::DeviceCMYK) =>
685 {
686 tint_fn.as_ref()
687 }
688 _ => None,
689 };
690
691 let [d_min, d_max] = function.domain_0();
692 let span = d_max - d_min;
693
694 let disc_positions = function.discontinuity_positions();
697 let mut disc_ts: Vec<f64> = disc_positions
698 .iter()
699 .filter_map(|&d| {
700 if span.abs() < 1e-15 {
701 return None;
702 }
703 let t = (d - d_min) / span;
704 if t > 0.0 && t < 1.0 { Some(t) } else { None }
705 })
706 .collect();
707 disc_ts.sort_by(f64::total_cmp);
713 disc_ts.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
714
715 let n_samples = n_samples.max(2);
722 let mut sample_ts: Vec<f64> = (0..n_samples)
723 .map(|i| i as f64 / (n_samples - 1) as f64)
724 .collect();
725 let eps = 1e-10;
726 for &dt in &disc_ts {
727 sample_ts.push((dt - eps).max(0.0));
728 sample_ts.push(dt);
729 }
730 sample_ts.sort_by(f64::total_cmp);
731 sample_ts.dedup_by(|a, b| (*a - *b).abs() < 1e-14);
732
733 let is_spot_with_cmyk_alt = cmyk_tint_fn.is_some();
734
735 let mut stops = Vec::with_capacity(sample_ts.len());
736 for t in sample_ts {
737 let input = d_min + t * span;
738 let components = function.evaluate(&[input]);
739 let color = components_to_device_color_icc(resolved_cs, &components, Some(icc_cache));
740
741 let (raw_components, source_components) = if let Some(tint) = cmyk_tint_fn {
747 let cmyk = tint.evaluate(&components);
748 let raw = if cmyk.len() >= 4 {
749 cmyk[..4].to_vec()
750 } else {
751 components.clone()
752 };
753 (raw, components)
754 } else if is_spot_with_cmyk_alt {
755 (components.clone(), components)
757 } else {
758 (components, Vec::new())
759 };
760
761 stops.push(ColorStop {
762 position: t,
763 color,
764 raw_components,
765 source_components,
766 });
767 }
768 stops
769}
770
771fn transform_bbox(bbox: &Option<[f64; 4]>, ctm: &Matrix) -> Option<[f64; 4]> {
773 bbox.map(|b| {
774 let corners = [
775 ctm.transform_point(b[0], b[1]),
776 ctm.transform_point(b[2], b[1]),
777 ctm.transform_point(b[0], b[3]),
778 ctm.transform_point(b[2], b[3]),
779 ];
780 let x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
781 let y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
782 let x_max = corners
783 .iter()
784 .map(|c| c.0)
785 .fold(f64::NEG_INFINITY, f64::max);
786 let y_max = corners
787 .iter()
788 .map(|c| c.1)
789 .fold(f64::NEG_INFINITY, f64::max);
790 [x_min, y_min, x_max, y_max]
791 })
792}
793
794fn parse_bbox(dict: &PdfDict) -> Option<[f64; 4]> {
795 dict.get_array(b"BBox").and_then(|arr| {
796 let vals: Vec<f64> = arr.iter().filter_map(|o| o.as_f64()).collect();
797 if vals.len() == 4 {
798 Some([vals[0], vals[1], vals[2], vals[3]])
799 } else {
800 None
801 }
802 })
803}
804
805fn parse_extend(dict: &PdfDict) -> (bool, bool) {
806 dict.get_array(b"Extend")
807 .and_then(|arr| {
808 if arr.len() == 2 {
809 let a = matches!(arr[0], PdfObj::Bool(true));
810 let b = matches!(arr[1], PdfObj::Bool(true));
811 Some((a, b))
812 } else {
813 None
814 }
815 })
816 .unwrap_or((false, false))
817}
818
819fn resolve_shading_resolved_cs(dict: &PdfDict, resolver: &Resolver) -> ResolvedColorSpace {
821 if let Some(cs_obj) = dict.get(b"ColorSpace")
822 && let Ok(resolved) = resolve_color_space_obj(cs_obj, resolver)
823 {
824 return resolved;
825 }
826 ResolvedColorSpace::DeviceRGB
827}
828
829fn resolved_cs_to_shading_cs(cs: &ResolvedColorSpace) -> ShadingColorSpace {
833 match cs {
834 ResolvedColorSpace::DeviceGray => ShadingColorSpace::DeviceGray,
835 ResolvedColorSpace::DeviceRGB => ShadingColorSpace::DeviceRGB,
836 ResolvedColorSpace::DeviceCMYK => ShadingColorSpace::DeviceCMYK,
837 ResolvedColorSpace::ICCBased {
840 n,
841 profile_hash: Some(hash),
842 profile_data: Some(data),
843 ..
844 } if *n != 1 && *n != 4 => ShadingColorSpace::ICCBased {
845 n: *n as u32,
846 profile_hash: *hash,
847 profile_data: Arc::clone(data),
848 },
849 ResolvedColorSpace::ICCBased { n, .. } => match n {
850 1 => ShadingColorSpace::DeviceGray,
851 4 => ShadingColorSpace::DeviceCMYK,
852 _ => ShadingColorSpace::DeviceRGB,
853 },
854 ResolvedColorSpace::Indexed { base, .. } => resolved_cs_to_shading_cs(base),
856 ResolvedColorSpace::Separation { name, alt, tint_fn }
861 if matches!(**alt, ResolvedColorSpace::DeviceCMYK) =>
862 {
863 match tint_fn {
864 Some(tint) => ShadingColorSpace::Separation {
865 name: name.clone(),
866 alternate: SimpleColorSpace::DeviceCMYK,
867 tint_function: sample_tint_function_1d(tint, 16),
868 },
869 None => ShadingColorSpace::DeviceCMYK,
870 }
871 }
872 ResolvedColorSpace::DeviceN {
873 names,
874 alt,
875 tint_fn,
876 } if matches!(**alt, ResolvedColorSpace::DeviceCMYK) => match tint_fn {
877 Some(tint) => ShadingColorSpace::DeviceN {
878 names: names.clone(),
879 alternate: SimpleColorSpace::DeviceCMYK,
880 tint_function: sample_tint_function_nd(tint, names.len(), 8),
881 },
882 None => ShadingColorSpace::DeviceCMYK,
883 },
884 _ => ShadingColorSpace::DeviceRGB,
885 }
886}
887
888fn sample_tint_function_1d(tint: &PdfFunction, samples_per_dim: usize) -> SpotTintFunction {
893 let mut cmyk_samples = Vec::with_capacity(samples_per_dim * 4);
894 for i in 0..samples_per_dim {
895 let t = i as f64 / (samples_per_dim - 1).max(1) as f64;
896 let out = tint.evaluate(&[t]);
897 for k in 0..4 {
898 cmyk_samples.push(out.get(k).copied().unwrap_or(0.0));
899 }
900 }
901 SpotTintFunction {
902 input_dim: 1,
903 samples_per_dim,
904 cmyk_samples: Arc::new(cmyk_samples),
905 }
906}
907
908fn sample_tint_function_nd(
912 tint: &PdfFunction,
913 input_dim: usize,
914 samples_per_dim: usize,
915) -> SpotTintFunction {
916 let total = samples_per_dim.pow(input_dim as u32);
917 let mut cmyk_samples = Vec::with_capacity(total * 4);
918 let mut input = vec![0.0_f64; input_dim];
919 for idx in 0..total {
920 let mut rem = idx;
922 for axis in (0..input_dim).rev() {
923 let i = rem % samples_per_dim;
924 rem /= samples_per_dim;
925 input[axis] = i as f64 / (samples_per_dim - 1).max(1) as f64;
926 }
927 let out = tint.evaluate(&input);
928 for k in 0..4 {
929 cmyk_samples.push(out.get(k).copied().unwrap_or(0.0));
930 }
931 }
932 SpotTintFunction {
933 input_dim,
934 samples_per_dim,
935 cmyk_samples: Arc::new(cmyk_samples),
936 }
937}