1use std::collections::{HashMap, HashSet};
8use std::hash::{Hash, Hasher};
9use std::sync::{Arc, Mutex};
10
11#[cfg(feature = "parallel")]
12use rayon::prelude::*;
13use stet_tiny_skia::{
14 BlendMode, Color, FillRule as SkiaFillRule, LineCap as SkiaLineCap, LineJoin as SkiaLineJoin,
15 Mask, Paint, PathBuilder, Pixmap, Stroke, StrokeDash, Transform,
16};
17
18use stet_core::device::OutputDevice;
19use stet_fonts::geometry::{Matrix, PathSegment, PsPath};
20use stet_graphics::color::{DeviceColor, FillRule, LineCap, LineJoin};
21use stet_graphics::device::{
22 AxialShadingParams, ClipParams, FillParams, ImageColorSpace, ImageParams, MeshShadingParams,
23 PageSinkFactory, PatchShadingParams, RadialShadingParams, ShadingColorSpace, ShadingVertex,
24 StrokeParams, TintLookupTable,
25};
26use stet_graphics::icc::IccCache;
27use stet_graphics::layer_set::LayerSet;
28
29#[derive(Clone, Copy)]
31struct ClipRect {
32 x0: u32,
33 y0: u32, x1: u32,
35 y1: u32, }
37
38impl ClipRect {
39 fn intersect(&self, other: &ClipRect) -> ClipRect {
41 ClipRect {
42 x0: self.x0.max(other.x0),
43 y0: self.y0.max(other.y0),
44 x1: self.x1.min(other.x1),
45 y1: self.y1.min(other.y1),
46 }
47 }
48
49 fn is_empty(&self) -> bool {
50 self.x0 >= self.x1 || self.y0 >= self.y1
51 }
52
53 fn is_full_page(&self, w: u32, h: u32) -> bool {
55 self.x0 == 0 && self.y0 == 0 && self.x1 == w && self.y1 == h
56 }
57
58 fn make_mask(self, w: u32, h: u32) -> Option<Mask> {
60 if self.is_empty() {
61 return None;
62 }
63 let mut mask = Mask::new(w, h)?;
64 let data = mask.data_mut();
65 let stride = w as usize;
66 for y in self.y0..self.y1 {
67 let row_start = y as usize * stride + self.x0 as usize;
68 let row_end = y as usize * stride + self.x1 as usize;
69 data[row_start..row_end].fill(255);
70 }
71 Some(mask)
72 }
73}
74
75enum ClipRegion {
77 Rect(ClipRect),
78 Mask(Mask),
79}
80
81pub struct SkiaDevice {
83 pixmap: Pixmap,
84 page_w: u32,
87 page_h: u32,
88 dpi: f64,
90 clip_region: Option<ClipRegion>,
91 clip_mask_cache: HashMap<u64, Mask>,
94 clip_mask_seen: HashSet<u64>,
95 spare_mask: Option<Mask>,
97 pending_render: Option<std::sync::mpsc::Receiver<Result<(), String>>>,
100 sink_factory: Box<dyn PageSinkFactory>,
102 system_cmyk_bytes: Option<std::sync::Arc<Vec<u8>>>,
104 render_icc_cache: Option<IccCache>,
106 no_aa: bool,
108 use_viewport_path: bool,
113 layer_set: LayerSet,
118}
119
120impl SkiaDevice {
121 pub fn new(width: u32, height: u32) -> Self {
127 Self::with_sink_factory(width, height, Box::new(crate::PngSinkFactory))
128 }
129
130 pub fn with_sink_factory(
132 width: u32,
133 height: u32,
134 sink_factory: Box<dyn PageSinkFactory>,
135 ) -> Self {
136 let dpi = height as f64 * 72.0 / 792.0;
139
140 let pixmap = Pixmap::new(1, 1).expect("Failed to create placeholder pixmap");
144 Self {
145 pixmap,
146 page_w: width,
147 page_h: height,
148 dpi,
149 clip_region: None,
150 clip_mask_cache: HashMap::new(),
151 clip_mask_seen: HashSet::new(),
152 spare_mask: None,
153 pending_render: None,
154 sink_factory,
155 system_cmyk_bytes: None,
156 render_icc_cache: None,
157 no_aa: false,
158 use_viewport_path: false,
159 layer_set: LayerSet::new(),
160 }
161 }
162
163 pub fn set_use_viewport_path(&mut self, on: bool) {
166 self.use_viewport_path = on;
167 }
168
169 pub fn set_layer_set(&mut self, layer_set: LayerSet) {
176 self.layer_set = layer_set;
177 }
178
179 pub fn layer_set(&self) -> &LayerSet {
181 &self.layer_set
182 }
183
184 fn ensure_full_pixmap(&mut self) {
187 if self.pixmap.width() != self.page_w || self.pixmap.height() != self.page_h {
188 self.pixmap =
189 Pixmap::new(self.page_w, self.page_h).expect("Failed to create page pixmap");
190 self.pixmap.fill(Color::WHITE);
191 }
192 }
193
194 pub fn pixmap(&self) -> &Pixmap {
196 &self.pixmap
197 }
198
199 pub fn set_system_cmyk_bytes(&mut self, bytes: std::sync::Arc<Vec<u8>>) {
201 self.system_cmyk_bytes = Some(bytes);
202 }
203
204 pub fn set_no_aa(&mut self, no_aa: bool) {
206 self.no_aa = no_aa;
207 }
208}
209
210fn to_transform(m: &Matrix) -> Transform {
212 Transform::from_row(
213 m.a as f32,
214 m.b as f32,
215 m.c as f32,
216 m.d as f32,
217 m.tx as f32,
218 m.ty as f32,
219 )
220}
221
222fn to_paint(color: &DeviceColor) -> Paint<'static> {
224 to_paint_alpha(color, 1.0, 0, false)
225}
226
227fn to_paint_alpha(color: &DeviceColor, alpha: f64, blend_mode: u8, no_aa: bool) -> Paint<'static> {
229 let mut paint = Paint::default();
230 let a = (alpha * 255.0).round().clamp(0.0, 255.0) as u8;
231 paint.set_color_rgba8(
232 (color.r * 255.0).round().clamp(0.0, 255.0) as u8,
233 (color.g * 255.0).round().clamp(0.0, 255.0) as u8,
234 (color.b * 255.0).round().clamp(0.0, 255.0) as u8,
235 a,
236 );
237 paint.anti_alias = !no_aa;
238 paint.blend_mode = u8_to_blend_mode(blend_mode);
239 paint
240}
241
242fn u8_to_blend_mode(mode: u8) -> BlendMode {
244 match mode {
245 1 => BlendMode::Multiply,
246 2 => BlendMode::Screen,
247 3 => BlendMode::Overlay,
248 4 => BlendMode::Darken,
249 5 => BlendMode::Lighten,
250 6 => BlendMode::ColorDodge,
251 7 => BlendMode::ColorBurn,
252 8 => BlendMode::HardLight,
253 9 => BlendMode::SoftLight,
254 10 => BlendMode::Difference,
255 11 => BlendMode::Exclusion,
256 12 => BlendMode::Hue,
257 13 => BlendMode::Saturation,
258 14 => BlendMode::Color,
259 15 => BlendMode::Luminosity,
260 _ => BlendMode::SourceOver,
261 }
262}
263
264const MAX_PATH_COORD: f32 = 1e6;
270
271fn build_skia_path(path: &PsPath) -> Option<stet_tiny_skia::Path> {
272 let mut pb = PathBuilder::new();
273
274 for seg in &path.segments {
275 match seg {
276 PathSegment::MoveTo(x, y) => {
277 pb.move_to(*x as f32, *y as f32);
278 }
279 PathSegment::LineTo(x, y) => {
280 pb.line_to(*x as f32, *y as f32);
281 }
282 PathSegment::CurveTo {
283 x1,
284 y1,
285 x2,
286 y2,
287 x3,
288 y3,
289 } => {
290 pb.cubic_to(
291 *x1 as f32, *y1 as f32, *x2 as f32, *y2 as f32, *x3 as f32, *y3 as f32,
292 );
293 }
294 PathSegment::ClosePath => {
295 pb.close();
296 }
297 }
298 }
299
300 let result = pb.finish()?;
301
302 let b = result.bounds();
306 if b.left().abs() > MAX_PATH_COORD
307 || b.top().abs() > MAX_PATH_COORD
308 || b.right().abs() > MAX_PATH_COORD
309 || b.bottom().abs() > MAX_PATH_COORD
310 {
311 return None;
312 }
313
314 Some(result)
315}
316
317fn is_degenerate_fill(path: &PsPath) -> bool {
329 let mut x_min = f64::INFINITY;
330 let mut x_max = f64::NEG_INFINITY;
331 let mut y_min = f64::INFINITY;
332 let mut y_max = f64::NEG_INFINITY;
333
334 for seg in &path.segments {
335 let (x, y) = match seg {
336 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => (*x, *y),
337 PathSegment::CurveTo { .. } => return false,
339 PathSegment::ClosePath => continue,
340 };
341 x_min = x_min.min(x);
342 x_max = x_max.max(x);
343 y_min = y_min.min(y);
344 y_max = y_max.max(y);
345 }
346
347 if x_min > x_max {
348 return false; }
350
351 let w = x_max - x_min;
352 let h = y_max - y_min;
353
354 let eps = 1e-6;
358 (w < eps && h > eps) || (h < eps && w > eps)
359}
360
361fn to_fill_rule(rule: &FillRule) -> SkiaFillRule {
366 match rule {
367 FillRule::NonZeroWinding => SkiaFillRule::Winding,
368 FillRule::EvenOdd => SkiaFillRule::EvenOdd,
369 _ => SkiaFillRule::Winding,
370 }
371}
372
373fn to_line_cap(cap: LineCap) -> SkiaLineCap {
375 match cap {
376 LineCap::Butt => SkiaLineCap::Butt,
377 LineCap::Round => SkiaLineCap::Round,
378 LineCap::Square => SkiaLineCap::Square,
379 _ => SkiaLineCap::Butt,
380 }
381}
382
383fn to_line_join(join: LineJoin) -> SkiaLineJoin {
385 match join {
386 LineJoin::Miter => SkiaLineJoin::Miter,
387 LineJoin::Round => SkiaLineJoin::Round,
388 LineJoin::Bevel => SkiaLineJoin::Bevel,
389 _ => SkiaLineJoin::Miter,
390 }
391}
392
393fn detect_rect(path: &PsPath, page_w: u32, page_h: u32) -> Option<ClipRect> {
396 let segs = &path.segments;
397 let (move_to, lines, _has_close) = match segs.len() {
401 5 => {
402 if !matches!(segs[4], PathSegment::ClosePath) {
404 return None;
405 }
406 (&segs[0], &segs[1..4], true)
407 }
408 6 => {
409 if !matches!(segs[5], PathSegment::ClosePath) {
411 return None;
412 }
413 (&segs[0], &segs[1..5], true)
414 }
415 4 => {
416 (&segs[0], &segs[1..4], false)
418 }
419 _ => return None,
420 };
421
422 let PathSegment::MoveTo(mx, my) = move_to else {
423 return None;
424 };
425
426 let mut pts = vec![(*mx, *my)];
428 for seg in lines {
429 match seg {
430 PathSegment::LineTo(x, y) => pts.push((*x, *y)),
431 _ => return None,
432 }
433 }
434
435 if pts.len() == 5 {
437 let (fx, fy) = pts[0];
438 let (lx, ly) = pts[4];
439 if (fx - lx).abs() > 0.01 || (fy - ly).abs() > 0.01 {
440 return None;
441 }
442 pts.truncate(4);
443 }
444
445 for i in 0..4 {
447 let (x1, y1) = pts[i];
448 let (x2, y2) = pts[(i + 1) % 4];
449 let dx = (x2 - x1).abs();
450 let dy = (y2 - y1).abs();
451 if dx > 0.01 && dy > 0.01 {
452 return None; }
454 }
455
456 let min_x = pts.iter().map(|p| p.0).fold(f64::INFINITY, f64::min);
458 let min_y = pts.iter().map(|p| p.1).fold(f64::INFINITY, f64::min);
459 let max_x = pts.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max);
460 let max_y = pts.iter().map(|p| p.1).fold(f64::NEG_INFINITY, f64::max);
461
462 let x0 = (min_x.floor().max(0.0) as u32).min(page_w);
464 let y0 = (min_y.floor().max(0.0) as u32).min(page_h);
465 let x1 = (max_x.ceil().max(0.0) as u32).min(page_w);
466 let y1 = (max_y.ceil().max(0.0) as u32).min(page_h);
467
468 Some(ClipRect { x0, y0, x1, y1 })
469}
470
471fn intersect_mask_with_rect(mask: &mut Mask, rect: &ClipRect, w: u32, h: u32) {
473 let data = mask.data_mut();
474 let stride = w as usize;
475
476 if rect.y0 > 0 {
478 let end = (rect.y0 as usize * stride).min(data.len());
479 data[..end].fill(0);
480 }
481
482 if rect.y1 < h {
484 let start = (rect.y1 as usize * stride).min(data.len());
485 data[start..].fill(0);
486 }
487
488 for y in rect.y0..rect.y1.min(h) {
490 let row_start = y as usize * stride;
491 if rect.x0 > 0 {
493 let end = row_start + rect.x0 as usize;
494 data[row_start..end].fill(0);
495 }
496 if rect.x1 < w {
498 let start = row_start + rect.x1 as usize;
499 let end = row_start + stride;
500 data[start..end].fill(0);
501 }
502 }
503}
504
505fn resolve_clip_mask<'a>(
510 clip_region: &'a Option<ClipRegion>,
511 temp_mask: &'a mut Option<Mask>,
512 w: u32,
513 h: u32,
514) -> Option<Option<&'a Mask>> {
515 match clip_region {
516 None => Some(None),
517 Some(ClipRegion::Mask(m)) => Some(Some(m)),
518 Some(ClipRegion::Rect(rect)) => {
519 if rect.is_empty() {
520 return None; }
522 if rect.is_full_page(w, h) {
523 return Some(None); }
525 *temp_mask = rect.make_mask(w, h);
526 Some(temp_mask.as_ref())
527 }
528 }
529}
530
531fn hash_clip_path(path: &PsPath, fill_rule: &FillRule) -> u64 {
534 let mut hasher = std::collections::hash_map::DefaultHasher::new();
535 std::mem::discriminant(fill_rule).hash(&mut hasher);
536 for seg in &path.segments {
537 match seg {
538 PathSegment::MoveTo(x, y) => {
539 0u8.hash(&mut hasher);
540 x.to_bits().hash(&mut hasher);
541 y.to_bits().hash(&mut hasher);
542 }
543 PathSegment::LineTo(x, y) => {
544 1u8.hash(&mut hasher);
545 x.to_bits().hash(&mut hasher);
546 y.to_bits().hash(&mut hasher);
547 }
548 PathSegment::CurveTo {
549 x1,
550 y1,
551 x2,
552 y2,
553 x3,
554 y3,
555 } => {
556 2u8.hash(&mut hasher);
557 x1.to_bits().hash(&mut hasher);
558 y1.to_bits().hash(&mut hasher);
559 x2.to_bits().hash(&mut hasher);
560 y2.to_bits().hash(&mut hasher);
561 x3.to_bits().hash(&mut hasher);
562 y3.to_bits().hash(&mut hasher);
563 }
564 PathSegment::ClosePath => {
565 3u8.hash(&mut hasher);
566 }
567 }
568 }
569 hasher.finish()
570}
571
572fn intersect_masks(dst: &mut Mask, src: &Mask) {
574 let dst_data = dst.data_mut();
575 let src_data = src.data();
576 for (d, s) in dst_data.iter_mut().zip(src_data.iter()) {
577 *d = ((*d as u16 * *s as u16 + 127) / 255) as u8;
578 }
579}
580
581use stet_graphics::display_list::{DisplayElement, DisplayList};
584
585struct BandState {
587 clip_region: Option<ClipRegion>,
588 spare_mask: Option<Mask>,
589 clip_mask_cache: HashMap<u64, Mask>,
591 clip_mask_seen: HashSet<u64>,
593 mask_pool: Vec<Mask>,
595 cmyk_buffer: Option<Vec<f32>>,
599 op_bg_snapshot: Option<Vec<u8>>,
606 op_touched: Option<Vec<u8>>,
611 spot_mask: Option<Vec<u8>>,
619}
620
621const MAX_POOL_MASKS: usize = 8;
624
625impl BandState {
626 #[allow(dead_code)]
628 fn recycle_cache(&mut self) {
629 for (_, mask) in self.clip_mask_cache.drain() {
630 if self.mask_pool.len() < MAX_POOL_MASKS {
631 self.mask_pool.push(mask);
632 }
633 }
635 }
636
637 fn recycle_mask(&mut self, mask: Mask) {
639 if self.mask_pool.len() < MAX_POOL_MASKS {
640 self.mask_pool.push(mask);
641 }
642 }
643
644 fn take_mask(&mut self, w: u32, h: u32) -> Mask {
646 self.spare_mask
647 .take()
648 .or_else(|| self.mask_pool.pop())
649 .unwrap_or_else(|| Mask::new(w, h).expect("Failed to create mask"))
650 }
651
652 fn take_op_buffers(&mut self, w: u32, h: u32) -> (Vec<u8>, Vec<u8>) {
657 let n = w as usize * h as usize;
658 let bg = self
659 .op_bg_snapshot
660 .take()
661 .unwrap_or_else(|| vec![0u8; n * 4]);
662 let touched = self.op_touched.take().unwrap_or_else(|| vec![0u8; n]);
663 (bg, touched)
664 }
665
666 fn restore_op_buffers(&mut self, bg: Vec<u8>, touched: Vec<u8>) {
668 self.op_bg_snapshot = Some(bg);
669 self.op_touched = Some(touched);
670 }
671
672 fn take_spot_mask(&mut self, w: u32, h: u32) -> Vec<u8> {
674 let n = w as usize * h as usize;
675 self.spot_mask.take().unwrap_or_else(|| vec![0u8; n])
676 }
677
678 fn restore_spot_mask(&mut self, mask: Vec<u8>) {
680 self.spot_mask = Some(mask);
681 }
682
683 #[allow(dead_code)]
687 fn invalidate_op_snapshot(
688 &mut self,
689 bbox_x0: usize,
690 bbox_y0: usize,
691 bbox_x1: usize,
692 bbox_y1: usize,
693 stride: usize,
694 ) {
695 if let Some(touched) = self.op_touched.as_mut() {
696 for y in bbox_y0..bbox_y1 {
697 let row = y * stride;
698 for x in bbox_x0..bbox_x1 {
699 touched[row + x] = 0;
700 }
701 }
702 }
703 }
704}
705
706struct RenderContext<'a> {
711 vp_x: f32,
713 vp_y: f32,
715 scale_x: f32,
717 scale_y: f32,
719 out_w: u32,
721 out_h: u32,
723 effective_dpi: f64,
725 icc: Option<&'a IccCache>,
727 image_cache: Option<&'a ImageCache>,
729 preprocessed: Option<&'a [Option<PreprocessedImage>]>,
731 elem_idx: usize,
733 no_aa: bool,
735 opm_zero_transparent: bool,
737 knockout_painter_pass: KnockoutPainterPass,
753 parent_group_isolated: bool,
761 alpha_extraction_pass: bool,
766 layer_set: &'a LayerSet,
769}
770
771#[derive(Clone, Copy, PartialEq, Eq)]
774enum KnockoutPainterPass {
775 None,
777 ColorPass,
779 CoveragePass,
782}
783
784impl RenderContext<'_> {
785 fn transform(&self, m: &Matrix) -> Transform {
787 viewport_transform(
788 to_transform(m),
789 self.vp_x,
790 self.vp_y,
791 self.scale_x,
792 self.scale_y,
793 )
794 }
795}
796
797struct YBBox {
799 y_min: f64,
800 y_max: f64,
801}
802
803struct ClipEpoch {
808 start_idx: usize,
810 end_idx: usize,
812 paint_bbox: Option<YBBox>,
815 has_erase_page: bool,
817}
818
819fn select_band_height(w: u32, h: u32) -> u32 {
822 if w == 0 || h == 0 {
823 return h;
824 }
825 let per_row = w as u64 * 6;
827 let budget = 2 * 1024 * 1024u64; let max_rows = budget / per_row;
829
830 let band = if max_rows >= h as u64 {
832 h
833 } else {
834 let mut p = 1u32;
835 while (p as u64) * 2 <= max_rows {
836 p *= 2;
837 }
838 p.clamp(128, h)
844 };
845
846 if h.div_ceil(band) <= 2 {
848 return h;
849 }
850 band
851}
852
853fn contains_clip_op(list: &DisplayList) -> bool {
860 list.elements().iter().any(|e| match e {
861 DisplayElement::Clip { .. } | DisplayElement::InitClip => true,
862 DisplayElement::OcgGroup { elements, .. } => contains_clip_op(elements),
863 DisplayElement::Group { elements, .. } => contains_clip_op(elements),
864 DisplayElement::SoftMasked { content, .. } => contains_clip_op(content),
865 _ => false,
866 })
867}
868
869fn precompute_bboxes(list: &DisplayList, dpi: f64) -> Vec<Option<YBBox>> {
875 list.elements()
876 .iter()
877 .map(|elem| match elem {
878 DisplayElement::Fill { path, params } => fill_device_y_bbox(path, ¶ms.ctm),
879 DisplayElement::Stroke { path, params } => stroke_device_y_bbox(path, params, dpi),
880 DisplayElement::Image { params, .. } => image_y_bbox(params),
881 DisplayElement::AxialShading { params } => {
882 shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
883 }
884 DisplayElement::RadialShading { params } => {
885 shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
886 }
887 DisplayElement::MeshShading { params } => {
888 shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
889 }
890 DisplayElement::PatchShading { params } => {
891 shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
892 }
893 DisplayElement::PatternFill { params } => pattern_fill_y_bbox(params),
894 DisplayElement::Group { params, .. } => Some(YBBox {
895 y_min: params.bbox[1],
896 y_max: params.bbox[3],
897 }),
898 DisplayElement::SoftMasked { params, .. } => Some(YBBox {
899 y_min: params.bbox[1],
900 y_max: params.bbox[3],
901 }),
902 DisplayElement::OcgGroup {
903 elements,
904 visibility,
905 } => {
906 if !visibility.default_visible() && !contains_clip_op(elements) {
912 return None;
913 }
914 let child_bboxes = precompute_bboxes(elements, dpi);
915 let mut y_min = f64::INFINITY;
916 let mut y_max = f64::NEG_INFINITY;
917 for cb in child_bboxes.into_iter().flatten() {
918 y_min = y_min.min(cb.y_min);
919 y_max = y_max.max(cb.y_max);
920 }
921 if y_min <= y_max {
922 Some(YBBox { y_min, y_max })
923 } else {
924 None
925 }
926 }
927 _ => None, })
929 .collect()
930}
931
932fn shading_y_bbox_from_bbox(bbox: &Option<[f64; 4]>, ctm: &Matrix) -> Option<YBBox> {
936 if let Some(bbox) = bbox {
937 let corners = [
938 (bbox[0], bbox[1]),
939 (bbox[2], bbox[1]),
940 (bbox[0], bbox[3]),
941 (bbox[2], bbox[3]),
942 ];
943 let mut y_min = f64::INFINITY;
944 let mut y_max = f64::NEG_INFINITY;
945 for (x, y) in &corners {
946 let (_, dy) = ctm.transform_point(*x, *y);
947 y_min = y_min.min(dy);
948 y_max = y_max.max(dy);
949 }
950 Some(YBBox { y_min, y_max })
951 } else {
952 Some(YBBox {
955 y_min: 0.0,
956 y_max: 1e9,
957 })
958 }
959}
960
961fn stroke_device_y_bbox(path: &PsPath, params: &StrokeParams, dpi: f64) -> Option<YBBox> {
968 let m = ¶ms.ctm;
969 let is_identity =
970 m.a == 1.0 && m.b == 0.0 && m.c == 0.0 && m.d == 1.0 && m.tx == 0.0 && m.ty == 0.0;
971
972 let effective_lw = params.line_width.max(hairline_min_width(¶ms.ctm, dpi));
974
975 if is_identity {
976 return path_y_bbox(path).map(|mut bbox| {
978 let expand = effective_lw * params.miter_limit * 0.5;
979 bbox.y_min -= expand;
980 bbox.y_max += expand;
981 bbox
982 });
983 }
984
985 let (mut x_min, mut x_max) = (f64::INFINITY, f64::NEG_INFINITY);
988 let (mut y_min, mut y_max) = (f64::INFINITY, f64::NEG_INFINITY);
989 for seg in &path.segments {
990 match seg {
991 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => {
992 x_min = x_min.min(*x);
993 x_max = x_max.max(*x);
994 y_min = y_min.min(*y);
995 y_max = y_max.max(*y);
996 }
997 PathSegment::CurveTo {
998 x1,
999 y1,
1000 x2,
1001 y2,
1002 x3,
1003 y3,
1004 } => {
1005 x_min = x_min.min(*x1).min(*x2).min(*x3);
1006 x_max = x_max.max(*x1).max(*x2).max(*x3);
1007 y_min = y_min.min(*y1).min(*y2).min(*y3);
1008 y_max = y_max.max(*y1).max(*y2).max(*y3);
1009 }
1010 PathSegment::ClosePath => {}
1011 }
1012 }
1013 if x_min > x_max {
1014 return None;
1015 }
1016
1017 let corners = [
1019 (x_min, y_min),
1020 (x_max, y_min),
1021 (x_min, y_max),
1022 (x_max, y_max),
1023 ];
1024 let mut dev_y_min = f64::INFINITY;
1025 let mut dev_y_max = f64::NEG_INFINITY;
1026 for (x, y) in &corners {
1027 let dy = m.b * x + m.d * y + m.ty;
1028 dev_y_min = dev_y_min.min(dy);
1029 dev_y_max = dev_y_max.max(dy);
1030 }
1031
1032 let col_y_len = (m.c * m.c + m.d * m.d).sqrt().max(1.0);
1035 let expand = effective_lw * col_y_len * params.miter_limit * 0.5;
1036 dev_y_min -= expand;
1037 dev_y_max += expand;
1038
1039 Some(YBBox {
1040 y_min: dev_y_min,
1041 y_max: dev_y_max,
1042 })
1043}
1044
1045fn fill_device_y_bbox(path: &PsPath, ctm: &Matrix) -> Option<YBBox> {
1050 let is_identity = ctm.a == 1.0
1051 && ctm.b == 0.0
1052 && ctm.c == 0.0
1053 && ctm.d == 1.0
1054 && ctm.tx == 0.0
1055 && ctm.ty == 0.0;
1056 if is_identity {
1057 return path_y_bbox(path);
1058 }
1059 let bbox = path_full_bbox(path)?;
1060 let corners = [
1061 (bbox.x_min, bbox.y_min),
1062 (bbox.x_max, bbox.y_min),
1063 (bbox.x_min, bbox.y_max),
1064 (bbox.x_max, bbox.y_max),
1065 ];
1066 let mut dev_y_min = f64::INFINITY;
1067 let mut dev_y_max = f64::NEG_INFINITY;
1068 for (x, y) in &corners {
1069 let dy = ctm.b * x + ctm.d * y + ctm.ty;
1070 dev_y_min = dev_y_min.min(dy);
1071 dev_y_max = dev_y_max.max(dy);
1072 }
1073 Some(YBBox {
1074 y_min: dev_y_min,
1075 y_max: dev_y_max,
1076 })
1077}
1078
1079fn path_y_bbox(path: &PsPath) -> Option<YBBox> {
1081 let mut y_min = f64::INFINITY;
1082 let mut y_max = f64::NEG_INFINITY;
1083 for seg in &path.segments {
1084 match seg {
1085 PathSegment::MoveTo(_, y) | PathSegment::LineTo(_, y) => {
1086 y_min = y_min.min(*y);
1087 y_max = y_max.max(*y);
1088 }
1089 PathSegment::CurveTo { y1, y2, y3, .. } => {
1090 y_min = y_min.min(*y1).min(*y2).min(*y3);
1091 y_max = y_max.max(*y1).max(*y2).max(*y3);
1092 }
1093 PathSegment::ClosePath => {}
1094 }
1095 }
1096 if y_min <= y_max {
1097 Some(YBBox { y_min, y_max })
1098 } else {
1099 None
1100 }
1101}
1102
1103fn image_y_bbox(params: &ImageParams) -> Option<YBBox> {
1105 let image_inv = params.image_matrix.invert()?;
1106 let combined = params.ctm.concat(&image_inv);
1107 let corners = [
1108 (0.0, 0.0),
1109 (params.width as f64, 0.0),
1110 (params.width as f64, params.height as f64),
1111 (0.0, params.height as f64),
1112 ];
1113 let mut y_min = f64::INFINITY;
1114 let mut y_max = f64::NEG_INFINITY;
1115 for (x, y) in &corners {
1116 let (_, dy) = combined.transform_point(*x, *y);
1117 y_min = y_min.min(dy);
1118 y_max = y_max.max(dy);
1119 }
1120 Some(YBBox { y_min, y_max })
1121}
1122
1123fn precompute_clip_seen(list: &DisplayList) -> HashSet<u64> {
1126 let mut counts: HashMap<u64, u32> = HashMap::new();
1127 for elem in list.elements() {
1128 if let DisplayElement::Clip { path, params } = elem {
1129 let hash = hash_clip_path(path, ¶ms.fill_rule);
1130 *counts.entry(hash).or_insert(0) += 1;
1131 }
1132 }
1133 counts
1134 .into_iter()
1135 .filter(|(_, c)| *c > 1)
1136 .map(|(h, _)| h)
1137 .collect()
1138}
1139
1140fn build_clip_epochs(list: &DisplayList, bboxes: &[Option<YBBox>]) -> Vec<ClipEpoch> {
1143 let elements = list.elements();
1144 let mut epochs = Vec::new();
1145 let mut epoch_start = 0;
1146 let mut y_min = f64::INFINITY;
1147 let mut y_max = f64::NEG_INFINITY;
1148 let mut has_erase = false;
1149
1150 for (i, element) in elements.iter().enumerate() {
1151 if matches!(element, DisplayElement::InitClip) && i > epoch_start {
1153 epochs.push(ClipEpoch {
1154 start_idx: epoch_start,
1155 end_idx: i,
1156 paint_bbox: if y_min <= y_max {
1157 Some(YBBox { y_min, y_max })
1158 } else {
1159 None
1160 },
1161 has_erase_page: has_erase,
1162 });
1163 epoch_start = i;
1164 y_min = f64::INFINITY;
1165 y_max = f64::NEG_INFINITY;
1166 has_erase = false;
1167 }
1168 if matches!(element, DisplayElement::ErasePage) {
1169 has_erase = true;
1170 }
1171 if let Some(ref bbox) = bboxes[i] {
1172 y_min = y_min.min(bbox.y_min);
1173 y_max = y_max.max(bbox.y_max);
1174 }
1175 }
1176 if epoch_start < elements.len() {
1178 epochs.push(ClipEpoch {
1179 start_idx: epoch_start,
1180 end_idx: elements.len(),
1181 paint_bbox: if y_min <= y_max {
1182 Some(YBBox { y_min, y_max })
1183 } else {
1184 None
1185 },
1186 has_erase_page: has_erase,
1187 });
1188 }
1189 epochs
1190}
1191
1192fn composite_onto_white(data: &mut [u8]) {
1199 for pixel in data.chunks_exact_mut(4) {
1200 let a = pixel[3] as u16;
1201 if a == 255 {
1202 continue; }
1204 let inv_a = 255 - a;
1205 pixel[0] = (pixel[0] as u16 + inv_a).min(255) as u8;
1206 pixel[1] = (pixel[1] as u16 + inv_a).min(255) as u8;
1207 pixel[2] = (pixel[2] as u16 + inv_a).min(255) as u8;
1208 pixel[3] = 255;
1209 }
1210}
1211
1212fn composite_non_isolated_group_cropped(
1221 target: &mut Pixmap,
1222 source: &Pixmap,
1223 backdrop: &[u8],
1224 params: &stet_graphics::display_list::GroupParams,
1225 clip_mask: Option<&stet_tiny_skia::Mask>,
1226 crop_x: i32,
1227 crop_y: i32,
1228) {
1229 let cw = source.width();
1230 let ch = source.height();
1231
1232 let Some(mut contribution) = Pixmap::new(cw, ch) else {
1234 return;
1235 };
1236 let src_data = source.data();
1237 let contrib_data = contribution.data_mut();
1238
1239 for (i, chunk) in contrib_data.chunks_exact_mut(4).enumerate() {
1240 let off = i * 4;
1241 if src_data[off] != backdrop[off]
1242 || src_data[off + 1] != backdrop[off + 1]
1243 || src_data[off + 2] != backdrop[off + 2]
1244 || src_data[off + 3] != backdrop[off + 3]
1245 {
1246 chunk.copy_from_slice(&src_data[off..off + 4]);
1247 }
1248 }
1249
1250 let paint = stet_tiny_skia::PixmapPaint {
1251 opacity: params.alpha as f32,
1252 blend_mode: u8_to_blend_mode(params.blend_mode),
1253 quality: stet_tiny_skia::FilterQuality::Nearest,
1254 };
1255 target.draw_pixmap(
1256 crop_x,
1257 crop_y,
1258 contribution.as_ref(),
1259 &paint,
1260 Transform::identity(),
1261 clip_mask,
1262 );
1263}
1264
1265fn composite_non_isolated_extracted(
1278 target: &mut Pixmap,
1279 source: &Pixmap,
1280 isolated: &Pixmap,
1281 backdrop: &[u8],
1282 params: &stet_graphics::display_list::GroupParams,
1283 clip_mask: Option<&stet_tiny_skia::Mask>,
1284 crop_x: i32,
1285 crop_y: i32,
1286) {
1287 let cw = source.width();
1288 let ch = source.height();
1289
1290 let Some(mut contribution) = Pixmap::new(cw, ch) else {
1291 return;
1292 };
1293 let src_data = source.data();
1294 let iso_data = isolated.data();
1295 let contrib_data = contribution.data_mut();
1296
1297 for i in 0..(cw as usize * ch as usize) {
1298 let off = i * 4;
1299 let alpha_g = iso_data[off + 3];
1300 if alpha_g == 0 {
1301 continue; }
1303
1304 let inv_alpha = 255 - alpha_g as i32;
1306 for c in 0..3 {
1307 let r = src_data[off + c] as i32;
1308 let b = backdrop[off + c] as i32;
1309 let raw = r - (b * inv_alpha + 127) / 255;
1310 contrib_data[off + c] = raw.clamp(0, 255) as u8;
1311 }
1312 contrib_data[off + 3] = alpha_g;
1313 }
1314
1315 let paint = stet_tiny_skia::PixmapPaint {
1316 opacity: params.alpha as f32,
1317 blend_mode: u8_to_blend_mode(params.blend_mode),
1318 quality: stet_tiny_skia::FilterQuality::Nearest,
1319 };
1320 target.draw_pixmap(
1321 crop_x,
1322 crop_y,
1323 contribution.as_ref(),
1324 &paint,
1325 Transform::identity(),
1326 clip_mask,
1327 );
1328}
1329
1330fn viewport_transform(t: Transform, vp_x: f32, vp_y: f32, scale_x: f32, scale_y: f32) -> Transform {
1335 Transform::from_row(
1337 t.sx * scale_x,
1338 t.ky * scale_y,
1339 t.kx * scale_x,
1340 t.sy * scale_y,
1341 (t.tx - vp_x) * scale_x,
1342 (t.ty - vp_y) * scale_y,
1343 )
1344}
1345
1346fn box_resample(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
1353 if dw == 0 || dh == 0 {
1354 return Vec::new();
1355 }
1356 let (sw, sh, dw, dh) = (sw as usize, sh as usize, dw as usize, dh as usize);
1357
1358 let ratio_x = sw as f32 / dw as f32;
1362 let mut tmp = vec![0.0f32; dw * sh * 4];
1363 let tmp_stride = dw * 4;
1364
1365 for y in 0..sh {
1366 let row_off = y * sw * 4;
1367 let dst_row = y * tmp_stride;
1368 for dx in 0..dw {
1369 let left_f = dx as f32 * ratio_x;
1370 let right_f = (dx + 1) as f32 * ratio_x;
1371 let left = (left_f as usize).min(sw - 1);
1372 let right = (right_f.ceil() as usize).min(sw);
1373 let inv_area = 1.0 / (right_f - left_f);
1374 let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0, 0.0, 0.0);
1375 for sx in left..right {
1376 let pixel_left = sx as f32;
1378 let pixel_right = (sx + 1) as f32;
1379 let w = pixel_right.min(right_f) - pixel_left.max(left_f);
1380 let i = row_off + sx * 4;
1381 r += src[i] as f32 * w;
1382 g += src[i + 1] as f32 * w;
1383 b += src[i + 2] as f32 * w;
1384 a += src[i + 3] as f32 * w;
1385 }
1386 let di = dst_row + dx * 4;
1387 tmp[di] = r * inv_area;
1388 tmp[di + 1] = g * inv_area;
1389 tmp[di + 2] = b * inv_area;
1390 tmp[di + 3] = a * inv_area;
1391 }
1392 }
1393
1394 let ratio_y = sh as f32 / dh as f32;
1396 let mut out = vec![0u8; dw * dh * 4];
1397 let out_stride = dw * 4;
1398
1399 for dy in 0..dh {
1400 let top_f = dy as f32 * ratio_y;
1401 let bottom_f = (dy + 1) as f32 * ratio_y;
1402 let top = (top_f as usize).min(sh - 1);
1403 let bottom = (bottom_f.ceil() as usize).min(sh);
1404 let inv_area = 1.0 / (bottom_f - top_f);
1405
1406 let n_rows = bottom - top;
1408 let mut row_weights_buf: [(usize, f32); 8] = [(0, 0.0); 8];
1409 let row_weights_vec: Vec<(usize, f32)>;
1410 let row_weights: &[(usize, f32)] = if n_rows <= 8 {
1411 for (i, sy) in (top..bottom).enumerate() {
1412 let pixel_top = sy as f32;
1413 let pixel_bottom = (sy + 1) as f32;
1414 let w = pixel_bottom.min(bottom_f) - pixel_top.max(top_f);
1415 row_weights_buf[i] = (sy, w);
1416 }
1417 &row_weights_buf[..n_rows]
1418 } else {
1419 row_weights_vec = (top..bottom)
1420 .map(|sy| {
1421 let pixel_top = sy as f32;
1422 let pixel_bottom = (sy + 1) as f32;
1423 let w = pixel_bottom.min(bottom_f) - pixel_top.max(top_f);
1424 (sy, w)
1425 })
1426 .collect();
1427 &row_weights_vec
1428 };
1429
1430 let dst_row = dy * out_stride;
1431 for dx in 0..dw {
1432 let col = dx * 4;
1433 let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0, 0.0, 0.0);
1434 for &(sy, w) in row_weights {
1435 let i = sy * tmp_stride + col;
1436 r += tmp[i] * w;
1437 g += tmp[i + 1] * w;
1438 b += tmp[i + 2] * w;
1439 a += tmp[i + 3] * w;
1440 }
1441 let di = dst_row + col;
1442 out[di] = (r * inv_area + 0.5).clamp(0.0, 255.0) as u8;
1443 out[di + 1] = (g * inv_area + 0.5).clamp(0.0, 255.0) as u8;
1444 out[di + 2] = (b * inv_area + 0.5).clamp(0.0, 255.0) as u8;
1445 out[di + 3] = (a * inv_area + 0.5).clamp(0.0, 255.0) as u8;
1446 }
1447 }
1448
1449 out
1450}
1451
1452fn bicubic_resample(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
1459 if dw == 0 || dh == 0 {
1460 return Vec::new();
1461 }
1462
1463 let (sw, sh, dw, dh) = (sw as usize, sh as usize, dw as usize, dh as usize);
1464 let ratio_x = sw as f32 / dw as f32;
1465 let ratio_y = sh as f32 / dh as f32;
1466
1467 let mut tmp = vec![0.0f32; dw * sh * 4];
1469 for y in 0..sh {
1470 let src_row = y * sw * 4;
1471 let dst_row = y * dw * 4;
1472 for dx in 0..dw {
1473 let sx = (dx as f32 + 0.5) * ratio_x - 0.5;
1474 let sx_floor = sx.floor() as i32;
1475 let fx = sx - sx_floor as f32;
1476 let w0 = catmull_rom(fx + 1.0);
1477 let w1 = catmull_rom(fx);
1478 let w2 = catmull_rom(1.0 - fx);
1479 let w3 = catmull_rom(2.0 - fx);
1480 let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0, 0.0, 0.0);
1481 for (k, w) in [
1482 (sx_floor - 1, w0),
1483 (sx_floor, w1),
1484 (sx_floor + 1, w2),
1485 (sx_floor + 2, w3),
1486 ] {
1487 let px = k.clamp(0, sw as i32 - 1) as usize;
1488 let i = src_row + px * 4;
1489 r += src[i] as f32 * w;
1490 g += src[i + 1] as f32 * w;
1491 b += src[i + 2] as f32 * w;
1492 a += src[i + 3] as f32 * w;
1493 }
1494 let di = dst_row + dx * 4;
1495 tmp[di] = r;
1496 tmp[di + 1] = g;
1497 tmp[di + 2] = b;
1498 tmp[di + 3] = a;
1499 }
1500 }
1501
1502 let mut out = vec![0u8; dw * dh * 4];
1505 let tmp_stride = dw * 4;
1506 let out_stride = dw * 4;
1507 for dy in 0..dh {
1508 let sy = (dy as f32 + 0.5) * ratio_y - 0.5;
1509 let sy_floor = sy.floor() as i32;
1510 let fy = sy - sy_floor as f32;
1511 let w0 = catmull_rom(fy + 1.0);
1512 let w1 = catmull_rom(fy);
1513 let w2 = catmull_rom(1.0 - fy);
1514 let w3 = catmull_rom(2.0 - fy);
1515 let py0 = (sy_floor - 1).clamp(0, sh as i32 - 1) as usize * tmp_stride;
1516 let py1 = sy_floor.clamp(0, sh as i32 - 1) as usize * tmp_stride;
1517 let py2 = (sy_floor + 1).clamp(0, sh as i32 - 1) as usize * tmp_stride;
1518 let py3 = (sy_floor + 2).clamp(0, sh as i32 - 1) as usize * tmp_stride;
1519 let dst_row = dy * out_stride;
1520 for dx in 0..dw {
1521 let col = dx * 4;
1522 let r = tmp[py0 + col] * w0
1523 + tmp[py1 + col] * w1
1524 + tmp[py2 + col] * w2
1525 + tmp[py3 + col] * w3;
1526 let g = tmp[py0 + col + 1] * w0
1527 + tmp[py1 + col + 1] * w1
1528 + tmp[py2 + col + 1] * w2
1529 + tmp[py3 + col + 1] * w3;
1530 let b = tmp[py0 + col + 2] * w0
1531 + tmp[py1 + col + 2] * w1
1532 + tmp[py2 + col + 2] * w2
1533 + tmp[py3 + col + 2] * w3;
1534 let a = tmp[py0 + col + 3] * w0
1535 + tmp[py1 + col + 3] * w1
1536 + tmp[py2 + col + 3] * w2
1537 + tmp[py3 + col + 3] * w3;
1538 let di = dst_row + col;
1539 out[di] = r.round().clamp(0.0, 255.0) as u8;
1540 out[di + 1] = g.round().clamp(0.0, 255.0) as u8;
1541 out[di + 2] = b.round().clamp(0.0, 255.0) as u8;
1542 out[di + 3] = a.round().clamp(0.0, 255.0) as u8;
1543 }
1544 }
1545
1546 out
1547}
1548
1549#[inline]
1551fn catmull_rom(t: f32) -> f32 {
1552 let t = t.abs();
1553 if t < 1.0 {
1554 (1.5 * t - 2.5) * t * t + 1.0
1555 } else if t < 2.0 {
1556 ((-0.5 * t + 2.5) * t - 4.0) * t + 2.0
1557 } else {
1558 0.0
1559 }
1560}
1561
1562pub fn build_icc_cache_for_list(
1580 list: &DisplayList,
1581 system_cmyk_bytes: Option<&std::sync::Arc<Vec<u8>>>,
1582 proofing_enabled: bool,
1583) -> IccCache {
1584 let mut cache = IccCache::new();
1585 let mut seen = HashSet::new();
1586
1587 if let Some(cmyk_bytes) = system_cmyk_bytes
1590 && let Some(hash) = cache.register_profile(cmyk_bytes)
1591 {
1592 seen.insert(hash);
1593 cache.set_default_cmyk_hash(hash);
1595 cache.prepare_reverse_cmyk();
1599 cache.prepare_lab_to_oi_cmyk();
1603 }
1604
1605 cache.set_proofing_enabled(proofing_enabled);
1610
1611 fn scan_elements(
1613 elements: &[DisplayElement],
1614 seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
1615 cache: &mut IccCache,
1616 ) {
1617 for element in elements {
1618 if let DisplayElement::Group { elements: sub, .. } = element {
1620 scan_elements(sub.elements(), seen, cache);
1621 }
1622 if let DisplayElement::SoftMasked { content, mask, .. } = element {
1623 scan_elements(content.elements(), seen, cache);
1624 scan_elements(mask.elements(), seen, cache);
1625 }
1626 if let DisplayElement::OcgGroup { elements: sub, .. } = element {
1627 scan_elements(sub.elements(), seen, cache);
1628 }
1629 let shading_cs = match element {
1631 DisplayElement::AxialShading { params } => Some(¶ms.color_space),
1632 DisplayElement::RadialShading { params } => Some(¶ms.color_space),
1633 DisplayElement::MeshShading { params } => Some(¶ms.color_space),
1634 DisplayElement::PatchShading { params } => Some(¶ms.color_space),
1635 _ => None,
1636 };
1637 if let Some(stet_graphics::device::ShadingColorSpace::ICCBased {
1638 n,
1639 profile_hash,
1640 profile_data,
1641 }) = shading_cs
1642 {
1643 if seen.insert(*profile_hash) {
1644 cache.register_profile_with_n(profile_data, Some(*n));
1645 }
1646 }
1647 if let DisplayElement::Image { params, .. } = element {
1649 match ¶ms.color_space {
1650 ImageColorSpace::ICCBased {
1651 n,
1652 profile_hash,
1653 profile_data,
1654 } if seen.insert(*profile_hash) => {
1655 cache.register_profile_with_n(profile_data, Some(*n));
1656 }
1657 ImageColorSpace::Indexed { base, .. }
1658 if matches!(base.as_ref(), ImageColorSpace::ICCBased { .. }) =>
1659 {
1660 if let ImageColorSpace::ICCBased {
1661 n,
1662 profile_hash,
1663 profile_data,
1664 } = base.as_ref()
1665 {
1666 if seen.insert(*profile_hash) {
1667 cache.register_profile_with_n(profile_data, Some(*n));
1668 }
1669 }
1670 }
1671 _ => {}
1672 }
1673 }
1674 }
1675 }
1676 scan_elements(list.elements(), &mut seen, &mut cache);
1677
1678 cache
1679}
1680
1681fn register_shading_icc_profiles(list: &DisplayList, cache: &mut IccCache) {
1686 fn register_image_iccs(
1687 cs: &ImageColorSpace,
1688 seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
1689 cache: &mut IccCache,
1690 ) {
1691 match cs {
1692 ImageColorSpace::ICCBased {
1693 n,
1694 profile_hash,
1695 profile_data,
1696 } => {
1697 if seen.insert(*profile_hash) {
1698 cache.register_profile_with_n(profile_data, Some(*n));
1699 }
1700 }
1701 ImageColorSpace::Indexed { base, .. } => register_image_iccs(base, seen, cache),
1702 ImageColorSpace::Separation { alt_space, .. }
1703 | ImageColorSpace::DeviceN { alt_space, .. } => {
1704 register_image_iccs(alt_space, seen, cache)
1705 }
1706 _ => {}
1707 }
1708 }
1709 fn scan(
1710 elements: &[DisplayElement],
1711 seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
1712 cache: &mut IccCache,
1713 ) {
1714 for element in elements {
1715 if let DisplayElement::Group { elements: sub, .. } = element {
1716 scan(sub.elements(), seen, cache);
1717 }
1718 if let DisplayElement::SoftMasked { content, mask, .. } = element {
1719 scan(content.elements(), seen, cache);
1720 scan(mask.elements(), seen, cache);
1721 }
1722 if let DisplayElement::OcgGroup { elements: sub, .. } = element {
1723 scan(sub.elements(), seen, cache);
1724 }
1725 let shading_cs = match element {
1726 DisplayElement::AxialShading { params } => Some(¶ms.color_space),
1727 DisplayElement::RadialShading { params } => Some(¶ms.color_space),
1728 DisplayElement::MeshShading { params } => Some(¶ms.color_space),
1729 DisplayElement::PatchShading { params } => Some(¶ms.color_space),
1730 _ => None,
1731 };
1732 if let Some(stet_graphics::device::ShadingColorSpace::ICCBased {
1733 n,
1734 profile_hash,
1735 profile_data,
1736 }) = shading_cs
1737 && seen.insert(*profile_hash)
1738 {
1739 cache.register_profile_with_n(profile_data, Some(*n));
1740 }
1741 if let DisplayElement::Image { params, .. } = element {
1742 register_image_iccs(¶ms.color_space, seen, cache);
1743 }
1744 }
1745 }
1746 let mut seen = HashSet::new();
1747 scan(list.elements(), &mut seen, cache);
1748}
1749
1750fn samples_to_rgba(
1754 data: &[u8],
1755 params: &ImageParams,
1756 icc: Option<&IccCache>,
1757 opm_zero_transparent: bool,
1758) -> Vec<u8> {
1759 let w = params.width as usize;
1760 let h = params.height as usize;
1761 let npixels = w * h;
1762 let bpc = params.bits_per_component;
1763 match ¶ms.color_space {
1764 ImageColorSpace::PreconvertedRGBA => {
1765 data.to_vec()
1767 }
1768 ImageColorSpace::DeviceGray => {
1769 let mut rgba = vec![255u8; npixels * 4];
1770 if bpc == 16 {
1771 for i in 0..npixels {
1772 let g = data.get(i * 2).copied().unwrap_or(0);
1773 let pi = i * 4;
1774 rgba[pi] = g;
1775 rgba[pi + 1] = g;
1776 rgba[pi + 2] = g;
1777 }
1778 } else {
1779 for i in 0..npixels {
1780 let g = data.get(i).copied().unwrap_or(0);
1781 let pi = i * 4;
1782 rgba[pi] = g;
1783 rgba[pi + 1] = g;
1784 rgba[pi + 2] = g;
1785 }
1786 }
1787 rgba
1788 }
1789 ImageColorSpace::DeviceRGB => {
1790 let mut rgba = vec![255u8; npixels * 4];
1791 if bpc == 16 {
1792 for i in 0..npixels {
1795 let si = i * 6;
1796 let pi = i * 4;
1797 rgba[pi] = data.get(si).copied().unwrap_or(0);
1798 rgba[pi + 1] = data.get(si + 2).copied().unwrap_or(0);
1799 rgba[pi + 2] = data.get(si + 4).copied().unwrap_or(0);
1800 }
1801 } else {
1802 for i in 0..npixels {
1803 let si = i * 3;
1804 let pi = i * 4;
1805 rgba[pi] = data.get(si).copied().unwrap_or(0);
1806 rgba[pi + 1] = data.get(si + 1).copied().unwrap_or(0);
1807 rgba[pi + 2] = data.get(si + 2).copied().unwrap_or(0);
1808 }
1809 }
1810 rgba
1811 }
1812 ImageColorSpace::DeviceCMYK => {
1813 if let Some(cache) = icc
1817 && let Some(cmyk_hash) = cache.default_cmyk_hash()
1818 {
1819 let avail_pixels = data.len() / 4;
1820 let icc_pixels = avail_pixels.min(npixels);
1821 if icc_pixels > 0
1822 && let Some(rgb) = cache.convert_image_8bit(cmyk_hash, data, icc_pixels)
1823 {
1824 let mut rgba = vec![255u8; npixels * 4];
1825 for i in 0..icc_pixels {
1826 rgba[i * 4] = rgb[i * 3];
1827 rgba[i * 4 + 1] = rgb[i * 3 + 1];
1828 rgba[i * 4 + 2] = rgb[i * 3 + 2];
1829 if opm_zero_transparent {
1831 let si = i * 4;
1832 if data[si] == 0
1833 && data[si + 1] == 0
1834 && data[si + 2] == 0
1835 && data[si + 3] == 0
1836 {
1837 rgba[i * 4 + 3] = 0;
1838 }
1839 }
1840 }
1841 return rgba;
1843 }
1844 }
1845 let mut rgba = vec![255u8; npixels * 4];
1847 for i in 0..npixels {
1848 let si = i * 4;
1849 let c = data.get(si).copied().unwrap_or(0) as f64 / 255.0;
1850 let m = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0;
1851 let y = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0;
1852 let k = data.get(si + 3).copied().unwrap_or(0) as f64 / 255.0;
1853 let r = (1.0 - c.min(1.0)) * (1.0 - k.min(1.0));
1854 let g = (1.0 - m.min(1.0)) * (1.0 - k.min(1.0));
1855 let b = (1.0 - y.min(1.0)) * (1.0 - k.min(1.0));
1856 let pi = i * 4;
1857 rgba[pi] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
1858 rgba[pi + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
1859 rgba[pi + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
1860 if opm_zero_transparent
1862 && data.get(si).copied().unwrap_or(0) == 0
1863 && data.get(si + 1).copied().unwrap_or(0) == 0
1864 && data.get(si + 2).copied().unwrap_or(0) == 0
1865 && data.get(si + 3).copied().unwrap_or(0) == 0
1866 {
1867 rgba[pi + 3] = 0;
1868 }
1869 }
1870 rgba
1871 }
1872 ImageColorSpace::ICCBased {
1873 n,
1874 profile_hash,
1875 profile_data,
1876 } => {
1877 let intent = stet_graphics::icc::intent_from_pdf_byte(params.rendering_intent);
1888 if let Some(cache) = icc
1889 && cache.has_profile(profile_hash)
1890 && let Some(rgb) =
1891 cache.convert_image_8bit_with_intent(profile_hash, data, npixels, intent)
1892 {
1893 let mut rgba = vec![255u8; npixels * 4];
1894 for i in 0..npixels {
1895 rgba[i * 4] = rgb[i * 3];
1896 rgba[i * 4 + 1] = rgb[i * 3 + 1];
1897 rgba[i * 4 + 2] = rgb[i * 3 + 2];
1898 if opm_zero_transparent && *n == 4 {
1900 let si = i * *n as usize;
1901 if si + 3 < data.len()
1902 && data[si] == 0
1903 && data[si + 1] == 0
1904 && data[si + 2] == 0
1905 && data[si + 3] == 0
1906 {
1907 rgba[i * 4 + 3] = 0;
1908 }
1909 }
1910 }
1911 return rgba;
1912 }
1913 let _ = (profile_hash, profile_data);
1915 let fallback = match n {
1916 1 => ImageColorSpace::DeviceGray,
1917 4 => ImageColorSpace::DeviceCMYK,
1918 _ => ImageColorSpace::DeviceRGB,
1919 };
1920 let p = ImageParams {
1921 color_space: fallback,
1922 bits_per_component: 8,
1923 ..params.clone()
1924 };
1925 samples_to_rgba(data, &p, icc, opm_zero_transparent)
1926 }
1927 ImageColorSpace::Indexed {
1928 base,
1929 hival,
1930 lookup,
1931 } => {
1932 let base_ncomp = base.num_components() as usize;
1933 let mut expanded = Vec::with_capacity(npixels * base_ncomp);
1935 for i in 0..npixels {
1936 let idx = data.get(i).copied().unwrap_or(0) as usize;
1937 let idx = idx.min(*hival as usize);
1938 let offset = idx * base_ncomp;
1939 for c in 0..base_ncomp {
1940 expanded.push(lookup.get(offset + c).copied().unwrap_or(0));
1941 }
1942 }
1943 let p = ImageParams {
1944 color_space: *base.clone(),
1945 bits_per_component: 8,
1946 ..params.clone()
1947 };
1948 samples_to_rgba(&expanded, &p, icc, opm_zero_transparent)
1949 }
1950 ImageColorSpace::CIEBasedABC { params: cie_params } => {
1951 let mut rgba = vec![255u8; npixels * 4];
1952 for i in 0..npixels {
1953 let si = i * 3;
1954 let a = data.get(si).copied().unwrap_or(0) as f64 / 255.0;
1955 let b = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0;
1956 let c = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0;
1957 let color = DeviceColor::from_cie_abc(a, b, c, cie_params);
1958 let pi = i * 4;
1959 rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
1960 rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
1961 rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
1962 }
1963 rgba
1964 }
1965 ImageColorSpace::CIEBasedA { params: cie_params } => {
1966 let mut rgba = vec![255u8; npixels * 4];
1967 for i in 0..npixels {
1968 let val = data.get(i).copied().unwrap_or(0) as f64 / 255.0;
1969 let color = DeviceColor::from_cie_a(val, cie_params);
1970 let pi = i * 4;
1971 rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
1972 rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
1973 rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
1974 }
1975 rgba
1976 }
1977 ImageColorSpace::Lab { range, .. } => {
1978 let mut rgba = vec![255u8; npixels * 4];
1979 let a_span = range[1] - range[0];
1980 let b_span = range[3] - range[2];
1981 for i in 0..npixels {
1982 let si = i * 3;
1983 let l = data.get(si).copied().unwrap_or(0) as f64 / 255.0 * 100.0;
1984 let a = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0 * a_span + range[0];
1985 let b = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0 * b_span + range[2];
1986 let color = DeviceColor::from_lab(l, a, b, range);
1987 let pi = i * 4;
1988 rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
1989 rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
1990 rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
1991 }
1992 rgba
1993 }
1994 ImageColorSpace::Separation {
1995 alt_space,
1996 tint_table,
1997 ..
1998 } => {
1999 if matches!(alt_space.as_ref(), ImageColorSpace::DeviceCMYK)
2002 && let Some(rgba) = tint_separation_via_icc(data, npixels, tint_table, icc)
2003 {
2004 return rgba;
2005 }
2006 let mut rgba = vec![255u8; npixels * 4];
2007 let no = tint_table.num_outputs as usize;
2008 let mut alt_comps = vec![0.0f32; no];
2009 for i in 0..npixels {
2010 let tint = data.get(i).copied().unwrap_or(0) as f32 / 255.0;
2011 tint_table.lookup_1d(tint, &mut alt_comps);
2012 let (r, g, b) = alt_comps_to_rgb(&alt_comps, alt_space);
2013 let pi = i * 4;
2014 rgba[pi] = r;
2015 rgba[pi + 1] = g;
2016 rgba[pi + 2] = b;
2017 }
2018 rgba
2019 }
2020 ImageColorSpace::DeviceN {
2021 alt_space,
2022 tint_table,
2023 ..
2024 } => {
2025 let ni = tint_table.num_inputs as usize;
2026 let no = tint_table.num_outputs as usize;
2027 if matches!(alt_space.as_ref(), ImageColorSpace::DeviceCMYK)
2029 && let Some(rgba) = tint_devicen_via_icc(data, npixels, ni, tint_table, icc)
2030 {
2031 return rgba;
2032 }
2033 let mut rgba = vec![255u8; npixels * 4];
2034 let mut inputs = vec![0.0f32; ni];
2035 let mut alt_comps = vec![0.0f32; no];
2036 for i in 0..npixels {
2037 let si = i * ni;
2038 for (c, inp) in inputs.iter_mut().enumerate() {
2039 *inp = data.get(si + c).copied().unwrap_or(0) as f32 / 255.0;
2040 }
2041 tint_table.lookup_nd(&inputs, &mut alt_comps);
2042 let (r, g, b) = alt_comps_to_rgb(&alt_comps, alt_space);
2043 let pi = i * 4;
2044 rgba[pi] = r;
2045 rgba[pi + 1] = g;
2046 rgba[pi + 2] = b;
2047 }
2048 rgba
2049 }
2050 ImageColorSpace::Mask {
2051 color, polarity, ..
2052 } => {
2053 let mut rgba = vec![0u8; npixels * 4];
2054 let r = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
2055 let g = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
2056 let b = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
2057 let bytes_per_row = (w).div_ceil(8);
2058 for row in 0..h {
2059 for col in 0..w {
2060 let byte_idx = row * bytes_per_row + col / 8;
2061 let bit_offset = 7 - (col % 8);
2062 let bit = if byte_idx < data.len() {
2063 (data[byte_idx] >> bit_offset) & 1
2064 } else {
2065 0
2066 };
2067 let paint = if *polarity { bit == 1 } else { bit == 0 };
2068 if paint {
2069 let pi = (row * w + col) * 4;
2070 rgba[pi] = r;
2071 rgba[pi + 1] = g;
2072 rgba[pi + 2] = b;
2073 rgba[pi + 3] = 255;
2074 }
2075 }
2076 }
2077 rgba
2078 }
2079 _ => vec![0u8; npixels * 4],
2080 }
2081}
2082
2083fn tint_separation_via_icc(
2086 data: &[u8],
2087 npixels: usize,
2088 tint_table: &TintLookupTable,
2089 icc: Option<&IccCache>,
2090) -> Option<Vec<u8>> {
2091 let cache = icc?;
2092 let cmyk_hash = cache.default_cmyk_hash()?;
2093 let mut cmyk_data = vec![0u8; npixels * 4];
2095 let mut alt_comps = [0.0f32; 4];
2096 for i in 0..npixels {
2097 let tint = data.get(i).copied().unwrap_or(0) as f32 / 255.0;
2098 tint_table.lookup_1d(tint, &mut alt_comps);
2099 let si = i * 4;
2100 cmyk_data[si] = (alt_comps[0].clamp(0.0, 1.0) * 255.0).round() as u8;
2101 cmyk_data[si + 1] = (alt_comps[1].clamp(0.0, 1.0) * 255.0).round() as u8;
2102 cmyk_data[si + 2] = (alt_comps[2].clamp(0.0, 1.0) * 255.0).round() as u8;
2103 cmyk_data[si + 3] = (alt_comps[3].clamp(0.0, 1.0) * 255.0).round() as u8;
2104 }
2105 let rgb = cache.convert_image_8bit(cmyk_hash, &cmyk_data, npixels)?;
2106 let mut rgba = vec![255u8; npixels * 4];
2107 for i in 0..npixels {
2108 rgba[i * 4] = rgb[i * 3];
2109 rgba[i * 4 + 1] = rgb[i * 3 + 1];
2110 rgba[i * 4 + 2] = rgb[i * 3 + 2];
2111 }
2112 Some(rgba)
2113}
2114
2115fn tint_devicen_via_icc(
2117 data: &[u8],
2118 npixels: usize,
2119 ni: usize,
2120 tint_table: &TintLookupTable,
2121 icc: Option<&IccCache>,
2122) -> Option<Vec<u8>> {
2123 let cache = icc?;
2124 let cmyk_hash = cache.default_cmyk_hash()?;
2125 let mut cmyk_data = vec![0u8; npixels * 4];
2126 let mut inputs = vec![0.0f32; ni];
2127 let mut alt_comps = [0.0f32; 4];
2128 for i in 0..npixels {
2129 let si = i * ni;
2130 for (c, inp) in inputs.iter_mut().enumerate() {
2131 *inp = data.get(si + c).copied().unwrap_or(0) as f32 / 255.0;
2132 }
2133 tint_table.lookup_nd(&inputs, &mut alt_comps);
2134 let di = i * 4;
2135 cmyk_data[di] = (alt_comps[0].clamp(0.0, 1.0) * 255.0).round() as u8;
2136 cmyk_data[di + 1] = (alt_comps[1].clamp(0.0, 1.0) * 255.0).round() as u8;
2137 cmyk_data[di + 2] = (alt_comps[2].clamp(0.0, 1.0) * 255.0).round() as u8;
2138 cmyk_data[di + 3] = (alt_comps[3].clamp(0.0, 1.0) * 255.0).round() as u8;
2139 }
2140 let rgb = cache.convert_image_8bit(cmyk_hash, &cmyk_data, npixels)?;
2141 let mut rgba = vec![255u8; npixels * 4];
2142 for i in 0..npixels {
2143 rgba[i * 4] = rgb[i * 3];
2144 rgba[i * 4 + 1] = rgb[i * 3 + 1];
2145 rgba[i * 4 + 2] = rgb[i * 3 + 2];
2146 }
2147 Some(rgba)
2148}
2149
2150fn alt_comps_to_rgb(comps: &[f32], alt_space: &ImageColorSpace) -> (u8, u8, u8) {
2152 match alt_space {
2153 ImageColorSpace::DeviceGray => {
2154 let g = (comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2155 (g, g, g)
2156 }
2157 ImageColorSpace::DeviceRGB => {
2158 let r = (comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2159 let g = (comps.get(1).copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2160 let b = (comps.get(2).copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2161 (r, g, b)
2162 }
2163 ImageColorSpace::DeviceCMYK => {
2164 let c = comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0);
2165 let m = comps.get(1).copied().unwrap_or(0.0).clamp(0.0, 1.0);
2166 let y = comps.get(2).copied().unwrap_or(0.0).clamp(0.0, 1.0);
2167 let k = comps.get(3).copied().unwrap_or(0.0).clamp(0.0, 1.0);
2168 let r = ((1.0 - (c + k).min(1.0)) * 255.0).round() as u8;
2169 let g = ((1.0 - (m + k).min(1.0)) * 255.0).round() as u8;
2170 let b = ((1.0 - (y + k).min(1.0)) * 255.0).round() as u8;
2171 (r, g, b)
2172 }
2173 _ => (0, 0, 0),
2174 }
2175}
2176
2177fn apply_mask_color_rgba(rgba: &mut [u8], sample_data: &[u8], params: &ImageParams) {
2179 let mask_color = match ¶ms.mask_color {
2180 Some(mc) => mc,
2181 None => return,
2182 };
2183 let ncomp = params.color_space.num_components() as usize;
2184 let npixels = params.width as usize * params.height as usize;
2185 let is_range = mask_color.len() == 2 * ncomp;
2186
2187 for i in 0..npixels {
2188 let si = i * ncomp;
2189 let matched = if is_range {
2190 (0..ncomp).all(|c| {
2191 let sample = sample_data.get(si + c).copied().unwrap_or(0);
2192 let min_val = mask_color.get(c * 2).copied().unwrap_or(0);
2193 let max_val = mask_color.get(c * 2 + 1).copied().unwrap_or(0);
2194 sample >= min_val && sample <= max_val
2195 })
2196 } else {
2197 (0..ncomp).all(|c| {
2198 let sample = sample_data.get(si + c).copied().unwrap_or(0);
2199 let target = mask_color.get(c).copied().unwrap_or(0);
2200 sample == target
2201 })
2202 };
2203 if matched {
2204 let pi = i * 4;
2205 if pi + 3 < rgba.len() {
2206 rgba[pi] = 0;
2207 rgba[pi + 1] = 0;
2208 rgba[pi + 2] = 0;
2209 rgba[pi + 3] = 0;
2210 }
2211 }
2212 }
2213}
2214
2215fn image_filter_quality(transform: Transform, interpolate: bool) -> stet_tiny_skia::FilterQuality {
2221 let eff_sx = (transform.sx * transform.sx + transform.ky * transform.ky).sqrt();
2222 let eff_sy = (transform.kx * transform.kx + transform.sy * transform.sy).sqrt();
2223 let min_scale = eff_sx.min(eff_sy);
2224 if (eff_sx - 1.0).abs() < 0.01 && (eff_sy - 1.0).abs() < 0.01 {
2226 stet_tiny_skia::FilterQuality::Nearest
2227 } else if !interpolate && min_scale >= 0.95 {
2228 stet_tiny_skia::FilterQuality::Nearest
2230 } else {
2231 stet_tiny_skia::FilterQuality::Bilinear
2232 }
2233}
2234
2235fn prescale_image(
2240 rgba_data: &[u8],
2241 w: u32,
2242 h: u32,
2243 transform: Transform,
2244 interpolate: bool,
2245) -> Option<(Vec<u8>, u32, u32, Transform)> {
2246 let scale_x = (transform.sx * transform.sx + transform.ky * transform.ky).sqrt();
2248 let scale_y = (transform.kx * transform.kx + transform.sy * transform.sy).sqrt();
2249 let min_scale = scale_x.min(scale_y);
2250
2251 if min_scale > 1.05 {
2255 if interpolate {
2256 let is_axis_aligned = transform.kx.abs() < 1e-4 && transform.ky.abs() < 1e-4;
2257 if is_axis_aligned && w >= 2 && h >= 2 {
2258 let dw = (w as f32 * transform.sx.abs()).round().max(1.0) as u32;
2259 let dh = (h as f32 * transform.sy.abs()).round().max(1.0) as u32;
2260 if dw > w || dh > h {
2261 let resampled = bicubic_resample(rgba_data, w, h, dw, dh);
2262 let new_sx = transform.sx * w as f32 / dw as f32;
2263 let new_sy = transform.sy * h as f32 / dh as f32;
2264 let adjusted = Transform::from_row(
2265 new_sx,
2266 transform.ky,
2267 transform.kx,
2268 new_sy,
2269 transform.tx,
2270 transform.ty,
2271 );
2272 return Some((resampled, dw, dh, adjusted));
2273 }
2274 }
2275 }
2276 return None;
2277 }
2278
2279 if min_scale >= 0.95 {
2281 return None;
2282 }
2283
2284 let is_axis_aligned = transform.kx.abs() < 1e-4 && transform.ky.abs() < 1e-4;
2287 if is_axis_aligned && w >= 2 && h >= 2 {
2288 let dw = (w as f32 * transform.sx.abs()).ceil().max(1.0) as u32;
2289 let dh = (h as f32 * transform.sy.abs()).ceil().max(1.0) as u32;
2290 if dw < w || dh < h {
2291 let resampled = box_resample(rgba_data, w, h, dw, dh);
2292 let new_sx = transform.sx * w as f32 / dw as f32;
2294 let new_sy = transform.sy * h as f32 / dh as f32;
2295 let adjusted = Transform::from_row(
2296 new_sx,
2297 transform.ky,
2298 transform.kx,
2299 new_sy,
2300 transform.tx,
2301 transform.ty,
2302 );
2303 return Some((resampled, dw, dh, adjusted));
2304 }
2305 }
2306
2307 let factor = (1.0 / min_scale) as u32;
2309 if factor < 2 || w < factor || h < factor {
2310 return None;
2311 }
2312 let nw = w / factor;
2313 let nh = h / factor;
2314 if nw == 0 || nh == 0 {
2315 return None;
2316 }
2317 let area = factor * factor;
2318 let half = area / 2;
2319 let stride = w as usize * 4;
2320 let mut out = vec![0u8; (nw * nh * 4) as usize];
2321 for dy in 0..nh {
2322 for dx in 0..nw {
2323 let (mut r, mut g, mut b, mut a) = (0u32, 0u32, 0u32, 0u32);
2324 let sy0 = (dy * factor) as usize;
2325 let sx0 = (dx * factor) as usize;
2326 for iy in 0..factor as usize {
2327 let row = (sy0 + iy) * stride + sx0 * 4;
2328 for ix in 0..factor as usize {
2329 let i = row + ix * 4;
2330 r += rgba_data[i] as u32;
2331 g += rgba_data[i + 1] as u32;
2332 b += rgba_data[i + 2] as u32;
2333 a += rgba_data[i + 3] as u32;
2334 }
2335 }
2336 let di = (dy * nw + dx) as usize * 4;
2337 out[di] = ((r + half) / area) as u8;
2338 out[di + 1] = ((g + half) / area) as u8;
2339 out[di + 2] = ((b + half) / area) as u8;
2340 out[di + 3] = ((a + half) / area) as u8;
2341 }
2342 }
2343 let f = factor as f32;
2344 let adjusted = Transform::from_row(
2345 transform.sx * f,
2346 transform.ky * f,
2347 transform.kx * f,
2348 transform.sy * f,
2349 transform.tx,
2350 transform.ty,
2351 );
2352 Some((out, nw, nh, adjusted))
2353}
2354
2355fn translate_clip_rect(rect: &ClipRect, y_start: u32, band_h: u32) -> ClipRect {
2357 ClipRect {
2358 x0: rect.x0,
2359 y0: rect.y0.saturating_sub(y_start).min(band_h),
2360 x1: rect.x1,
2361 y1: rect.y1.saturating_sub(y_start).min(band_h),
2362 }
2363}
2364
2365fn enforce_min_image_size(transform: Transform, img_w: u32, img_h: u32) -> Transform {
2372 let eff_w =
2374 ((transform.sx * img_w as f32).powi(2) + (transform.ky * img_w as f32).powi(2)).sqrt();
2375 let eff_h =
2376 ((transform.kx * img_h as f32).powi(2) + (transform.sy * img_h as f32).powi(2)).sqrt();
2377
2378 if eff_w >= 1.0 && eff_h >= 1.0 {
2379 return transform;
2380 }
2381
2382 let ratio = eff_w.max(eff_h) / eff_w.min(eff_h).max(0.001);
2386 if ratio < 3.0 {
2387 return transform;
2388 }
2389
2390 let mut t = transform;
2391 if eff_w < 1.0 && eff_w > 0.001 {
2392 let boost = 1.0 / eff_w;
2393 t.sx *= boost;
2394 t.ky *= boost;
2395 }
2396 if eff_h < 1.0 && eff_h > 0.001 {
2397 let boost = 1.0 / eff_h;
2398 t.kx *= boost;
2399 t.sy *= boost;
2400 }
2401 t
2402}
2403
2404fn hairline_min_width(ctm: &Matrix, dpi: f64) -> f64 {
2408 let (a, b, c, d) = (ctm.a, ctm.b, ctm.c, ctm.d);
2409 let sum_sq = a * a + b * b + c * c + d * d;
2410 let diff = ((a * a + b * b - c * c - d * d).powi(2) + 4.0 * (a * c + b * d).powi(2)).sqrt();
2411 let s_max = (0.5 * (sum_sq + diff)).max(0.0).sqrt();
2412 let min_px = if dpi <= 150.0 { 0.5 } else { 1.0 };
2413 if s_max > 1e-10 {
2414 min_px / s_max
2415 } else {
2416 min_px
2417 }
2418}
2419
2420fn is_k_only_src(color: &DeviceColor) -> bool {
2426 if let Some((c, m, y, _k)) = color.native_cmyk {
2427 c == 0.0 && m == 0.0 && y == 0.0
2428 } else {
2429 false
2430 }
2431}
2432
2433fn needs_gray_promotion(
2442 overprint: bool,
2443 painted_channels: u8,
2444 is_device_cmyk: bool,
2445 color: &DeviceColor,
2446) -> Option<f64> {
2447 if !overprint
2448 || painted_channels != 0
2449 || is_device_cmyk
2450 || color.native_cmyk.is_some()
2451 || color.process_cmyk.is_some()
2452 {
2453 return None;
2454 }
2455 let r = color.r;
2456 if (r - color.g).abs() > f64::EPSILON || (r - color.b).abs() > f64::EPSILON {
2457 return None;
2458 }
2459 Some(r.clamp(0.0, 1.0))
2460}
2461
2462fn maybe_promote_gray_fill<'a>(
2465 params: &'a FillParams,
2466 buf: &'a mut Option<FillParams>,
2467) -> &'a FillParams {
2468 if let Some(gray) = needs_gray_promotion(
2469 params.overprint,
2470 params.painted_channels,
2471 params.is_device_cmyk,
2472 ¶ms.color,
2473 ) {
2474 let mut promoted = params.clone();
2475 promoted.is_device_cmyk = true;
2476 promoted.painted_channels = stet_graphics::device::CMYK_K;
2477 promoted.color.native_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2478 promoted.color.process_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2479 *buf = Some(promoted);
2480 return buf.as_ref().unwrap();
2481 }
2482 params
2483}
2484
2485fn maybe_promote_gray_stroke<'a>(
2487 params: &'a StrokeParams,
2488 buf: &'a mut Option<StrokeParams>,
2489) -> &'a StrokeParams {
2490 if let Some(gray) = needs_gray_promotion(
2491 params.overprint,
2492 params.painted_channels,
2493 params.is_device_cmyk,
2494 ¶ms.color,
2495 ) {
2496 let mut promoted = params.clone();
2497 promoted.is_device_cmyk = true;
2498 promoted.painted_channels = stet_graphics::device::CMYK_K;
2499 promoted.color.native_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2500 promoted.color.process_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2501 *buf = Some(promoted);
2502 return buf.as_ref().unwrap();
2503 }
2504 params
2505}
2506
2507fn build_stroke(params: &StrokeParams, dpi: f64) -> Stroke {
2511 let min_lw = hairline_min_width(¶ms.ctm, dpi);
2512 let mut stroke = Stroke {
2513 width: (params.line_width as f32).max(min_lw as f32),
2514 line_cap: to_line_cap(params.line_cap),
2515 line_join: to_line_join(params.line_join),
2516 miter_limit: params.miter_limit as f32,
2517 ..Stroke::default()
2518 };
2519 if !params.dash_pattern.array.is_empty() {
2520 let mut dash_array: Vec<f32> = params
2521 .dash_pattern
2522 .array
2523 .iter()
2524 .map(|&v| v as f32)
2525 .collect();
2526 if dash_array.len() % 2 == 1 {
2529 let clone = dash_array.clone();
2530 dash_array.extend_from_slice(&clone);
2531 }
2532 if let Some(dash) = StrokeDash::new(dash_array, params.dash_pattern.offset as f32) {
2533 stroke.dash = Some(dash);
2534 }
2535 }
2536 stroke
2537}
2538
2539fn ctm_is_device_space(ctm: &Matrix) -> bool {
2556 (ctm.a.abs() - 1.0).abs() < 0.01
2557 && ctm.b.abs() < 0.01
2558 && ctm.c.abs() < 0.01
2559 && (ctm.d.abs() - 1.0).abs() < 0.01
2560}
2561
2562fn stroke_adjust_path_viewport(
2568 path: &PsPath,
2569 device_width: f64,
2570 scale_x: f64,
2571 scale_y: f64,
2572 vp_x: f64,
2573 vp_y: f64,
2574) -> PsPath {
2575 let use_half_pixel = device_width < 1.5 || (device_width.round() as i32) % 2 == 1;
2576
2577 let snap_x = |v: f64| -> f64 {
2579 let out = (v - vp_x) * scale_x;
2580 let snapped = if use_half_pixel {
2581 out.floor() + 0.5
2582 } else {
2583 out.round()
2584 };
2585 snapped / scale_x + vp_x
2586 };
2587 let snap_y = |v: f64| -> f64 {
2588 let out = (v - vp_y) * scale_y;
2589 let snapped = if use_half_pixel {
2590 out.floor() + 0.5
2591 } else {
2592 out.round()
2593 };
2594 snapped / scale_y + vp_y
2595 };
2596
2597 let mut result = PsPath::new();
2598 let mut prev_x = 0.0_f64;
2599 let mut prev_y = 0.0_f64;
2600
2601 for seg in &path.segments {
2602 match *seg {
2603 PathSegment::MoveTo(x, y) => {
2604 prev_x = x;
2605 prev_y = y;
2606 result.segments.push(PathSegment::MoveTo(x, y));
2607 }
2608 PathSegment::LineTo(x, y) => {
2609 let is_horizontal = (y - prev_y).abs() < 1e-6;
2610 let is_vertical = (x - prev_x).abs() < 1e-6;
2611
2612 if is_horizontal {
2613 let snapped_y = snap_y(y);
2614 if let Some(PathSegment::MoveTo(_, ly) | PathSegment::LineTo(_, ly)) =
2615 result.segments.last_mut()
2616 {
2617 *ly = snapped_y;
2618 }
2619 result.segments.push(PathSegment::LineTo(x, snapped_y));
2620 prev_x = x;
2621 prev_y = snapped_y;
2622 } else if is_vertical {
2623 let snapped_x = snap_x(x);
2624 if let Some(PathSegment::MoveTo(lx, _) | PathSegment::LineTo(lx, _)) =
2625 result.segments.last_mut()
2626 {
2627 *lx = snapped_x;
2628 }
2629 result.segments.push(PathSegment::LineTo(snapped_x, y));
2630 prev_x = snapped_x;
2631 prev_y = y;
2632 } else {
2633 result.segments.push(PathSegment::LineTo(x, y));
2634 prev_x = x;
2635 prev_y = y;
2636 }
2637 }
2638 PathSegment::CurveTo {
2639 x1,
2640 y1,
2641 x2,
2642 y2,
2643 x3,
2644 y3,
2645 } => {
2646 result.segments.push(PathSegment::CurveTo {
2647 x1,
2648 y1,
2649 x2,
2650 y2,
2651 x3,
2652 y3,
2653 });
2654 prev_x = x3;
2655 prev_y = y3;
2656 }
2657 PathSegment::ClosePath => {
2658 result.segments.push(PathSegment::ClosePath);
2659 }
2660 }
2661 }
2662 result
2663}
2664
2665fn render_element(
2671 pixmap: &mut Pixmap,
2672 band_state: &mut BandState,
2673 element: &DisplayElement,
2674 ctx: &RenderContext<'_>,
2675) {
2676 match element {
2677 DisplayElement::Fill { path, params } => {
2678 let mut promoted_fill: Option<FillParams> = None;
2684 let params = maybe_promote_gray_fill(params, &mut promoted_fill);
2685 let painted = params.painted_channels;
2706 let subset_channels = painted != 0 && painted != stet_graphics::device::CMYK_ALL;
2707 let opm1_cmyk = params.is_device_cmyk && params.overprint_mode == 1;
2708 let custom_spot = painted == 0
2717 && !params.is_device_cmyk
2718 && params.color.native_cmyk.is_some()
2719 && params.color.process_cmyk.is_some();
2720 let is_k_only_cmyk =
2726 params.is_device_cmyk && params.overprint_mode == 0 && is_k_only_src(¶ms.color);
2727 let needs_overprint = params.overprint
2728 && band_state.cmyk_buffer.is_some()
2729 && params.blend_mode == 0
2730 && (subset_channels || opm1_cmyk || custom_spot || is_k_only_cmyk);
2731
2732 if needs_overprint {
2733 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2734 let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
2735 let spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2736 render_overprint_fill(
2737 pixmap,
2738 &mut cmyk_buf,
2739 &mut op_bg,
2740 &mut op_touched,
2741 &spot_mask,
2742 band_state,
2743 path,
2744 params,
2745 ctx.vp_x,
2746 ctx.vp_y,
2747 ctx.scale_x,
2748 ctx.scale_y,
2749 ctx.out_w,
2750 ctx.out_h,
2751 ctx.icc,
2752 ctx.no_aa,
2753 );
2754 band_state.cmyk_buffer = Some(cmyk_buf);
2755 band_state.restore_op_buffers(op_bg, op_touched);
2756 band_state.restore_spot_mask(spot_mask);
2757 } else {
2758 let Some(skia_path) = build_skia_path(path) else {
2759 return;
2760 };
2761 let mut temp_mask = None;
2762 let Some(mask_ref) = resolve_clip_mask(
2763 &band_state.clip_region,
2764 &mut temp_mask,
2765 ctx.out_w,
2766 ctx.out_h,
2767 ) else {
2768 return;
2769 };
2770 let paint =
2771 to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, ctx.no_aa);
2772 let transform = ctx.transform(¶ms.ctm);
2773
2774 if is_degenerate_fill(path) {
2780 let stroke = Stroke {
2781 width: 1.0,
2782 ..Stroke::default()
2783 };
2784 pixmap.stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
2785 } else {
2786 let fill_rule = to_fill_rule(¶ms.fill_rule);
2787 pixmap.fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
2788 }
2789
2790 if band_state.cmyk_buffer.is_some() {
2792 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2793 let mut spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2794 update_cmyk_buffer_for_fill(
2795 &mut cmyk_buf,
2796 &mut spot_mask,
2797 path,
2798 params,
2799 ctx.vp_x,
2800 ctx.vp_y,
2801 ctx.scale_x,
2802 ctx.scale_y,
2803 ctx.out_w,
2804 ctx.out_h,
2805 &band_state.clip_region,
2806 ctx.no_aa,
2807 ctx.icc,
2808 );
2809 band_state.cmyk_buffer = Some(cmyk_buf);
2810 band_state.restore_spot_mask(spot_mask);
2811 }
2812 }
2813 }
2814 DisplayElement::Stroke { path, params } => {
2815 let mut promoted_stroke: Option<StrokeParams> = None;
2816 let params = maybe_promote_gray_stroke(params, &mut promoted_stroke);
2817 let transform = ctx.transform(¶ms.ctm);
2818 let vp_ctm = Matrix {
2821 a: transform.sx as f64,
2822 b: transform.ky as f64,
2823 c: transform.kx as f64,
2824 d: transform.sy as f64,
2825 tx: 0.0,
2826 ty: 0.0,
2827 };
2828 let vp_params = StrokeParams {
2829 ctm: vp_ctm,
2830 ..params.clone()
2831 };
2832 let stroke = build_stroke(&vp_params, ctx.effective_dpi);
2833
2834 let adjusted;
2836 let draw_path = if params.stroke_adjust
2837 && stroke.width <= 2.0
2838 && ctm_is_device_space(¶ms.ctm)
2839 {
2840 adjusted = stroke_adjust_path_viewport(
2841 path,
2842 stroke.width as f64,
2843 ctx.scale_x as f64,
2844 ctx.scale_y as f64,
2845 ctx.vp_x as f64,
2846 ctx.vp_y as f64,
2847 );
2848 &adjusted
2849 } else {
2850 path
2851 };
2852
2853 let painted = params.painted_channels;
2861 let subset_channels = painted != 0 && painted != stet_graphics::device::CMYK_ALL;
2862 let opm1_cmyk = params.is_device_cmyk && params.overprint_mode == 1;
2863 let custom_spot = painted == 0
2868 && !params.is_device_cmyk
2869 && params.color.native_cmyk.is_some()
2870 && params.color.process_cmyk.is_some();
2871 let is_k_only_cmyk =
2872 params.is_device_cmyk && params.overprint_mode == 0 && is_k_only_src(¶ms.color);
2873 let needs_overprint = params.overprint
2874 && band_state.cmyk_buffer.is_some()
2875 && params.blend_mode == 0
2876 && (subset_channels || opm1_cmyk || custom_spot || is_k_only_cmyk);
2877
2878 let Some(skia_path) = build_skia_path(draw_path) else {
2879 return;
2880 };
2881 let mut temp_mask = None;
2882 let Some(mask_ref) = resolve_clip_mask(
2883 &band_state.clip_region,
2884 &mut temp_mask,
2885 ctx.out_w,
2886 ctx.out_h,
2887 ) else {
2888 return;
2889 };
2890
2891 if needs_overprint {
2892 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2897 let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
2898 let spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2899 render_overprint_stroke(
2900 pixmap,
2901 &mut cmyk_buf,
2902 &mut op_bg,
2903 &mut op_touched,
2904 &spot_mask,
2905 band_state,
2906 &skia_path,
2907 &stroke,
2908 transform,
2909 params,
2910 ctx.out_w,
2911 ctx.out_h,
2912 ctx.icc,
2913 ctx.no_aa,
2914 );
2915 band_state.cmyk_buffer = Some(cmyk_buf);
2916 band_state.restore_op_buffers(op_bg, op_touched);
2917 band_state.restore_spot_mask(spot_mask);
2918 } else {
2919 let paint =
2920 to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, ctx.no_aa);
2921 pixmap.stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
2922
2923 if band_state.cmyk_buffer.is_some() {
2924 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2925 let mut spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2926 update_cmyk_buffer_for_stroke(
2927 &mut cmyk_buf,
2928 &mut spot_mask,
2929 draw_path,
2930 params,
2931 &stroke,
2932 transform,
2933 ctx.out_w,
2934 ctx.out_h,
2935 &band_state.clip_region,
2936 ctx.no_aa,
2937 ctx.icc,
2938 );
2939 band_state.cmyk_buffer = Some(cmyk_buf);
2940 band_state.restore_spot_mask(spot_mask);
2941 }
2942 }
2943 }
2944 DisplayElement::Clip { path, params } => {
2945 clip_path_unified(band_state, path, params, ctx);
2946 }
2947 DisplayElement::InitClip => {
2948 if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
2949 band_state.recycle_mask(mask);
2950 }
2951 band_state.clip_region = None;
2952 }
2953 DisplayElement::ErasePage => {
2954 pixmap.fill(Color::TRANSPARENT);
2955 if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
2956 band_state.recycle_mask(mask);
2957 }
2958 band_state.clip_region = None;
2959 }
2960 DisplayElement::Image {
2961 sample_data,
2962 params,
2963 } => {
2964 let iw = params.width;
2965 let ih = params.height;
2966 if iw == 0 || ih == 0 {
2967 return;
2968 }
2969
2970 let needs_overprint = params.overprint
2971 && band_state.cmyk_buffer.is_some()
2972 && image_supports_overprint(¶ms.color_space);
2973
2974 if needs_overprint {
2975 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2976 let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
2977 render_overprint_image(
2978 pixmap,
2979 &mut cmyk_buf,
2980 &mut op_bg,
2981 &mut op_touched,
2982 band_state,
2983 sample_data,
2984 params,
2985 ctx.vp_x,
2986 ctx.vp_y,
2987 ctx.scale_x,
2988 ctx.scale_y,
2989 ctx.out_w,
2990 ctx.out_h,
2991 ctx.icc,
2992 );
2993 band_state.cmyk_buffer = Some(cmyk_buf);
2994 band_state.restore_op_buffers(op_bg, op_touched);
2995 } else if let Some(pp) = ctx
2996 .preprocessed
2997 .and_then(|pp| pp.get(ctx.elem_idx))
2998 .and_then(|e| e.as_ref())
2999 {
3000 let Some(image_inv) = params.image_matrix.invert() else {
3003 return;
3004 };
3005 let combined = params.ctm.concat(&image_inv);
3006 let raw_transform = ctx.transform(&combined);
3007 let transform = Transform::from_row(
3008 pp.adj_sx,
3009 pp.adj_ky,
3010 pp.adj_kx,
3011 pp.adj_sy,
3012 raw_transform.tx,
3013 raw_transform.ty,
3014 );
3015
3016 let Some(img_pixmap) =
3017 stet_tiny_skia::PixmapRef::from_bytes(&pp.data, pp.width, pp.height)
3018 else {
3019 return;
3020 };
3021 #[allow(unused_assignments)]
3022 let mut temp_mask = None;
3023 let mask_ref = match &band_state.clip_region {
3024 None => None,
3025 Some(ClipRegion::Mask(m)) => Some(m as &Mask),
3026 Some(ClipRegion::Rect(rect)) => {
3027 if rect.is_empty() {
3028 return;
3029 } else if rect.is_full_page(ctx.out_w, ctx.out_h) {
3030 None
3031 } else {
3032 temp_mask = rect.make_mask(ctx.out_w, ctx.out_h);
3033 temp_mask.as_ref()
3034 }
3035 }
3036 };
3037 let img_paint = stet_tiny_skia::PixmapPaint {
3038 quality: pp.quality,
3039 opacity: params.alpha as f32,
3040 blend_mode: u8_to_blend_mode(params.blend_mode),
3041 };
3042 pixmap.draw_pixmap(0, 0, img_pixmap, &img_paint, transform, mask_ref);
3043
3044 if let Some(ref mut cmyk_buf) = band_state.cmyk_buffer {
3050 update_cmyk_buffer_for_image(
3051 cmyk_buf,
3052 sample_data,
3053 pixmap.data(),
3054 params,
3055 ctx.vp_x,
3056 ctx.vp_y,
3057 ctx.scale_x,
3058 ctx.scale_y,
3059 ctx.out_w,
3060 ctx.out_h,
3061 &band_state.clip_region,
3062 ctx.icc,
3063 );
3064 }
3065 } else {
3066 let owned_rgba;
3068 let rgba_data: &[u8] = if let Some(cached) =
3069 ctx.image_cache.and_then(|c| c.get(ctx.elem_idx))
3070 {
3071 cached
3072 } else {
3073 owned_rgba = {
3074 let mut rgba =
3075 samples_to_rgba(sample_data, params, ctx.icc, ctx.opm_zero_transparent);
3076 if params.mask_color.is_some() {
3077 apply_mask_color_rgba(&mut rgba, sample_data, params);
3078 }
3079 rgba
3080 };
3081 &owned_rgba
3082 };
3083 let expected = (iw * ih * 4) as usize;
3084 if rgba_data.len() < expected {
3085 return;
3086 }
3087 let Some(image_inv) = params.image_matrix.invert() else {
3088 return;
3089 };
3090 let combined = params.ctm.concat(&image_inv);
3091 let raw_transform = enforce_min_image_size(ctx.transform(&combined), iw, ih);
3092
3093 let prescaled =
3098 prescale_image(rgba_data, iw, ih, raw_transform, params.interpolate);
3099 let (img_data, img_w, img_h, transform) = match &prescaled {
3100 Some((data, w, h, t)) => (data.as_slice(), *w, *h, *t),
3101 None => (rgba_data, iw, ih, raw_transform),
3102 };
3103
3104 let Some(img_pixmap) =
3105 stet_tiny_skia::PixmapRef::from_bytes(img_data, img_w, img_h)
3106 else {
3107 return;
3108 };
3109 #[allow(unused_assignments)]
3110 let mut temp_mask = None;
3111 let mask_ref = match &band_state.clip_region {
3112 None => None,
3113 Some(ClipRegion::Mask(m)) => Some(m as &Mask),
3114 Some(ClipRegion::Rect(rect)) => {
3115 if rect.is_empty() {
3116 return;
3117 } else if rect.is_full_page(ctx.out_w, ctx.out_h) {
3118 None
3119 } else {
3120 temp_mask = rect.make_mask(ctx.out_w, ctx.out_h);
3121 temp_mask.as_ref()
3122 }
3123 }
3124 };
3125 let img_paint = stet_tiny_skia::PixmapPaint {
3126 quality: image_filter_quality(transform, params.interpolate),
3127 opacity: params.alpha as f32,
3128 blend_mode: u8_to_blend_mode(params.blend_mode),
3129 };
3130 pixmap.draw_pixmap(0, 0, img_pixmap, &img_paint, transform, mask_ref);
3131
3132 if let Some(ref mut cmyk_buf) = band_state.cmyk_buffer {
3136 update_cmyk_buffer_for_image(
3137 cmyk_buf,
3138 sample_data,
3139 pixmap.data(),
3140 params,
3141 ctx.vp_x,
3142 ctx.vp_y,
3143 ctx.scale_x,
3144 ctx.scale_y,
3145 ctx.out_w,
3146 ctx.out_h,
3147 &band_state.clip_region,
3148 ctx.icc,
3149 );
3150 }
3151 }
3152 }
3153 DisplayElement::AxialShading { params } => {
3154 let mut temp_mask = None;
3155 let Some(mask_ref) = resolve_clip_mask(
3156 &band_state.clip_region,
3157 &mut temp_mask,
3158 ctx.out_w,
3159 ctx.out_h,
3160 ) else {
3161 return;
3162 };
3163 render_axial_shading(
3164 pixmap,
3165 params,
3166 ctx.vp_x,
3167 ctx.vp_y,
3168 ctx.scale_x,
3169 ctx.scale_y,
3170 mask_ref,
3171 ctx.no_aa,
3172 band_state.cmyk_buffer.as_deref_mut(),
3173 ctx.icc,
3174 );
3175 }
3176 DisplayElement::RadialShading { params } => {
3177 let mut temp_mask = None;
3178 let Some(mask_ref) = resolve_clip_mask(
3179 &band_state.clip_region,
3180 &mut temp_mask,
3181 ctx.out_w,
3182 ctx.out_h,
3183 ) else {
3184 return;
3185 };
3186 render_radial_shading(
3187 pixmap,
3188 params,
3189 ctx.vp_x,
3190 ctx.vp_y,
3191 ctx.scale_x,
3192 ctx.scale_y,
3193 mask_ref,
3194 ctx.no_aa,
3195 band_state.cmyk_buffer.as_deref_mut(),
3196 ctx.icc,
3197 );
3198 }
3199 DisplayElement::MeshShading { params } => {
3200 let mut temp_mask = None;
3201 let Some(mask_ref) = resolve_clip_mask(
3202 &band_state.clip_region,
3203 &mut temp_mask,
3204 ctx.out_w,
3205 ctx.out_h,
3206 ) else {
3207 return;
3208 };
3209 render_mesh_shading(
3210 pixmap,
3211 params,
3212 ctx.vp_x,
3213 ctx.vp_y,
3214 ctx.scale_x,
3215 ctx.scale_y,
3216 mask_ref,
3217 band_state.cmyk_buffer.as_deref_mut(),
3218 ctx.icc,
3219 );
3220 }
3221 DisplayElement::PatchShading { params } => {
3222 let mut temp_mask = None;
3223 let Some(mask_ref) = resolve_clip_mask(
3224 &band_state.clip_region,
3225 &mut temp_mask,
3226 ctx.out_w,
3227 ctx.out_h,
3228 ) else {
3229 return;
3230 };
3231 render_patch_shading(
3232 pixmap,
3233 params,
3234 ctx.vp_x,
3235 ctx.vp_y,
3236 ctx.scale_x,
3237 ctx.scale_y,
3238 mask_ref,
3239 band_state.cmyk_buffer.as_deref_mut(),
3240 ctx.icc,
3241 );
3242 }
3243 DisplayElement::PatternFill { params } => {
3244 render_pattern_fill(pixmap, band_state, params, ctx);
3245 }
3246 DisplayElement::Group { elements, params } => {
3247 render_group(pixmap, band_state, elements, params, ctx);
3248 }
3249 DisplayElement::SoftMasked {
3250 mask,
3251 content,
3252 params,
3253 mask_cache,
3254 } => {
3255 render_soft_masked(pixmap, band_state, mask, content, params, mask_cache, ctx);
3256 }
3257 DisplayElement::Text { .. } => {} DisplayElement::OcgGroup {
3259 elements,
3260 visibility,
3261 } => {
3262 let visible = ctx.layer_set.evaluate(visibility);
3268 for (idx, elem) in elements.elements().iter().enumerate() {
3269 if !visible
3270 && !matches!(elem, DisplayElement::Clip { .. } | DisplayElement::InitClip)
3271 {
3272 continue;
3273 }
3274 let elem_ctx = RenderContext {
3275 elem_idx: idx,
3276 ..*ctx
3277 };
3278 render_element(pixmap, band_state, elem, &elem_ctx);
3279 }
3280 }
3281 _ => {}
3282 }
3283}
3284
3285fn compute_group_crop(bbox: &[f64; 4], ctx: &RenderContext<'_>) -> Option<(i32, i32, u32, u32)> {
3290 let px_min = ((bbox[0] as f32 - ctx.vp_x) * ctx.scale_x).floor() as i32;
3292 let py_min = ((bbox[1] as f32 - ctx.vp_y) * ctx.scale_y).floor() as i32;
3293 let px_max = ((bbox[2] as f32 - ctx.vp_x) * ctx.scale_x).ceil() as i32;
3294 let py_max = ((bbox[3] as f32 - ctx.vp_y) * ctx.scale_y).ceil() as i32;
3295
3296 let x0 = px_min.max(0);
3298 let y0 = py_min.max(0);
3299 let x1 = px_max.min(ctx.out_w as i32);
3300 let y1 = py_max.min(ctx.out_h as i32);
3301
3302 if x0 >= x1 || y0 >= y1 {
3303 return None;
3304 }
3305
3306 let crop_w = (x1 - x0) as u32;
3307 let crop_h = (y1 - y0) as u32;
3308
3309 let crop_pixels = crop_w as u64 * crop_h as u64;
3311 let full_pixels = ctx.out_w as u64 * ctx.out_h as u64;
3312 if crop_pixels * 4 >= full_pixels * 3 {
3313 return None;
3314 }
3315
3316 Some((x0, y0, crop_w, crop_h))
3317}
3318
3319fn blend_cmyk_separable_channel(cb: f64, cs: f64, mode: u8) -> f64 {
3323 let cbi = 1.0 - cb;
3324 let csi = 1.0 - cs;
3325 let result_inv = match mode {
3326 1 => cbi * csi, 2 => cbi + csi - cbi * csi, 3 => {
3329 if cbi <= 0.5 {
3331 2.0 * cbi * csi
3332 } else {
3333 1.0 - 2.0 * (1.0 - cbi) * (1.0 - csi)
3334 }
3335 }
3336 4 => cbi.min(csi), 5 => cbi.max(csi), 6 => {
3339 if csi >= 1.0 {
3341 1.0
3342 } else {
3343 (cbi / (1.0 - csi)).min(1.0)
3344 }
3345 }
3346 7 => {
3347 if csi <= 0.0 {
3349 0.0
3350 } else {
3351 1.0 - ((1.0 - cbi) / csi).min(1.0)
3352 }
3353 }
3354 8 => {
3355 if csi <= 0.5 {
3357 2.0 * cbi * csi
3358 } else {
3359 1.0 - 2.0 * (1.0 - cbi) * (1.0 - csi)
3360 }
3361 }
3362 9 => {
3363 let d = if cbi <= 0.25 {
3365 ((16.0 * cbi - 12.0) * cbi + 4.0) * cbi
3366 } else {
3367 cbi.sqrt()
3368 };
3369 if csi <= 0.5 {
3370 cbi - (1.0 - 2.0 * csi) * cbi * (1.0 - cbi)
3371 } else {
3372 cbi + (2.0 * csi - 1.0) * (d - cbi)
3373 }
3374 }
3375 10 => (cbi - csi).abs(), 11 => cbi + csi - 2.0 * cbi * csi, _ => csi, };
3379 1.0 - result_inv.clamp(0.0, 1.0)
3380}
3381
3382fn blend_cmyk_nonseparable(cb: [f64; 4], cs: [f64; 4], mode: u8) -> [f64; 4] {
3388 fn lum(c: [f64; 3]) -> f64 {
3389 0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
3390 }
3391 fn clip_color(mut c: [f64; 3]) -> [f64; 3] {
3392 let l = lum(c);
3393 let n = c[0].min(c[1]).min(c[2]);
3394 let x = c[0].max(c[1]).max(c[2]);
3395 if n < 0.0 {
3396 for ci in c.iter_mut() {
3397 *ci = l + (*ci - l) * l / (l - n);
3398 }
3399 }
3400 if x > 1.0 {
3401 for ci in c.iter_mut() {
3402 *ci = l + (*ci - l) * (1.0 - l) / (x - l);
3403 }
3404 }
3405 c
3406 }
3407 fn set_lum(c: [f64; 3], l: f64) -> [f64; 3] {
3408 let d = l - lum(c);
3409 clip_color([c[0] + d, c[1] + d, c[2] + d])
3410 }
3411 fn sat(c: [f64; 3]) -> f64 {
3412 c[0].max(c[1]).max(c[2]) - c[0].min(c[1]).min(c[2])
3413 }
3414 fn set_sat(c: [f64; 3], s: f64) -> [f64; 3] {
3415 let mut idx = [0usize, 1, 2];
3417 idx.sort_by(|a, b| {
3418 c[*a]
3419 .partial_cmp(&c[*b])
3420 .unwrap_or(std::cmp::Ordering::Equal)
3421 });
3422 let (i_min, i_mid, i_max) = (idx[0], idx[1], idx[2]);
3423 let mut out = c;
3424 if c[i_max] > c[i_min] {
3425 out[i_mid] = (c[i_mid] - c[i_min]) * s / (c[i_max] - c[i_min]);
3426 out[i_max] = s;
3427 } else {
3428 out[i_mid] = 0.0;
3429 out[i_max] = 0.0;
3430 }
3431 out[i_min] = 0.0;
3432 out
3433 }
3434
3435 let cb_rgb = [1.0 - cb[0], 1.0 - cb[1], 1.0 - cb[2]];
3436 let cs_rgb = [1.0 - cs[0], 1.0 - cs[1], 1.0 - cs[2]];
3437 let result_rgb = match mode {
3438 12 => set_lum(set_sat(cs_rgb, sat(cb_rgb)), lum(cb_rgb)), 13 => set_lum(set_sat(cb_rgb, sat(cs_rgb)), lum(cb_rgb)), 14 => set_lum(cs_rgb, lum(cb_rgb)), 15 => set_lum(cb_rgb, lum(cs_rgb)), _ => cs_rgb,
3443 };
3444 let result_k = if mode == 15 { cs[3] } else { cb[3] };
3448 [
3449 (1.0 - result_rgb[0]).clamp(0.0, 1.0),
3450 (1.0 - result_rgb[1]).clamp(0.0, 1.0),
3451 (1.0 - result_rgb[2]).clamp(0.0, 1.0),
3452 result_k,
3453 ]
3454}
3455
3456fn ps_path_bbox(path: &PsPath) -> Option<(f64, f64, f64, f64)> {
3461 let mut it = path.segments.iter().filter_map(|seg| match *seg {
3462 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => Some(vec![(x, y)]),
3463 PathSegment::CurveTo {
3464 x1,
3465 y1,
3466 x2,
3467 y2,
3468 x3,
3469 y3,
3470 } => Some(vec![(x1, y1), (x2, y2), (x3, y3)]),
3471 PathSegment::ClosePath => None,
3472 });
3473 let first = it.next()?.into_iter().next()?;
3474 let (mut x0, mut y0) = first;
3475 let (mut x1, mut y1) = first;
3476 for seg_points in std::iter::once(vec![first]).chain(it) {
3477 for (x, y) in seg_points {
3478 x0 = x0.min(x);
3479 y0 = y0.min(y);
3480 x1 = x1.max(x);
3481 y1 = y1.max(y);
3482 }
3483 }
3484 Some((x0, y0, x1, y1))
3485}
3486
3487fn bbox_contains(outer: (f64, f64, f64, f64), inner: (f64, f64, f64, f64), tolerance: f64) -> bool {
3490 inner.0 >= outer.0 - tolerance
3491 && inner.1 >= outer.1 - tolerance
3492 && inner.2 <= outer.2 + tolerance
3493 && inner.3 <= outer.3 + tolerance
3494}
3495
3496fn compute_obscured_fill_skips(elements: &DisplayList) -> Vec<usize> {
3522 let mut skips = Vec::new();
3523 let els = elements.elements();
3524 for i in 0..els.len() {
3525 let DisplayElement::Fill {
3526 path: parent_path,
3527 params: parent_params,
3528 } = &els[i]
3529 else {
3530 continue;
3531 };
3532 if (parent_params.alpha - 1.0).abs() > 1e-6 || parent_params.blend_mode != 0 {
3533 continue;
3534 }
3535 let Some(parent_bbox) = ps_path_bbox(parent_path) else {
3536 continue;
3537 };
3538 let mut j = i + 1;
3542 let mut clips_ok = true;
3543 while j < els.len() {
3544 match &els[j] {
3545 DisplayElement::InitClip => {}
3546 DisplayElement::Clip {
3547 path: clip_path, ..
3548 } => match ps_path_bbox(clip_path) {
3549 Some(cb) if bbox_contains(cb, parent_bbox, 0.5) => {}
3550 _ => {
3551 clips_ok = false;
3552 break;
3553 }
3554 },
3555 _ => break,
3556 }
3557 j += 1;
3558 }
3559 if !clips_ok {
3560 continue;
3561 }
3562 let Some(DisplayElement::Group {
3563 elements: group_elements,
3564 params: group_params,
3565 }) = els.get(j)
3566 else {
3567 continue;
3568 };
3569 if !group_params.isolated
3570 || (group_params.alpha - 1.0).abs() > 1e-6
3571 || group_params.blend_mode != 0
3572 {
3573 continue;
3574 }
3575 let group_bbox = (
3579 group_params.bbox[0],
3580 group_params.bbox[1],
3581 group_params.bbox[2],
3582 group_params.bbox[3],
3583 );
3584 if !bbox_contains(group_bbox, parent_bbox, 0.5) {
3585 continue;
3586 }
3587 let inner_els = group_elements.elements();
3590 let mut k = 0;
3591 let mut inner_clips_ok = true;
3592 while k < inner_els.len() {
3593 match &inner_els[k] {
3594 DisplayElement::InitClip => {}
3595 DisplayElement::Clip {
3596 path: clip_path, ..
3597 } => match ps_path_bbox(clip_path) {
3598 Some(cb) if bbox_contains(cb, parent_bbox, 0.5) => {}
3599 _ => {
3600 inner_clips_ok = false;
3601 break;
3602 }
3603 },
3604 _ => break,
3605 }
3606 k += 1;
3607 }
3608 if !inner_clips_ok {
3609 continue;
3610 }
3611 let Some(DisplayElement::Fill {
3612 path: group_path,
3613 params: group_fill_params,
3614 }) = inner_els.get(k)
3615 else {
3616 continue;
3617 };
3618 if (group_fill_params.alpha - 1.0).abs() > 1e-6 || group_fill_params.blend_mode != 0 {
3619 continue;
3620 }
3621 if paths_approximately_equal(parent_path, group_path, 0.5) {
3622 skips.push(i);
3623 }
3624 }
3625 skips
3626}
3627
3628fn paths_approximately_equal(a: &PsPath, b: &PsPath, tolerance: f64) -> bool {
3634 if a.segments.len() != b.segments.len() {
3635 return false;
3636 }
3637 for (sa, sb) in a.segments.iter().zip(b.segments.iter()) {
3638 let close_pair = |(x1, y1): (f64, f64), (x2, y2): (f64, f64)| -> bool {
3639 (x1 - x2).abs() <= tolerance && (y1 - y2).abs() <= tolerance
3640 };
3641 match (sa, sb) {
3642 (PathSegment::MoveTo(x1, y1), PathSegment::MoveTo(x2, y2)) => {
3643 if !close_pair((*x1, *y1), (*x2, *y2)) {
3644 return false;
3645 }
3646 }
3647 (PathSegment::LineTo(x1, y1), PathSegment::LineTo(x2, y2)) => {
3648 if !close_pair((*x1, *y1), (*x2, *y2)) {
3649 return false;
3650 }
3651 }
3652 (
3653 PathSegment::CurveTo {
3654 x1: ax1,
3655 y1: ay1,
3656 x2: ax2,
3657 y2: ay2,
3658 x3: ax3,
3659 y3: ay3,
3660 },
3661 PathSegment::CurveTo {
3662 x1: bx1,
3663 y1: by1,
3664 x2: bx2,
3665 y2: by2,
3666 x3: bx3,
3667 y3: by3,
3668 },
3669 ) => {
3670 if !close_pair((*ax1, *ay1), (*bx1, *by1))
3671 || !close_pair((*ax2, *ay2), (*bx2, *by2))
3672 || !close_pair((*ax3, *ay3), (*bx3, *by3))
3673 {
3674 return false;
3675 }
3676 }
3677 (PathSegment::ClosePath, PathSegment::ClosePath) => {}
3678 _ => return false,
3679 }
3680 }
3681 true
3682}
3683
3684fn render_group(
3688 pixmap: &mut Pixmap,
3689 band_state: &mut BandState,
3690 elements: &DisplayList,
3691 params: &stet_graphics::display_list::GroupParams,
3692 ctx: &RenderContext<'_>,
3693) {
3694 if params.knockout {
3695 render_knockout_group(pixmap, band_state, elements, params, ctx);
3696 return;
3697 }
3698
3699 let crop = compute_group_crop(¶ms.bbox, ctx);
3700
3701 let (eff_w, eff_h, crop_x, crop_y, eff_vp_x, eff_vp_y) = match crop {
3702 Some((cx, cy, cw, ch)) => (
3703 cw,
3704 ch,
3705 cx,
3706 cy,
3707 ctx.vp_x + cx as f32 / ctx.scale_x,
3708 ctx.vp_y + cy as f32 / ctx.scale_y,
3709 ),
3710 None => (ctx.out_w, ctx.out_h, 0, 0, ctx.vp_x, ctx.vp_y),
3711 };
3712
3713 let Some(mut offscreen) = Pixmap::new(eff_w, eff_h) else {
3714 return;
3715 };
3716
3717 use stet_graphics::display_list::GroupColorSpace;
3725
3726 let needs_group_cmyk = has_overprint_elements(elements)
3732 || band_state.cmyk_buffer.is_some()
3733 || params.color_space == GroupColorSpace::DeviceCMYK
3734 || has_cmyk_group(elements);
3735
3736 let force_cmyk_compose =
3743 std::env::var_os("STET_FORCE_CMYK_COMPOSITE_BACK").as_deref() == Some("1".as_ref());
3744 let plan_cmyk_compose = match ctx.knockout_painter_pass {
3755 KnockoutPainterPass::CoveragePass => false,
3756 KnockoutPainterPass::ColorPass => {
3757 !params.isolated
3758 && params.blend_mode != 0
3759 && needs_group_cmyk
3760 && band_state.cmyk_buffer.is_some()
3761 && group_content_is_native_cmyk(elements)
3762 }
3763 KnockoutPainterPass::None if force_cmyk_compose => {
3764 !params.isolated
3782 && matches!(params.blend_mode, 10..=15)
3783 && needs_group_cmyk
3784 && band_state.cmyk_buffer.is_some()
3785 && group_content_is_native_cmyk(elements)
3786 }
3787 KnockoutPainterPass::None => {
3788 let inversion_sensitive = !params.isolated
3793 && matches!(params.blend_mode, 10..=15)
3794 && group_only_native_cmyk_fills(elements);
3795 let proofing_enabled = ctx.icc.is_some_and(|c| c.proofing_enabled());
3823 let effective_cs_is_cmyk = params.color_space == GroupColorSpace::DeviceCMYK
3832 || (params.color_space == GroupColorSpace::Inherited
3833 && band_state.cmyk_buffer.is_some());
3834 let cmyk_group_blend = !params.isolated
3835 && (ctx.parent_group_isolated || proofing_enabled)
3836 && params.blend_mode != 0
3837 && effective_cs_is_cmyk
3838 && needs_group_cmyk
3839 && band_state.cmyk_buffer.is_some()
3840 && group_content_is_native_cmyk(elements);
3841 inversion_sensitive || cmyk_group_blend
3842 }
3843 };
3844 let needs_alpha_extraction = !params.isolated
3849 && params.blend_mode != 0
3850 && !plan_cmyk_compose
3851 && !ctx.alpha_extraction_pass;
3852 let needs_backdrop_preload =
3853 !params.isolated && (params.blend_mode == 0 || plan_cmyk_compose || needs_alpha_extraction);
3854 let backdrop = if needs_backdrop_preload {
3855 let data = if crop.is_some() {
3856 copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h)
3857 } else {
3858 pixmap.data().to_vec()
3859 };
3860 offscreen.data_mut().copy_from_slice(&data);
3861 Some(data)
3862 } else {
3863 None
3864 };
3865 let group_cmyk = if needs_group_cmyk {
3866 let buf_size = eff_w as usize * eff_h as usize * 4;
3867 let mut buf = vec![0.0f32; buf_size];
3868 if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
3869 let parent_stride = ctx.out_w as usize * 4;
3870 let group_stride = eff_w as usize * 4;
3871 for gy in 0..eff_h as usize {
3872 let py = crop_y as usize + gy;
3873 if py < ctx.out_h as usize {
3874 let p_start = py * parent_stride + crop_x as usize * 4;
3875 let g_start = gy * group_stride;
3876 let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
3877 buf[g_start..g_start + copy_len]
3878 .copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
3879 }
3880 }
3881 }
3882 Some(buf)
3883 } else {
3884 None
3885 };
3886
3887 let backdrop_cmyk: Option<Vec<f32>> = if !params.isolated {
3892 group_cmyk.clone()
3893 } else {
3894 None
3895 };
3896
3897 let mut group_band = BandState {
3898 clip_region: None,
3899 spare_mask: None,
3900 clip_mask_cache: HashMap::new(),
3901 clip_mask_seen: HashSet::new(),
3902 mask_pool: Vec::new(),
3903 cmyk_buffer: group_cmyk,
3904 op_bg_snapshot: None,
3905 op_touched: None,
3906 spot_mask: None,
3907 };
3908
3909 let group_ctx = RenderContext {
3910 vp_x: eff_vp_x,
3911 vp_y: eff_vp_y,
3912 scale_x: ctx.scale_x,
3913 scale_y: ctx.scale_y,
3914 out_w: eff_w,
3915 out_h: eff_h,
3916 effective_dpi: ctx.effective_dpi,
3917 icc: ctx.icc,
3918 image_cache: None, preprocessed: None,
3920 elem_idx: 0,
3921 no_aa: ctx.no_aa,
3922 opm_zero_transparent: ctx.opm_zero_transparent,
3923 knockout_painter_pass: ctx.knockout_painter_pass,
3924 parent_group_isolated: params.isolated,
3926 alpha_extraction_pass: ctx.alpha_extraction_pass,
3927 layer_set: ctx.layer_set,
3928 };
3929
3930 let skip_indices = compute_obscured_fill_skips(elements);
3931 for (idx, elem) in elements.elements().iter().enumerate() {
3932 if skip_indices.contains(&idx) {
3933 continue;
3934 }
3935 let elem_ctx = RenderContext {
3936 elem_idx: idx,
3937 ..group_ctx
3938 };
3939 render_element(&mut offscreen, &mut group_band, elem, &elem_ctx);
3940 }
3941
3942 let alpha_offscreen = if needs_alpha_extraction {
3946 let mut iso = Pixmap::new(eff_w, eff_h);
3947 if let Some(ref mut iso_pm) = iso {
3948 let mut iso_band = BandState {
3949 clip_region: None,
3950 spare_mask: None,
3951 clip_mask_cache: HashMap::new(),
3952 clip_mask_seen: HashSet::new(),
3953 mask_pool: Vec::new(),
3954 cmyk_buffer: None,
3955 op_bg_snapshot: None,
3956 op_touched: None,
3957 spot_mask: None,
3958 };
3959 let iso_ctx = RenderContext {
3960 parent_group_isolated: true,
3961 alpha_extraction_pass: true,
3962 ..group_ctx
3963 };
3964 for (idx, elem) in elements.elements().iter().enumerate() {
3965 let elem_ctx = RenderContext {
3966 elem_idx: idx,
3967 ..iso_ctx
3968 };
3969 render_element(iso_pm, &mut iso_band, elem, &elem_ctx);
3970 }
3971 }
3972 iso
3973 } else {
3974 None
3975 };
3976
3977 let mut temp_mask = None;
3978 let mask_ref = match resolve_clip_mask(
3979 &band_state.clip_region,
3980 &mut temp_mask,
3981 ctx.out_w,
3982 ctx.out_h,
3983 ) {
3984 None => return, Some(m) => m,
3986 };
3987
3988 let coverage_params;
3993 let effective_params: &stet_graphics::display_list::GroupParams =
3994 if ctx.knockout_painter_pass == KnockoutPainterPass::CoveragePass {
3995 coverage_params = stet_graphics::display_list::GroupParams {
3996 alpha: 1.0,
3997 blend_mode: 0,
3998 ..params.clone()
3999 };
4000 &coverage_params
4001 } else {
4002 params
4003 };
4004
4005 let mut cmyk_compose_done = false;
4006 if let Some(backdrop) = &backdrop {
4007 let inner_cmyk = group_band.cmyk_buffer.as_deref();
4025 let pre_cmyk = backdrop_cmyk.as_deref();
4026 if plan_cmyk_compose && let (Some(inner), Some(pre)) = (inner_cmyk, pre_cmyk) {
4027 composite_non_isolated_cmyk(
4028 pixmap,
4029 band_state.cmyk_buffer.as_deref_mut(),
4030 &offscreen,
4031 inner,
4032 pre,
4033 backdrop,
4034 effective_params,
4035 mask_ref,
4036 crop_x,
4037 crop_y,
4038 ctx.icc,
4039 );
4040 cmyk_compose_done = true;
4041 } else if let Some(ref alpha_os) = alpha_offscreen {
4042 composite_non_isolated_extracted(
4043 pixmap,
4044 &offscreen,
4045 alpha_os,
4046 backdrop,
4047 effective_params,
4048 mask_ref,
4049 crop_x,
4050 crop_y,
4051 );
4052 } else {
4053 composite_non_isolated_group_cropped(
4054 pixmap,
4055 &offscreen,
4056 backdrop,
4057 effective_params,
4058 mask_ref,
4059 crop_x,
4060 crop_y,
4061 );
4062 }
4063 } else {
4064 let paint = stet_tiny_skia::PixmapPaint {
4065 opacity: effective_params.alpha as f32,
4066 blend_mode: u8_to_blend_mode(effective_params.blend_mode),
4067 quality: stet_tiny_skia::FilterQuality::Nearest,
4068 };
4069 pixmap.draw_pixmap(
4070 crop_x,
4071 crop_y,
4072 offscreen.as_ref(),
4073 &paint,
4074 Transform::identity(),
4075 mask_ref,
4076 );
4077 }
4078
4079 if !cmyk_compose_done
4085 && let (Some(group_cmyk), Some(parent_cmyk)) =
4086 (&group_band.cmyk_buffer, &mut band_state.cmyk_buffer)
4087 {
4088 copy_cmyk_buffer_to_parent(
4089 parent_cmyk,
4090 group_cmyk,
4091 offscreen.data(),
4092 crop_x as usize,
4093 crop_y as usize,
4094 eff_w as usize,
4095 eff_h as usize,
4096 ctx.out_w as usize,
4097 ctx.out_h as usize,
4098 );
4099 }
4100}
4101
4102#[allow(clippy::too_many_arguments)]
4114fn composite_non_isolated_cmyk(
4115 target: &mut Pixmap,
4116 parent_cmyk: Option<&mut [f32]>,
4117 source: &Pixmap,
4118 source_cmyk: &[f32],
4119 backdrop_cmyk: &[f32],
4120 backdrop_pixels: &[u8],
4121 params: &stet_graphics::display_list::GroupParams,
4122 clip_mask: Option<&stet_tiny_skia::Mask>,
4123 crop_x: i32,
4124 crop_y: i32,
4125 icc: Option<&IccCache>,
4126) {
4127 let cw = source.width() as usize;
4128 let ch = source.height() as usize;
4129 let target_w = target.width() as usize;
4130 let target_h = target.height() as usize;
4131
4132 let opacity = params.alpha.clamp(0.0, 1.0);
4133 let blend_mode = params.blend_mode;
4134 let is_nonseparable = matches!(blend_mode, 12..=15);
4135
4136 let target_data = target.data_mut();
4137 let target_stride = target_w * 4;
4138 let group_stride = cw * 4;
4139
4140 let clip_data = clip_mask.map(|m| m.data());
4141
4142 for gy in 0..ch {
4143 let ty = crop_y + gy as i32;
4144 if ty < 0 || ty as usize >= target_h {
4145 continue;
4146 }
4147 let ty = ty as usize;
4148 let group_row = gy * group_stride;
4149 let target_row = ty * target_stride;
4150
4151 for gx in 0..cw {
4152 let tx = crop_x + gx as i32;
4153 if tx < 0 || tx as usize >= target_w {
4154 continue;
4155 }
4156 let tx = tx as usize;
4157 let gi = group_row + gx * 4;
4158 let ti = target_row + tx * 4;
4159
4160 let bc = backdrop_cmyk[gi] as f64;
4162 let bm = backdrop_cmyk[gi + 1] as f64;
4163 let by_ = backdrop_cmyk[gi + 2] as f64;
4164 let bk = backdrop_cmyk[gi + 3] as f64;
4165 let sc = source_cmyk[gi] as f64;
4166 let sm = source_cmyk[gi + 1] as f64;
4167 let sy_ = source_cmyk[gi + 2] as f64;
4168 let sk = source_cmyk[gi + 3] as f64;
4169 if (sc - bc).abs() < 1.0 / 255.0
4170 && (sm - bm).abs() < 1.0 / 255.0
4171 && (sy_ - by_).abs() < 1.0 / 255.0
4172 && (sk - bk).abs() < 1.0 / 255.0
4173 {
4174 continue;
4175 }
4176
4177 let cov = if let Some(cd) = clip_data {
4179 cd[ty * target_w + tx] as f64 / 255.0
4180 } else {
4181 1.0
4182 };
4183 if cov <= 0.0 {
4184 continue;
4185 }
4186
4187 let backdrop_alpha = backdrop_pixels[gi + 3];
4206 let backdrop_transparent = backdrop_alpha == 0;
4207
4208 let mix = cov * opacity;
4209 let dst_a = target_data[ti + 3] as f64 / 255.0;
4210
4211 if backdrop_transparent {
4212 let src_data = source.data();
4225 let src_a_pm = src_data[gi + 3] as f64 / 255.0;
4226 if src_a_pm <= 0.0 {
4227 continue;
4228 }
4229 let (full_r, full_g, full_b) = icc
4234 .and_then(|i| i.convert_cmyk_readonly(sc, sm, sy_, sk))
4235 .unwrap_or_else(|| cmyk_to_rgb_plrm(sc, sm, sy_, sk));
4236 let alpha_s = mix;
4237 let inv_sa = 1.0 - alpha_s;
4238 let dst_r_pm = target_data[ti] as f64 / 255.0;
4239 let dst_g_pm = target_data[ti + 1] as f64 / 255.0;
4240 let dst_b_pm = target_data[ti + 2] as f64 / 255.0;
4241 let out_r = full_r * alpha_s + dst_r_pm * inv_sa;
4242 let out_g = full_g * alpha_s + dst_g_pm * inv_sa;
4243 let out_b = full_b * alpha_s + dst_b_pm * inv_sa;
4244 let out_a = alpha_s + dst_a * inv_sa;
4245 target_data[ti] = (out_r * 255.0).round().clamp(0.0, 255.0) as u8;
4246 target_data[ti + 1] = (out_g * 255.0).round().clamp(0.0, 255.0) as u8;
4247 target_data[ti + 2] = (out_b * 255.0).round().clamp(0.0, 255.0) as u8;
4248 target_data[ti + 3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4249 continue;
4250 }
4251
4252 let (rc, rm, ry, rk) = if is_nonseparable {
4254 let r = blend_cmyk_nonseparable([bc, bm, by_, bk], [sc, sm, sy_, sk], blend_mode);
4255 (r[0], r[1], r[2], r[3])
4256 } else {
4257 (
4258 blend_cmyk_separable_channel(bc, sc, blend_mode),
4259 blend_cmyk_separable_channel(bm, sm, blend_mode),
4260 blend_cmyk_separable_channel(by_, sy_, blend_mode),
4261 blend_cmyk_separable_channel(bk, sk, blend_mode),
4262 )
4263 };
4264
4265 let (new_r, new_g, new_b) = icc
4266 .and_then(|i| i.convert_cmyk_readonly(rc, rm, ry, rk))
4267 .unwrap_or_else(|| cmyk_to_rgb_plrm(rc, rm, ry, rk));
4268
4269 let alpha_s = mix;
4282 let alpha_b = dst_a;
4283 let out_a = alpha_s + alpha_b * (1.0 - alpha_s);
4284 if out_a <= 0.0 {
4285 continue;
4286 }
4287 let (dst_r, dst_g, dst_b) = if alpha_b > 0.0 {
4288 let inv_a = 1.0 / alpha_b;
4289 (
4290 (target_data[ti] as f64 / 255.0) * inv_a,
4291 (target_data[ti + 1] as f64 / 255.0) * inv_a,
4292 (target_data[ti + 2] as f64 / 255.0) * inv_a,
4293 )
4294 } else {
4295 (0.0, 0.0, 0.0)
4296 };
4297 let coef_b = alpha_s * alpha_b;
4305 let coef_s = alpha_s * (1.0 - alpha_b);
4306 let coef_d = (1.0 - alpha_s) * alpha_b;
4307 let out_r = (coef_s * new_r + coef_b * new_r + coef_d * dst_r) / out_a;
4308 let out_g = (coef_s * new_g + coef_b * new_g + coef_d * dst_g) / out_a;
4309 let out_b = (coef_s * new_b + coef_b * new_b + coef_d * dst_b) / out_a;
4310
4311 target_data[ti] = (out_r * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4312 target_data[ti + 1] = (out_g * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4313 target_data[ti + 2] = (out_b * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4314 target_data[ti + 3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4315 }
4316 }
4317
4318 if let Some(parent_cmyk) = parent_cmyk {
4323 for gy in 0..ch {
4324 let ty = crop_y + gy as i32;
4325 if ty < 0 || ty as usize >= target_h {
4326 continue;
4327 }
4328 let ty = ty as usize;
4329 let group_row = gy * group_stride;
4330 let parent_row = ty * target_stride;
4331
4332 for gx in 0..cw {
4333 let tx = crop_x + gx as i32;
4334 if tx < 0 || tx as usize >= target_w {
4335 continue;
4336 }
4337 let tx = tx as usize;
4338 let gi = group_row + gx * 4;
4339 let pi = parent_row + tx * 4;
4340
4341 let bc = backdrop_cmyk[gi] as f64;
4342 let bm = backdrop_cmyk[gi + 1] as f64;
4343 let by_ = backdrop_cmyk[gi + 2] as f64;
4344 let bk = backdrop_cmyk[gi + 3] as f64;
4345 let sc = source_cmyk[gi] as f64;
4346 let sm = source_cmyk[gi + 1] as f64;
4347 let sy_ = source_cmyk[gi + 2] as f64;
4348 let sk = source_cmyk[gi + 3] as f64;
4349 if (sc - bc).abs() < 1.0 / 255.0
4350 && (sm - bm).abs() < 1.0 / 255.0
4351 && (sy_ - by_).abs() < 1.0 / 255.0
4352 && (sk - bk).abs() < 1.0 / 255.0
4353 {
4354 continue;
4355 }
4356
4357 let backdrop_transparent = backdrop_pixels[gi + 3] == 0;
4363 let (rc, rm, ry, rk) = if backdrop_transparent {
4364 (sc, sm, sy_, sk)
4365 } else if is_nonseparable {
4366 let r =
4367 blend_cmyk_nonseparable([bc, bm, by_, bk], [sc, sm, sy_, sk], blend_mode);
4368 (r[0], r[1], r[2], r[3])
4369 } else {
4370 (
4371 blend_cmyk_separable_channel(bc, sc, blend_mode),
4372 blend_cmyk_separable_channel(bm, sm, blend_mode),
4373 blend_cmyk_separable_channel(by_, sy_, blend_mode),
4374 blend_cmyk_separable_channel(bk, sk, blend_mode),
4375 )
4376 };
4377 parent_cmyk[pi] = rc as f32;
4378 parent_cmyk[pi + 1] = rm as f32;
4379 parent_cmyk[pi + 2] = ry as f32;
4380 parent_cmyk[pi + 3] = rk as f32;
4381 }
4382 }
4383 }
4384}
4385
4386fn render_knockout_group(
4391 pixmap: &mut Pixmap,
4392 band_state: &mut BandState,
4393 elements: &DisplayList,
4394 params: &stet_graphics::display_list::GroupParams,
4395 ctx: &RenderContext<'_>,
4396) {
4397 let crop = compute_group_crop(¶ms.bbox, ctx);
4398
4399 let (eff_w, eff_h, crop_x, crop_y, eff_vp_x, eff_vp_y) = match crop {
4400 Some((cx, cy, cw, ch)) => (
4401 cw,
4402 ch,
4403 cx,
4404 cy,
4405 ctx.vp_x + cx as f32 / ctx.scale_x,
4406 ctx.vp_y + cy as f32 / ctx.scale_y,
4407 ),
4408 None => (ctx.out_w, ctx.out_h, 0, 0, ctx.vp_x, ctx.vp_y),
4409 };
4410
4411 let Some(mut offscreen) = Pixmap::new(eff_w, eff_h) else {
4412 return;
4413 };
4414
4415 let initial_backdrop = if !params.isolated {
4416 if crop.is_some() {
4417 copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h)
4418 } else {
4419 pixmap.data().to_vec()
4420 }
4421 } else {
4422 vec![0u8; (eff_w * eff_h * 4) as usize]
4423 };
4424
4425 let Some(mut accumulated) = Pixmap::new(eff_w, eff_h) else {
4426 return;
4427 };
4428 accumulated.data_mut().copy_from_slice(&initial_backdrop);
4429
4430 let needs_cmyk = has_overprint_elements(elements) || band_state.cmyk_buffer.is_some();
4432 let initial_cmyk = if needs_cmyk {
4433 let buf_size = eff_w as usize * eff_h as usize * 4;
4434 let mut buf = vec![0.0f32; buf_size];
4435 if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
4436 let parent_stride = ctx.out_w as usize * 4;
4437 let group_stride = eff_w as usize * 4;
4438 for gy in 0..eff_h as usize {
4439 let py = crop_y as usize + gy;
4440 if py < ctx.out_h as usize {
4441 let p_start = py * parent_stride + crop_x as usize * 4;
4442 let g_start = gy * group_stride;
4443 let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
4444 buf[g_start..g_start + copy_len]
4445 .copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
4446 }
4447 }
4448 }
4449 Some(buf)
4450 } else {
4451 None
4452 };
4453
4454 let mut accumulated_cmyk = initial_cmyk.clone();
4455
4456 let group_ctx = RenderContext {
4461 vp_x: eff_vp_x,
4462 vp_y: eff_vp_y,
4463 scale_x: ctx.scale_x,
4464 scale_y: ctx.scale_y,
4465 out_w: eff_w,
4466 out_h: eff_h,
4467 effective_dpi: ctx.effective_dpi,
4468 icc: ctx.icc,
4469 image_cache: None,
4470 preprocessed: None,
4471 elem_idx: 0,
4472 no_aa: true,
4473 opm_zero_transparent: ctx.opm_zero_transparent,
4474 knockout_painter_pass: ctx.knockout_painter_pass,
4475 parent_group_isolated: true,
4479 alpha_extraction_pass: false,
4480 layer_set: ctx.layer_set,
4481 };
4482
4483 let mut ko_band = BandState {
4487 clip_region: None,
4488 spare_mask: None,
4489 clip_mask_cache: HashMap::new(),
4490 clip_mask_seen: HashSet::new(),
4491 mask_pool: Vec::new(),
4492 cmyk_buffer: None,
4493 op_bg_snapshot: None,
4494 op_touched: None,
4495 spot_mask: None,
4496 };
4497
4498 let mut coverage_offscreen: Option<Pixmap> = None;
4501
4502 for elem in elements.elements() {
4503 match elem {
4504 DisplayElement::Clip { .. } | DisplayElement::InitClip => {
4506 render_element(&mut offscreen, &mut ko_band, elem, &group_ctx);
4507 }
4508 DisplayElement::Group { .. } => {
4518 let pass1_ctx = RenderContext {
4525 knockout_painter_pass: KnockoutPainterPass::ColorPass,
4526 ..group_ctx
4527 };
4528 offscreen.data_mut().copy_from_slice(&initial_backdrop);
4529 ko_band.cmyk_buffer = initial_cmyk.clone();
4530 render_element(&mut offscreen, &mut ko_band, elem, &pass1_ctx);
4531 let pass1_cmyk = ko_band.cmyk_buffer.take();
4532
4533 let cov = match coverage_offscreen.as_mut() {
4538 Some(p) => {
4539 p.data_mut().fill(0);
4540 p
4541 }
4542 None => {
4543 let Some(p) = Pixmap::new(eff_w, eff_h) else {
4544 replace_changed_pixels(
4548 accumulated.data_mut(),
4549 offscreen.data(),
4550 &initial_backdrop,
4551 );
4552 if let (Some(p1), Some(acc)) = (&pass1_cmyk, &mut accumulated_cmyk) {
4553 replace_changed_cmyk(acc, p1, offscreen.data(), &initial_backdrop);
4554 }
4555 continue;
4556 };
4557 coverage_offscreen = Some(p);
4558 coverage_offscreen.as_mut().unwrap()
4559 }
4560 };
4561 ko_band.cmyk_buffer = None;
4562 let coverage_ctx = RenderContext {
4567 knockout_painter_pass: KnockoutPainterPass::CoveragePass,
4568 ..group_ctx
4569 };
4570 render_element(cov, &mut ko_band, elem, &coverage_ctx);
4571
4572 replace_with_coverage_mask(accumulated.data_mut(), offscreen.data(), cov.data());
4576
4577 if let (Some(p1_cmyk), Some(acc_cmyk)) = (&pass1_cmyk, &mut accumulated_cmyk) {
4578 replace_cmyk_with_coverage_mask(acc_cmyk, p1_cmyk, cov.data());
4579 }
4580 ko_band.cmyk_buffer = None;
4581 }
4582 _ => {
4586 offscreen.data_mut().copy_from_slice(&initial_backdrop);
4587
4588 ko_band.cmyk_buffer = initial_cmyk.clone();
4589
4590 render_element(&mut offscreen, &mut ko_band, elem, &group_ctx);
4591
4592 if let (Some(elem_cmyk), Some(acc_cmyk)) =
4593 (&ko_band.cmyk_buffer, &mut accumulated_cmyk)
4594 {
4595 replace_changed_cmyk(acc_cmyk, elem_cmyk, offscreen.data(), &initial_backdrop);
4596 }
4597 ko_band.cmyk_buffer = None;
4598
4599 replace_changed_pixels(accumulated.data_mut(), offscreen.data(), &initial_backdrop);
4600 }
4601 }
4602 }
4603
4604 let mut temp_mask = None;
4605 let mask_ref = resolve_clip_mask(
4606 &band_state.clip_region,
4607 &mut temp_mask,
4608 ctx.out_w,
4609 ctx.out_h,
4610 );
4611 let mask_ref = match mask_ref {
4612 None => return,
4613 Some(m) => m,
4614 };
4615
4616 composite_non_isolated_group_cropped(
4617 pixmap,
4618 &accumulated,
4619 &initial_backdrop,
4620 params,
4621 mask_ref,
4622 crop_x,
4623 crop_y,
4624 );
4625
4626 if let (Some(acc_cmyk), Some(parent_cmyk)) = (&accumulated_cmyk, &mut band_state.cmyk_buffer) {
4627 copy_cmyk_buffer_to_parent(
4628 parent_cmyk,
4629 acc_cmyk,
4630 accumulated.data(),
4631 crop_x as usize,
4632 crop_y as usize,
4633 eff_w as usize,
4634 eff_h as usize,
4635 ctx.out_w as usize,
4636 ctx.out_h as usize,
4637 );
4638 }
4639}
4640fn replace_with_coverage_mask(target: &mut [u8], source: &[u8], coverage: &[u8]) {
4648 for i in (0..target.len()).step_by(4) {
4649 let cov_a = coverage[i + 3];
4650 if cov_a == 0 {
4651 continue;
4652 }
4653 if cov_a == 255 {
4654 target[i..i + 4].copy_from_slice(&source[i..i + 4]);
4655 continue;
4656 }
4657 let a = cov_a as u32;
4658 let inv = 255 - a;
4659 for c in 0..4 {
4660 let s = source[i + c] as u32;
4661 let t = target[i + c] as u32;
4662 target[i + c] = ((s * a + t * inv + 127) / 255) as u8;
4663 }
4664 }
4665}
4666
4667fn replace_cmyk_with_coverage_mask(target: &mut [f32], source: &[f32], coverage: &[u8]) {
4671 let pixel_count = target.len() / 4;
4672 for i in 0..pixel_count {
4673 let pi = i * 4;
4674 let cov_a = coverage[pi + 3];
4675 if cov_a == 0 {
4676 continue;
4677 }
4678 if cov_a == 255 {
4679 target[pi..pi + 4].copy_from_slice(&source[pi..pi + 4]);
4680 continue;
4681 }
4682 let a = cov_a as f32 / 255.0;
4683 let inv = 1.0 - a;
4684 for c in 0..4 {
4685 target[pi + c] = source[pi + c] * a + target[pi + c] * inv;
4686 }
4687 }
4688}
4689
4690fn replace_changed_pixels(target: &mut [u8], source: &[u8], backdrop: &[u8]) {
4694 for i in (0..target.len()).step_by(4) {
4695 if source[i] != backdrop[i]
4696 || source[i + 1] != backdrop[i + 1]
4697 || source[i + 2] != backdrop[i + 2]
4698 || source[i + 3] != backdrop[i + 3]
4699 {
4700 target[i..i + 4].copy_from_slice(&source[i..i + 4]);
4701 }
4702 }
4703}
4704
4705#[allow(clippy::too_many_arguments)]
4709fn copy_cmyk_buffer_to_parent(
4710 parent_cmyk: &mut [f32],
4711 group_cmyk: &[f32],
4712 group_pixels: &[u8],
4713 crop_x: usize,
4714 crop_y: usize,
4715 group_w: usize,
4716 group_h: usize,
4717 parent_w: usize,
4718 parent_h: usize,
4719) {
4720 let parent_stride = parent_w * 4;
4721 let group_stride = group_w * 4;
4722 for gy in 0..group_h {
4723 let py = crop_y + gy;
4724 if py >= parent_h {
4725 break;
4726 }
4727 for gx in 0..group_w {
4728 let px = crop_x + gx;
4729 if px >= parent_w {
4730 break;
4731 }
4732 let g_pixel_idx = (gy * group_w + gx) * 4;
4737 let g_cmyk_idx = gy * group_stride + gx * 4;
4738 if group_pixels[g_pixel_idx + 3] > 0
4739 && (group_cmyk[g_cmyk_idx] != 0.0
4740 || group_cmyk[g_cmyk_idx + 1] != 0.0
4741 || group_cmyk[g_cmyk_idx + 2] != 0.0
4742 || group_cmyk[g_cmyk_idx + 3] != 0.0)
4743 {
4744 let p_cmyk_idx = py * parent_stride + px * 4;
4745 parent_cmyk[p_cmyk_idx..p_cmyk_idx + 4]
4746 .copy_from_slice(&group_cmyk[g_cmyk_idx..g_cmyk_idx + 4]);
4747 }
4748 }
4749 }
4750}
4751
4752fn replace_changed_cmyk(
4755 target_cmyk: &mut [f32],
4756 source_cmyk: &[f32],
4757 source_pixels: &[u8],
4758 backdrop_pixels: &[u8],
4759) {
4760 let pixel_count = target_cmyk.len() / 4;
4761 for i in 0..pixel_count {
4762 let pi = i * 4;
4763 if source_pixels[pi] != backdrop_pixels[pi]
4764 || source_pixels[pi + 1] != backdrop_pixels[pi + 1]
4765 || source_pixels[pi + 2] != backdrop_pixels[pi + 2]
4766 || source_pixels[pi + 3] != backdrop_pixels[pi + 3]
4767 {
4768 target_cmyk[pi..pi + 4].copy_from_slice(&source_cmyk[pi..pi + 4]);
4769 }
4770 }
4771}
4772
4773#[allow(clippy::too_many_arguments)]
4781fn render_soft_masked(
4782 pixmap: &mut Pixmap,
4783 band_state: &mut BandState,
4784 mask_list: &DisplayList,
4785 content_list: &DisplayList,
4786 params: &stet_graphics::display_list::SoftMaskParams,
4787 mask_cache: &Arc<Mutex<Option<Option<stet_graphics::display_list::MaskRaster>>>>,
4788 ctx: &RenderContext<'_>,
4789) {
4790 let use_inline_mask = ctx.vp_x != 0.0;
4807 let bbox = ¶ms.bbox;
4808 let smask_px_x0 = ((bbox[0] as f32 - ctx.vp_x) * ctx.scale_x).floor() as i32;
4809 let smask_px_y0 = ((bbox[1] as f32 - ctx.vp_y) * ctx.scale_y).floor() as i32;
4810 let smask_px_x1 = ((bbox[2] as f32 - ctx.vp_x) * ctx.scale_x).ceil() as i32;
4811 let smask_px_y1 = ((bbox[3] as f32 - ctx.vp_y) * ctx.scale_y).ceil() as i32;
4812
4813 let crop_x = smask_px_x0.max(0);
4815 let crop_y = smask_px_y0.max(0);
4816 let crop_x1 = smask_px_x1.min(ctx.out_w as i32);
4817 let crop_y1 = smask_px_y1.min(ctx.out_h as i32);
4818 if crop_x >= crop_x1 || crop_y >= crop_y1 {
4819 return;
4820 }
4821 let eff_w = (crop_x1 - crop_x) as u32;
4822 let eff_h = (crop_y1 - crop_y) as u32;
4823
4824 let eff_vp_x = ctx.vp_x + crop_x as f32 / ctx.scale_x;
4829 let eff_vp_y = ctx.vp_y + crop_y as f32 / ctx.scale_y;
4830
4831 let sub_ctx = RenderContext {
4832 vp_x: eff_vp_x,
4833 vp_y: eff_vp_y,
4834 scale_x: ctx.scale_x,
4835 scale_y: ctx.scale_y,
4836 out_w: eff_w,
4837 out_h: eff_h,
4838 effective_dpi: ctx.effective_dpi,
4839 icc: ctx.icc,
4840 image_cache: None,
4841 preprocessed: None,
4842 elem_idx: 0,
4843 no_aa: ctx.no_aa,
4844 opm_zero_transparent: ctx.opm_zero_transparent,
4845 knockout_painter_pass: ctx.knockout_painter_pass,
4846 parent_group_isolated: ctx.parent_group_isolated,
4847 alpha_extraction_pass: false,
4851 layer_set: ctx.layer_set,
4852 };
4853
4854 let mut mask_values_inline: Vec<u8> = Vec::new();
4858 if use_inline_mask {
4859 let Some(mut mask_pixmap) = Pixmap::new(eff_w, eff_h) else {
4860 return;
4861 };
4862 let mut mask_band = BandState {
4863 clip_region: None,
4864 spare_mask: None,
4865 clip_mask_cache: HashMap::new(),
4866 clip_mask_seen: HashSet::new(),
4867 mask_pool: Vec::new(),
4868 cmyk_buffer: None,
4869 op_bg_snapshot: None,
4870 op_touched: None,
4871 spot_mask: None,
4872 };
4873 for (idx, elem) in mask_list.elements().iter().enumerate() {
4874 let elem_ctx = RenderContext {
4875 elem_idx: idx,
4876 ..sub_ctx
4877 };
4878 render_element(&mut mask_pixmap, &mut mask_band, elem, &elem_ctx);
4879 }
4880 if params.has_nested_mask_scope
4881 && params.subtype == stet_graphics::display_list::SoftMaskSubtype::Luminosity
4882 {
4883 let bc = params.backdrop_color.as_ref();
4884 let bd_r = bc.map_or(0u8, |c| (c[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
4885 let bd_g = bc.map_or(0u8, |c| (c[1].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
4886 let bd_b = bc.map_or(0u8, |c| (c[2].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
4887 for chunk in mask_pixmap.data_mut().chunks_exact_mut(4) {
4888 let a = chunk[3] as u16;
4889 if a == 255 {
4890 continue;
4891 }
4892 let inv_a = 255 - a;
4893 chunk[0] = ((chunk[0] as u16 * 255 + bd_r as u16 * inv_a + 127) / 255) as u8;
4894 chunk[1] = ((chunk[1] as u16 * 255 + bd_g as u16 * inv_a + 127) / 255) as u8;
4895 chunk[2] = ((chunk[2] as u16 * 255 + bd_b as u16 * inv_a + 127) / 255) as u8;
4896 chunk[3] = 255;
4897 }
4898 }
4899 mask_values_inline = vec![0u8; (eff_w * eff_h) as usize];
4900 extract_soft_mask_values(mask_pixmap.data(), &mut mask_values_inline, params);
4901 }
4902
4903 let raster_owned: Option<stet_graphics::display_list::MaskRaster> = if use_inline_mask {
4905 None
4906 } else {
4907 let mut guard = mask_cache.lock().unwrap();
4908 let needs_build = match guard.as_ref() {
4909 None => true,
4910 Some(None) => false, Some(Some(r)) => {
4912 (r.scale_x - ctx.scale_x).abs() > 1e-4 || (r.scale_y - ctx.scale_y).abs() > 1e-4
4913 }
4914 };
4915 if needs_build {
4916 let built = rasterize_mask(
4917 mask_list,
4918 params,
4919 ctx.icc,
4920 ctx.no_aa,
4921 ctx.effective_dpi,
4922 ctx.scale_x,
4923 ctx.scale_y,
4924 ctx.layer_set,
4925 );
4926 *guard = Some(built);
4927 }
4928 guard.as_ref().and_then(|inner| inner.clone())
4929 };
4930
4931 let fallback_mask = out_of_bounds_mask_value(params) as i32;
4935
4936 let Some(mut content_pixmap) = Pixmap::new(eff_w, eff_h) else {
4940 return;
4941 };
4942 let backdrop = copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h);
4943 content_pixmap.data_mut().copy_from_slice(&backdrop);
4944
4945 let content_cmyk = if has_overprint_elements(content_list) || band_state.cmyk_buffer.is_some() {
4946 let buf_size = eff_w as usize * eff_h as usize * 4;
4947 let mut buf = vec![0.0f32; buf_size];
4948 if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
4949 let parent_stride = ctx.out_w as usize * 4;
4950 let group_stride = eff_w as usize * 4;
4951 for gy in 0..eff_h as usize {
4952 let py = crop_y as usize + gy;
4953 if py < ctx.out_h as usize {
4954 let p_start = py * parent_stride + crop_x as usize * 4;
4955 let g_start = gy * group_stride;
4956 let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
4957 buf[g_start..g_start + copy_len]
4958 .copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
4959 }
4960 }
4961 }
4962 Some(buf)
4963 } else {
4964 None
4965 };
4966 let backdrop_cmyk: Option<Vec<f32>> = content_cmyk.clone();
4973 let mut content_band = BandState {
4974 clip_region: None,
4975 spare_mask: None,
4976 clip_mask_cache: HashMap::new(),
4977 clip_mask_seen: HashSet::new(),
4978 mask_pool: Vec::new(),
4979 cmyk_buffer: content_cmyk,
4980 op_bg_snapshot: None,
4981 op_touched: None,
4982 spot_mask: None,
4983 };
4984 for (idx, elem) in content_list.elements().iter().enumerate() {
4985 let elem_ctx = RenderContext {
4986 elem_idx: idx,
4987 ..sub_ctx
4988 };
4989 render_element(&mut content_pixmap, &mut content_band, elem, &elem_ctx);
4990 }
4991
4992 let vp_x_pixels = (ctx.vp_x * ctx.scale_x).round() as i32;
5012 let vp_y_pixels = (ctx.vp_y * ctx.scale_y).round() as i32;
5013
5014 let mut temp_mask = None;
5015 let clip_ref = resolve_clip_mask(
5016 &band_state.clip_region,
5017 &mut temp_mask,
5018 ctx.out_w,
5019 ctx.out_h,
5020 );
5021 let clip_ref = match clip_ref {
5022 None => return,
5023 Some(m) => m,
5024 };
5025
5026 let use_cmyk_blend = ctx.icc.is_some()
5042 && backdrop_cmyk.is_some()
5043 && content_band.cmyk_buffer.is_some()
5044 && content_list_is_simple_native_cmyk(content_list);
5045
5046 let content_data = content_pixmap.data();
5047 let parent_data = pixmap.data_mut();
5048 let parent_stride = ctx.out_w as usize * 4;
5049 let content_stride = eff_w as usize * 4;
5050
5051 for y in 0..eff_h as usize {
5052 let py = crop_y as usize + y;
5053 if py >= ctx.out_h as usize {
5054 break;
5055 }
5056 let ci_row = y * content_stride;
5057 let pi_row = py * parent_stride;
5058 let page_y = vp_y_pixels + crop_y + y as i32;
5059
5060 for x in 0..eff_w as usize {
5061 let px = crop_x as usize + x;
5062 if px >= ctx.out_w as usize {
5063 break;
5064 }
5065
5066 if let Some(clip) = clip_ref {
5068 if clip.data()[py * ctx.out_w as usize + px] == 0 {
5069 continue;
5070 }
5071 }
5072
5073 let m = if use_inline_mask {
5076 mask_values_inline[y * eff_w as usize + x] as i32
5077 } else if let Some(ref raster) = raster_owned {
5078 let page_x = vp_x_pixels + crop_x + x as i32;
5079 let mx = page_x - raster.origin_x;
5080 let my = page_y - raster.origin_y;
5081 if mx >= 0 && (mx as u32) < raster.width && my >= 0 && (my as u32) < raster.height {
5082 raster.data[my as usize * raster.width as usize + mx as usize] as i32
5083 } else {
5084 fallback_mask
5085 }
5086 } else {
5087 fallback_mask
5088 };
5089 if m == 0 {
5090 continue;
5091 }
5092
5093 let ci = ci_row + x * 4;
5094 let pi = pi_row + px * 4;
5095
5096 let ci_cmyk = (y * eff_w as usize + x) * 4;
5106 let cmyk_path_ok = use_cmyk_blend && {
5107 let bc_cmyk = &backdrop_cmyk.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5108 let cc_cmyk = &content_band.cmyk_buffer.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5109 let icc_match = |cmyk: &[f32], rgb: &[u8]| -> bool {
5110 let (r, g, b) = ctx
5111 .icc
5112 .and_then(|i| {
5113 i.convert_cmyk_readonly(
5114 cmyk[0] as f64,
5115 cmyk[1] as f64,
5116 cmyk[2] as f64,
5117 cmyk[3] as f64,
5118 )
5119 })
5120 .unwrap_or_else(|| {
5121 cmyk_to_rgb_plrm(
5122 cmyk[0] as f64,
5123 cmyk[1] as f64,
5124 cmyk[2] as f64,
5125 cmyk[3] as f64,
5126 )
5127 });
5128 let r = (r * 255.0).round() as i32;
5129 let g = (g * 255.0).round() as i32;
5130 let b = (b * 255.0).round() as i32;
5131 (r - rgb[0] as i32).abs() <= 3
5132 && (g - rgb[1] as i32).abs() <= 3
5133 && (b - rgb[2] as i32).abs() <= 3
5134 };
5135 icc_match(bc_cmyk, &backdrop[ci..ci + 3])
5136 && icc_match(cc_cmyk, &content_data[ci..ci + 3])
5137 };
5138
5139 if cmyk_path_ok {
5140 let bc_cmyk = &backdrop_cmyk.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5142 let cc_cmyk = &content_band.cmyk_buffer.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5143 let mf = m as f64 / 255.0;
5144 let rc = bc_cmyk[0] as f64 + mf * (cc_cmyk[0] as f64 - bc_cmyk[0] as f64);
5145 let rm = bc_cmyk[1] as f64 + mf * (cc_cmyk[1] as f64 - bc_cmyk[1] as f64);
5146 let ry = bc_cmyk[2] as f64 + mf * (cc_cmyk[2] as f64 - bc_cmyk[2] as f64);
5147 let rk = bc_cmyk[3] as f64 + mf * (cc_cmyk[3] as f64 - bc_cmyk[3] as f64);
5148 let (fr, fg, fb) = ctx
5149 .icc
5150 .and_then(|i| i.convert_cmyk_readonly(rc, rm, ry, rk))
5151 .unwrap_or_else(|| cmyk_to_rgb_plrm(rc, rm, ry, rk));
5152 parent_data[pi] = (fr * 255.0).round().clamp(0.0, 255.0) as u8;
5153 parent_data[pi + 1] = (fg * 255.0).round().clamp(0.0, 255.0) as u8;
5154 parent_data[pi + 2] = (fb * 255.0).round().clamp(0.0, 255.0) as u8;
5155 let content_a = content_data[ci + 3] as i32;
5157 let backdrop_a = backdrop[ci + 3] as i32;
5158 let delta = content_a - backdrop_a;
5159 if delta != 0 {
5160 let masked_delta = if delta > 0 {
5161 (delta * m + 128) / 255
5162 } else {
5163 (delta * m - 128) / 255
5164 };
5165 let result = (parent_data[pi + 3] as i32 + masked_delta).clamp(0, 255);
5166 parent_data[pi + 3] = result as u8;
5167 }
5168 } else {
5177 for c in 0..4 {
5178 let content_val = content_data[ci + c] as i32;
5179 let backdrop_val = backdrop[ci + c] as i32;
5180 let delta = content_val - backdrop_val;
5181 if delta != 0 {
5182 let masked_delta = if delta > 0 {
5183 (delta * m + 128) / 255
5184 } else {
5185 (delta * m - 128) / 255
5186 };
5187 let result = (parent_data[pi + c] as i32 + masked_delta).clamp(0, 255);
5188 parent_data[pi + c] = result as u8;
5189 }
5190 }
5191 }
5192 }
5193 }
5194
5195 if !use_cmyk_blend {
5199 if let (Some(content_cmyk), Some(parent_cmyk)) =
5200 (&content_band.cmyk_buffer, &mut band_state.cmyk_buffer)
5201 {
5202 copy_cmyk_buffer_to_parent(
5203 parent_cmyk,
5204 content_cmyk,
5205 content_pixmap.data(),
5206 crop_x as usize,
5207 crop_y as usize,
5208 eff_w as usize,
5209 eff_h as usize,
5210 ctx.out_w as usize,
5211 ctx.out_h as usize,
5212 );
5213 }
5214 }
5215}
5216fn extract_soft_mask_values(
5218 rgba: &[u8],
5219 out: &mut [u8],
5220 params: &stet_graphics::display_list::SoftMaskParams,
5221) {
5222 use stet_graphics::display_list::SoftMaskSubtype;
5223 let pixel_count = out.len();
5224
5225 match params.subtype {
5226 SoftMaskSubtype::Alpha => {
5227 for i in 0..pixel_count {
5228 let a = rgba[i * 4 + 3]; out[i] = if params.transfer_invert { 255 - a } else { a };
5230 }
5231 }
5232 SoftMaskSubtype::Luminosity => {
5233 let backdrop_lum = if let Some(bc) = ¶ms.backdrop_color {
5235 (0.2126 * bc[0] + 0.7152 * bc[1] + 0.0722 * bc[2]).clamp(0.0, 1.0)
5236 } else {
5237 0.0 };
5239 let backdrop_byte = (backdrop_lum * 255.0 + 0.5) as u8;
5240
5241 #[allow(clippy::needless_range_loop)]
5242 for i in 0..pixel_count {
5243 let off = i * 4;
5244 let a = rgba[off + 3];
5245 let lum_byte = if a == 0 {
5246 backdrop_byte
5247 } else if a < 255 {
5248 let af = a as f64;
5251 let bd = backdrop_lum * 255.0;
5252 let r = rgba[off] as f64 + bd * (255.0 - af) / 255.0;
5253 let g = rgba[off + 1] as f64 + bd * (255.0 - af) / 255.0;
5254 let b = rgba[off + 2] as f64 + bd * (255.0 - af) / 255.0;
5255 let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
5256 (lum + 0.5).clamp(0.0, 255.0) as u8
5257 } else {
5258 let lum = 0.2126 * rgba[off] as f64
5260 + 0.7152 * rgba[off + 1] as f64
5261 + 0.0722 * rgba[off + 2] as f64;
5262 (lum + 0.5).clamp(0.0, 255.0) as u8
5263 };
5264 out[i] = if params.transfer_invert {
5266 255 - lum_byte
5267 } else {
5268 lum_byte
5269 };
5270 }
5271 }
5272 }
5273}
5274
5275fn out_of_bounds_mask_value(params: &stet_graphics::display_list::SoftMaskParams) -> u8 {
5283 use stet_graphics::display_list::SoftMaskSubtype;
5284 let raw = match params.subtype {
5285 SoftMaskSubtype::Alpha => 0u8,
5286 SoftMaskSubtype::Luminosity => {
5287 let lum = if let Some(bc) = ¶ms.backdrop_color {
5288 (0.2126 * bc[0] + 0.7152 * bc[1] + 0.0722 * bc[2]).clamp(0.0, 1.0)
5289 } else {
5290 0.0
5291 };
5292 (lum * 255.0 + 0.5) as u8
5293 }
5294 };
5295 if params.transfer_invert {
5296 255 - raw
5297 } else {
5298 raw
5299 }
5300}
5301
5302const MAX_MASK_RASTER_PIXELS: u64 = 64 * 1024 * 1024;
5309
5310fn rasterize_mask(
5322 mask_list: &DisplayList,
5323 params: &stet_graphics::display_list::SoftMaskParams,
5324 icc: Option<&IccCache>,
5325 no_aa: bool,
5326 effective_dpi: f64,
5327 scale_x: f32,
5328 scale_y: f32,
5329 layer_set: &LayerSet,
5330) -> Option<stet_graphics::display_list::MaskRaster> {
5331 let mut bounds = compute_paint_bounds(mask_list, effective_dpi)?;
5338 if let Some(cap) = params.parent_clip_bbox {
5339 let cap_bbox = BBox2D {
5340 x_min: cap[0],
5341 y_min: cap[1],
5342 x_max: cap[2],
5343 y_max: cap[3],
5344 };
5345 bounds = intersect_bbox(&bounds, &cap_bbox)?;
5346 }
5347
5348 let px_x_min = (bounds.x_min as f32 * scale_x).floor() as i32 - 1;
5351 let px_y_min = (bounds.y_min as f32 * scale_y).floor() as i32 - 1;
5352 let px_x_max = (bounds.x_max as f32 * scale_x).ceil() as i32 + 1;
5353 let px_y_max = (bounds.y_max as f32 * scale_y).ceil() as i32 + 1;
5354 if px_x_min >= px_x_max || px_y_min >= px_y_max {
5355 return None;
5356 }
5357 let raster_w = (px_x_max - px_x_min) as u32;
5358 let raster_h = (px_y_max - px_y_min) as u32;
5359 if raster_w == 0 || raster_h == 0 {
5360 return None;
5361 }
5362 if (raster_w as u64) * (raster_h as u64) > MAX_MASK_RASTER_PIXELS {
5363 return None;
5364 }
5365
5366 let mut mask_pixmap = Pixmap::new(raster_w, raster_h)?;
5368
5369 let sub_ctx = RenderContext {
5373 vp_x: px_x_min as f32 / scale_x,
5374 vp_y: px_y_min as f32 / scale_y,
5375 scale_x,
5376 scale_y,
5377 out_w: raster_w,
5378 out_h: raster_h,
5379 effective_dpi,
5380 icc,
5381 image_cache: None,
5382 preprocessed: None,
5383 elem_idx: 0,
5384 no_aa,
5385 opm_zero_transparent: false,
5386 knockout_painter_pass: KnockoutPainterPass::None,
5387 parent_group_isolated: false,
5388 alpha_extraction_pass: false,
5389 layer_set,
5390 };
5391
5392 let mut mask_band = BandState {
5394 clip_region: None,
5395 spare_mask: None,
5396 clip_mask_cache: HashMap::new(),
5397 clip_mask_seen: HashSet::new(),
5398 mask_pool: Vec::new(),
5399 cmyk_buffer: None,
5400 op_bg_snapshot: None,
5401 op_touched: None,
5402 spot_mask: None,
5403 };
5404
5405 for (idx, elem) in mask_list.elements().iter().enumerate() {
5407 let elem_ctx = RenderContext {
5408 elem_idx: idx,
5409 ..sub_ctx
5410 };
5411 render_element(&mut mask_pixmap, &mut mask_band, elem, &elem_ctx);
5412 }
5413
5414 if params.has_nested_mask_scope
5422 && params.subtype == stet_graphics::display_list::SoftMaskSubtype::Luminosity
5423 {
5424 let bc = params.backdrop_color.as_ref();
5425 let bd_r = bc.map_or(0u8, |c| (c[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
5426 let bd_g = bc.map_or(0u8, |c| (c[1].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
5427 let bd_b = bc.map_or(0u8, |c| (c[2].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
5428 for chunk in mask_pixmap.data_mut().chunks_exact_mut(4) {
5429 let a = chunk[3] as u16;
5430 if a == 255 {
5431 continue;
5432 }
5433 let inv_a = 255 - a;
5434 chunk[0] = ((chunk[0] as u16 * 255 + bd_r as u16 * inv_a + 127) / 255) as u8;
5435 chunk[1] = ((chunk[1] as u16 * 255 + bd_g as u16 * inv_a + 127) / 255) as u8;
5436 chunk[2] = ((chunk[2] as u16 * 255 + bd_b as u16 * inv_a + 127) / 255) as u8;
5437 chunk[3] = 255;
5438 }
5439 }
5440
5441 let pixel_count = (raster_w * raster_h) as usize;
5443 let mut data = vec![0u8; pixel_count];
5444 extract_soft_mask_values(mask_pixmap.data(), &mut data, params);
5445
5446 Some(stet_graphics::display_list::MaskRaster {
5447 data,
5448 width: raster_w,
5449 height: raster_h,
5450 origin_x: px_x_min,
5451 origin_y: px_y_min,
5452 scale_x,
5453 scale_y,
5454 })
5455}
5456
5457fn transform_element_ctm(elem: &DisplayElement, pm: &Matrix) -> DisplayElement {
5461 match elem {
5462 DisplayElement::Fill { path, params } => {
5463 let mut p = params.clone();
5464 p.ctm = pm.concat(&p.ctm);
5465 DisplayElement::Fill {
5466 path: path.clone(),
5467 params: p,
5468 }
5469 }
5470 DisplayElement::Stroke { path, params } => {
5471 let mut p = params.clone();
5472 p.ctm = pm.concat(&p.ctm);
5473 DisplayElement::Stroke {
5474 path: path.clone(),
5475 params: p,
5476 }
5477 }
5478 DisplayElement::Clip { path, params } => {
5479 let mut p = params.clone();
5480 p.ctm = pm.concat(&p.ctm);
5481 if let Some(ref mut sp) = p.stroke_params {
5482 sp.ctm = pm.concat(&sp.ctm);
5483 }
5484 DisplayElement::Clip {
5485 path: path.clone(),
5486 params: p,
5487 }
5488 }
5489 DisplayElement::Image {
5490 sample_data,
5491 params,
5492 } => {
5493 let mut p = params.clone();
5494 p.ctm = pm.concat(&p.ctm);
5495 DisplayElement::Image {
5496 sample_data: sample_data.clone(),
5497 params: p,
5498 }
5499 }
5500 DisplayElement::MeshShading { params } => {
5501 let mut p = params.clone();
5502 p.ctm = pm.concat(&p.ctm);
5503 DisplayElement::MeshShading { params: p }
5504 }
5505 DisplayElement::PatchShading { params } => {
5506 let mut p = params.clone();
5507 p.ctm = pm.concat(&p.ctm);
5508 DisplayElement::PatchShading { params: p }
5509 }
5510 DisplayElement::AxialShading { params } => {
5511 let mut p = params.clone();
5512 p.ctm = pm.concat(&p.ctm);
5513 DisplayElement::AxialShading { params: p }
5514 }
5515 DisplayElement::RadialShading { params } => {
5516 let mut p = params.clone();
5517 p.ctm = pm.concat(&p.ctm);
5518 DisplayElement::RadialShading { params: p }
5519 }
5520 DisplayElement::Group { elements, params } => {
5521 let mut t = DisplayList::new();
5522 for child in elements.elements() {
5523 t.push(transform_element_ctm(child, pm));
5524 }
5525 let mut p = params.clone();
5526 let corners = [
5527 pm.transform_point(p.bbox[0], p.bbox[1]),
5528 pm.transform_point(p.bbox[2], p.bbox[1]),
5529 pm.transform_point(p.bbox[0], p.bbox[3]),
5530 pm.transform_point(p.bbox[2], p.bbox[3]),
5531 ];
5532 p.bbox = [
5533 corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min),
5534 corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min),
5535 corners
5536 .iter()
5537 .map(|c| c.0)
5538 .fold(f64::NEG_INFINITY, f64::max),
5539 corners
5540 .iter()
5541 .map(|c| c.1)
5542 .fold(f64::NEG_INFINITY, f64::max),
5543 ];
5544 DisplayElement::Group {
5545 elements: t,
5546 params: p,
5547 }
5548 }
5549 DisplayElement::SoftMasked {
5550 mask,
5551 content,
5552 params,
5553 ..
5554 } => {
5555 let mut t_mask = DisplayList::new();
5556 for child in mask.elements() {
5557 t_mask.push(transform_element_ctm(child, pm));
5558 }
5559 let mut t_content = DisplayList::new();
5560 for child in content.elements() {
5561 t_content.push(transform_element_ctm(child, pm));
5562 }
5563 let mut p = params.clone();
5564 let corners = [
5565 pm.transform_point(p.bbox[0], p.bbox[1]),
5566 pm.transform_point(p.bbox[2], p.bbox[1]),
5567 pm.transform_point(p.bbox[0], p.bbox[3]),
5568 pm.transform_point(p.bbox[2], p.bbox[3]),
5569 ];
5570 p.bbox = [
5571 corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min),
5572 corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min),
5573 corners
5574 .iter()
5575 .map(|c| c.0)
5576 .fold(f64::NEG_INFINITY, f64::max),
5577 corners
5578 .iter()
5579 .map(|c| c.1)
5580 .fold(f64::NEG_INFINITY, f64::max),
5581 ];
5582 if let Some(pcb) = p.parent_clip_bbox {
5589 let pcb_corners = [
5590 pm.transform_point(pcb[0], pcb[1]),
5591 pm.transform_point(pcb[2], pcb[1]),
5592 pm.transform_point(pcb[0], pcb[3]),
5593 pm.transform_point(pcb[2], pcb[3]),
5594 ];
5595 p.parent_clip_bbox = Some([
5596 pcb_corners
5597 .iter()
5598 .map(|c| c.0)
5599 .fold(f64::INFINITY, f64::min),
5600 pcb_corners
5601 .iter()
5602 .map(|c| c.1)
5603 .fold(f64::INFINITY, f64::min),
5604 pcb_corners
5605 .iter()
5606 .map(|c| c.0)
5607 .fold(f64::NEG_INFINITY, f64::max),
5608 pcb_corners
5609 .iter()
5610 .map(|c| c.1)
5611 .fold(f64::NEG_INFINITY, f64::max),
5612 ]);
5613 }
5614 DisplayElement::SoftMasked {
5618 mask: t_mask,
5619 content: t_content,
5620 params: p,
5621 mask_cache: Arc::new(Mutex::new(None)),
5622 }
5623 }
5624 DisplayElement::PatternFill { params } => {
5625 let mut p = params.clone();
5626 p.pattern_matrix = pm.concat(&p.pattern_matrix);
5627 p.path = transform_path_by_matrix(&p.path, pm);
5629 if let Some(ref mut sp) = p.stroke_params {
5630 sp.ctm = pm.concat(&sp.ctm);
5631 }
5632 DisplayElement::PatternFill { params: p }
5633 }
5634 DisplayElement::OcgGroup {
5635 elements,
5636 visibility,
5637 } => {
5638 let mut t = DisplayList::new();
5639 for child in elements.elements() {
5640 t.push(transform_element_ctm(child, pm));
5641 }
5642 DisplayElement::OcgGroup {
5643 elements: t,
5644 visibility: visibility.clone(),
5645 }
5646 }
5647 other => other.clone(),
5648 }
5649}
5650
5651fn transform_path_by_matrix(path: &PsPath, m: &Matrix) -> PsPath {
5653 use stet_fonts::geometry::PathSegment;
5654 let mut out = PsPath::new();
5655 for seg in &path.segments {
5656 out.segments.push(match *seg {
5657 PathSegment::MoveTo(x, y) => {
5658 let (nx, ny) = m.transform_point(x, y);
5659 PathSegment::MoveTo(nx, ny)
5660 }
5661 PathSegment::LineTo(x, y) => {
5662 let (nx, ny) = m.transform_point(x, y);
5663 PathSegment::LineTo(nx, ny)
5664 }
5665 PathSegment::CurveTo {
5666 x1,
5667 y1,
5668 x2,
5669 y2,
5670 x3,
5671 y3,
5672 } => {
5673 let (nx1, ny1) = m.transform_point(x1, y1);
5674 let (nx2, ny2) = m.transform_point(x2, y2);
5675 let (nx3, ny3) = m.transform_point(x3, y3);
5676 PathSegment::CurveTo {
5677 x1: nx1,
5678 y1: ny1,
5679 x2: nx2,
5680 y2: ny2,
5681 x3: nx3,
5682 y3: ny3,
5683 }
5684 }
5685 PathSegment::ClosePath => PathSegment::ClosePath,
5686 });
5687 }
5688 out
5689}
5690
5691fn bilinear_prescale(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
5698 let mut dst = vec![0u8; (dw * dh * 4) as usize];
5699 for dy in 0..dh {
5700 let sy_f = (dy as f64 + 0.5) * sh as f64 / dh as f64 - 0.5;
5701 let sy0 = sy_f.floor().max(0.0) as u32;
5702 let sy1 = (sy0 + 1).min(sh - 1);
5703 let fy = (sy_f - sy0 as f64) as f32;
5704 let ify = 1.0 - fy;
5705 for dx in 0..dw {
5706 let sx_f = (dx as f64 + 0.5) * sw as f64 / dw as f64 - 0.5;
5707 let sx0 = sx_f.floor().max(0.0) as u32;
5708 let sx1 = (sx0 + 1).min(sw - 1);
5709 let fx = (sx_f - sx0 as f64) as f32;
5710 let ifx = 1.0 - fx;
5711
5712 let i00 = (sy0 * sw + sx0) as usize * 4;
5713 let i10 = (sy0 * sw + sx1) as usize * 4;
5714 let i01 = (sy1 * sw + sx0) as usize * 4;
5715 let i11 = (sy1 * sw + sx1) as usize * 4;
5716 let di = (dy * dw + dx) as usize * 4;
5717 for c in 0..4 {
5718 dst[di + c] = (src[i00 + c] as f32 * ifx * ify
5719 + src[i10 + c] as f32 * fx * ify
5720 + src[i01 + c] as f32 * ifx * fy
5721 + src[i11 + c] as f32 * fx * fy)
5722 .round() as u8;
5723 }
5724 }
5725 }
5726 dst
5727}
5728
5729fn render_pattern_fill(
5730 pixmap: &mut Pixmap,
5731 band_state: &mut BandState,
5732 params: &stet_graphics::device::PatternFillParams,
5733 ctx: &RenderContext<'_>,
5734) {
5735 let mut temp_mask = None;
5736 let Some(mask_ref) = resolve_clip_mask(
5737 &band_state.clip_region,
5738 &mut temp_mask,
5739 ctx.out_w,
5740 ctx.out_h,
5741 ) else {
5742 return;
5743 };
5744
5745 let pm = ¶ms.pattern_matrix;
5746
5747 let (step_ux, step_uy) = pm.transform_delta(params.xstep, 0.0);
5749 let (step_vx, step_vy) = pm.transform_delta(0.0, params.ystep);
5750
5751 let step_u_len = (step_ux * step_ux + step_uy * step_uy).sqrt();
5752 let step_v_len = (step_vx * step_vx + step_vy * step_vy).sqrt();
5753 if step_u_len < 0.01 || step_v_len < 0.01 {
5754 return;
5755 }
5756
5757 let origin_x = pm.tx;
5758 let origin_y = pm.ty;
5759
5760 let dev_vp_x = ctx.vp_x as f64;
5762 let dev_vp_y = ctx.vp_y as f64;
5763 let dev_vp_w = ctx.out_w as f64 / ctx.scale_x as f64;
5764 let dev_vp_h = ctx.out_h as f64 / ctx.scale_y as f64;
5765
5766 let (mut min_x, mut min_y, mut max_x, mut max_y) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
5767 for seg in ¶ms.path.segments {
5768 let (x, y) = match seg {
5769 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => (*x, *y),
5770 PathSegment::CurveTo { x3, y3, .. } => (*x3, *y3),
5771 PathSegment::ClosePath => continue,
5772 };
5773 min_x = min_x.min(x);
5774 min_y = min_y.min(y);
5775 max_x = max_x.max(x);
5776 max_y = max_y.max(y);
5777 }
5778
5779 if let Some(ref sp) = params.stroke_params {
5783 let ctm = &sp.ctm;
5785 let corners = [
5786 ctm.transform_point(min_x, min_y),
5787 ctm.transform_point(max_x, min_y),
5788 ctm.transform_point(min_x, max_y),
5789 ctm.transform_point(max_x, max_y),
5790 ];
5791 min_x = f64::MAX;
5792 min_y = f64::MAX;
5793 max_x = f64::MIN;
5794 max_y = f64::MIN;
5795 for (cx, cy) in &corners {
5796 min_x = min_x.min(*cx);
5797 min_y = min_y.min(*cy);
5798 max_x = max_x.max(*cx);
5799 max_y = max_y.max(*cy);
5800 }
5801 let half_w = sp.line_width
5803 * 0.5
5804 * (ctm.a * ctm.a + ctm.b * ctm.b)
5805 .sqrt()
5806 .max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt());
5807 min_x -= half_w;
5808 min_y -= half_w;
5809 max_x += half_w;
5810 max_y += half_w;
5811 }
5812
5813 min_x = min_x.max(dev_vp_x);
5815 min_y = min_y.max(dev_vp_y);
5816 max_x = max_x.min(dev_vp_x + dev_vp_w);
5817 max_y = max_y.min(dev_vp_y + dev_vp_h);
5818 if min_x >= max_x || min_y >= max_y {
5819 return;
5820 }
5821
5822 let det = step_ux * step_vy - step_uy * step_vx;
5823 if det.abs() < 1e-10 {
5824 return;
5825 }
5826 let inv_det = 1.0 / det;
5827
5828 let mut tu_min = f64::MAX;
5829 let mut tu_max = f64::MIN;
5830 let mut tv_min = f64::MAX;
5831 let mut tv_max = f64::MIN;
5832 for &(cx, cy) in &[
5833 (min_x, min_y),
5834 (max_x, min_y),
5835 (min_x, max_y),
5836 (max_x, max_y),
5837 ] {
5838 let dx = cx - origin_x;
5839 let dy = cy - origin_y;
5840 let tu = (dx * step_vy - dy * step_vx) * inv_det;
5841 let tv = (-dx * step_uy + dy * step_ux) * inv_det;
5842 tu_min = tu_min.min(tu);
5843 tu_max = tu_max.max(tu);
5844 tv_min = tv_min.min(tv);
5845 tv_max = tv_max.max(tv);
5846 }
5847
5848 let tile_x_start = tu_min.floor() as i32 - 1;
5849 let tile_x_end = tu_max.ceil() as i32 + 1;
5850 let tile_y_start = tv_min.floor() as i32 - 1;
5851 let tile_y_end = tv_max.ceil() as i32 + 1;
5852
5853 let tile_count = (tile_x_end - tile_x_start) as i64 * (tile_y_end - tile_y_start) as i64;
5854 if tile_count > 10000 {
5855 return;
5856 }
5857
5858 let Some(mut tile_buf) = Pixmap::new(ctx.out_w, ctx.out_h) else {
5859 return;
5860 };
5861
5862 let sx_f = ctx.scale_x as f64;
5863 let sy_f = ctx.scale_y as f64;
5864
5865 if params.device_space_tile {
5866 for tv in tile_y_start..tile_y_end {
5872 for tu in tile_x_start..tile_x_end {
5873 let offset_x = tu as f64 * step_ux + tv as f64 * step_vx;
5874 let offset_y = tu as f64 * step_uy + tv as f64 * step_vy;
5875
5876 let tile_ctx = RenderContext {
5877 vp_x: ctx.vp_x - offset_x as f32,
5878 vp_y: ctx.vp_y - offset_y as f32,
5879 scale_x: ctx.scale_x,
5880 scale_y: ctx.scale_y,
5881 out_w: ctx.out_w,
5882 out_h: ctx.out_h,
5883 effective_dpi: ctx.effective_dpi,
5884 icc: ctx.icc,
5885 image_cache: None,
5886 preprocessed: None,
5887 elem_idx: 0,
5888 no_aa: ctx.no_aa,
5889 opm_zero_transparent: params.overprint_mode == 1,
5890 knockout_painter_pass: ctx.knockout_painter_pass,
5891 parent_group_isolated: ctx.parent_group_isolated,
5892 alpha_extraction_pass: ctx.alpha_extraction_pass,
5893 layer_set: ctx.layer_set,
5894 };
5895
5896 let mut tile_band = BandState {
5897 clip_region: None,
5898 spare_mask: None,
5899 clip_mask_cache: HashMap::new(),
5900 clip_mask_seen: HashSet::new(),
5901 mask_pool: Vec::new(),
5902 cmyk_buffer: None,
5903 op_bg_snapshot: None,
5904 op_touched: None,
5905 spot_mask: None,
5906 };
5907
5908 for (idx, elem) in params.tile.elements().iter().enumerate() {
5909 let elem_ctx = RenderContext {
5910 elem_idx: idx,
5911 ..tile_ctx
5912 };
5913 render_element(&mut tile_buf, &mut tile_band, elem, &elem_ctx);
5914 }
5915 }
5916 }
5917 } else if params.tile.elements().iter().any(|e| {
5918 !matches!(
5919 e,
5920 DisplayElement::Fill { .. }
5921 | DisplayElement::Stroke { .. }
5922 | DisplayElement::Image { .. }
5923 | DisplayElement::Clip { .. }
5924 | DisplayElement::InitClip
5925 )
5926 }) {
5927 let bbox = ¶ms.bbox;
5931 let corners_dev = [
5932 pm.transform_point(bbox[0], bbox[1]),
5933 pm.transform_point(bbox[2], bbox[1]),
5934 pm.transform_point(bbox[0], bbox[3]),
5935 pm.transform_point(bbox[2], bbox[3]),
5936 ];
5937 let (mut td_x0, mut td_y0) = (f64::MAX, f64::MAX);
5938 let (mut td_x1, mut td_y1) = (f64::MIN, f64::MIN);
5939 for (x, y) in &corners_dev {
5940 td_x0 = td_x0.min(*x);
5941 td_y0 = td_y0.min(*y);
5942 td_x1 = td_x1.max(*x);
5943 td_y1 = td_y1.max(*y);
5944 }
5945 let tile_pw = ((td_x1 - td_x0) * sx_f).ceil().max(1.0) as u32;
5946 let tile_ph = ((td_y1 - td_y0) * sy_f).ceil().max(1.0) as u32;
5947 let tile_pw = tile_pw.min(8192);
5948 let tile_ph = tile_ph.min(8192);
5949
5950 if let Some(mut one_tile) = Pixmap::new(tile_pw, tile_ph) {
5951 let tile_render_ctx = RenderContext {
5952 vp_x: td_x0 as f32,
5953 vp_y: td_y0 as f32,
5954 scale_x: ctx.scale_x,
5955 scale_y: ctx.scale_y,
5956 out_w: tile_pw,
5957 out_h: tile_ph,
5958 effective_dpi: ctx.effective_dpi,
5959 icc: ctx.icc,
5960 image_cache: None,
5961 preprocessed: None,
5962 elem_idx: 0,
5963 no_aa: ctx.no_aa,
5964 opm_zero_transparent: params.overprint_mode == 1,
5965 knockout_painter_pass: ctx.knockout_painter_pass,
5966 parent_group_isolated: ctx.parent_group_isolated,
5967 alpha_extraction_pass: ctx.alpha_extraction_pass,
5968 layer_set: ctx.layer_set,
5969 };
5970 let mut tile_bs = BandState {
5971 clip_region: None,
5972 spare_mask: None,
5973 clip_mask_cache: HashMap::new(),
5974 clip_mask_seen: HashSet::new(),
5975 mask_pool: Vec::new(),
5976 cmyk_buffer: None,
5977 op_bg_snapshot: None,
5978 op_touched: None,
5979 spot_mask: None,
5980 };
5981 for (idx, elem) in params.tile.elements().iter().enumerate() {
5982 let transformed = transform_element_ctm(elem, pm);
5983 let elem_ctx = RenderContext {
5984 elem_idx: idx,
5985 ..tile_render_ctx
5986 };
5987 render_element(&mut one_tile, &mut tile_bs, &transformed, &elem_ctx);
5988 }
5989 for tv in tile_y_start..tile_y_end {
5991 for tu in tile_x_start..tile_x_end {
5992 let offset_x = tu as f64 * step_ux + tv as f64 * step_vx;
5993 let offset_y = tu as f64 * step_uy + tv as f64 * step_vy;
5994 let px = ((td_x0 + offset_x - dev_vp_x) * sx_f) as i32;
5995 let py = ((td_y0 + offset_y - dev_vp_y) * sy_f) as i32;
5996 let paint = stet_tiny_skia::PixmapPaint {
5997 opacity: 1.0,
5998 blend_mode: BlendMode::SourceOver,
5999 quality: stet_tiny_skia::FilterQuality::Nearest,
6000 };
6001 tile_buf.draw_pixmap(
6002 px,
6003 py,
6004 one_tile.as_ref(),
6005 &paint,
6006 Transform::identity(),
6007 None,
6008 );
6009 }
6010 }
6011 }
6012 } else {
6013 struct PreprocessedImage {
6021 rgba: Vec<u8>,
6022 width: u32,
6023 height: u32,
6024 img_transform: Transform,
6027 }
6028 let tile_elements = params.tile.elements();
6029 let mut preprocessed: Vec<Option<PreprocessedImage>> =
6030 Vec::with_capacity(tile_elements.len());
6031 let tt_sx = (pm.a * sx_f) as f32;
6033 let tt_sy = (pm.d * sy_f) as f32;
6034 let tt_kx = (pm.c * sx_f) as f32;
6035 let tt_ky = (pm.b * sy_f) as f32;
6036 for elem in tile_elements {
6037 if let DisplayElement::Image {
6038 sample_data,
6039 params: ip,
6040 } = elem
6041 {
6042 let iw = ip.width;
6043 let ih = ip.height;
6044 if iw > 0 && ih > 0 {
6045 let mut rgba =
6046 samples_to_rgba(sample_data, ip, ctx.icc, ctx.opm_zero_transparent);
6047 if ip.mask_color.is_some() {
6048 apply_mask_color_rgba(&mut rgba, sample_data, ip);
6049 }
6050 let expected = (iw * ih * 4) as usize;
6051 if rgba.len() >= expected {
6052 if let Some(inv) = ip.image_matrix.invert() {
6053 let combined_mat = ip.ctm.concat(&inv);
6054 let t = to_transform(&combined_mat);
6055 let test = t.post_concat(Transform::from_row(
6058 tt_sx, tt_ky, tt_kx, tt_sy, 0.0, 0.0,
6059 ));
6060 let eff_sx = (test.sx * test.sx + test.ky * test.ky).sqrt();
6061 let eff_sy = (test.kx * test.kx + test.sy * test.sy).sqrt();
6062 if eff_sx < 0.99 || eff_sy < 0.99 {
6063 let tw = (iw as f32 * eff_sx).floor().max(1.0) as u32;
6067 let th = (ih as f32 * eff_sy).floor().max(1.0) as u32;
6068 let scaled = bilinear_prescale(&rgba, iw, ih, tw, th);
6069 let adj = Transform::from_scale(
6072 iw as f32 / tw as f32,
6073 ih as f32 / th as f32,
6074 );
6075 preprocessed.push(Some(PreprocessedImage {
6076 rgba: scaled,
6077 width: tw,
6078 height: th,
6079 img_transform: t.pre_concat(adj),
6080 }));
6081 } else {
6082 preprocessed.push(Some(PreprocessedImage {
6083 rgba,
6084 width: iw,
6085 height: ih,
6086 img_transform: t,
6087 }));
6088 }
6089 } else {
6090 preprocessed.push(None);
6091 }
6092 } else {
6093 preprocessed.push(None);
6094 }
6095 } else {
6096 preprocessed.push(None);
6097 }
6098 }
6101 }
6102
6103 for tv in tile_y_start..tile_y_end {
6104 for tu in tile_x_start..tile_x_end {
6105 let pat_offset_x = tu as f64 * params.xstep;
6106 let pat_offset_y = tv as f64 * params.ystep;
6107
6108 let tile_transform = Transform::from_row(
6109 tt_sx,
6110 tt_ky,
6111 tt_kx,
6112 tt_sy,
6113 ((pm.a * pat_offset_x + pm.c * pat_offset_y + pm.tx - dev_vp_x) * sx_f) as f32,
6114 ((pm.b * pat_offset_x + pm.d * pat_offset_y + pm.ty - dev_vp_y) * sy_f) as f32,
6115 );
6116
6117 let bbox_clip = {
6119 let bb = ¶ms.bbox;
6120 let mut bp = stet_tiny_skia::PathBuilder::new();
6121 bp.move_to(bb[0] as f32, bb[1] as f32);
6122 bp.line_to(bb[2] as f32, bb[1] as f32);
6123 bp.line_to(bb[2] as f32, bb[3] as f32);
6124 bp.line_to(bb[0] as f32, bb[3] as f32);
6125 bp.close();
6126 bp.finish().and_then(|sp| {
6127 let mut m = Mask::new(ctx.out_w, ctx.out_h)?;
6128 m.fill_path(
6129 &sp,
6130 stet_tiny_skia::FillRule::Winding,
6131 false,
6132 tile_transform,
6133 );
6134 Some(m)
6135 })
6136 };
6137 let mut tile_clip: Option<Mask> = bbox_clip;
6138 let mut img_idx = 0usize;
6139 for elem in tile_elements {
6140 let clip_ref = tile_clip.as_ref();
6141 match elem {
6142 DisplayElement::Clip { path, params: cp } => {
6143 if let Some(sp) = build_skia_path(path) {
6144 let t = to_transform(&cp.ctm);
6145 let combined = t.post_concat(tile_transform);
6146 let mut mask = Mask::new(ctx.out_w, ctx.out_h).expect("mask");
6147 mask.fill_path(&sp, to_fill_rule(&cp.fill_rule), false, combined);
6148 if let Some(prev) = tile_clip.take() {
6149 intersect_masks(&mut mask, &prev);
6150 }
6151 tile_clip = Some(mask);
6152 }
6153 }
6154 DisplayElement::InitClip => {
6155 tile_clip = None;
6156 }
6157 DisplayElement::Fill { path, params: fp } => {
6158 if let Some(sp) = build_skia_path(path) {
6159 let mut paint = if params.paint_type == 1 {
6160 to_paint(&fp.color)
6161 } else {
6162 to_paint(
6163 params
6164 .underlying_color
6165 .as_ref()
6166 .unwrap_or(&DeviceColor::black()),
6167 )
6168 };
6169 paint.anti_alias = false;
6170 let t = to_transform(&fp.ctm);
6171 let combined = t.post_concat(tile_transform);
6172 let fr = to_fill_rule(&fp.fill_rule);
6173 tile_buf.fill_path(&sp, &paint, fr, combined, clip_ref);
6174 }
6175 }
6176 DisplayElement::Stroke { path, params: sp } => {
6177 if let Some(skp) = build_skia_path(path) {
6178 let effective_ctm = pm.concat(&sp.ctm);
6182 let mut sp_adj = sp.clone();
6183 sp_adj.ctm = effective_ctm;
6184 let stroke = build_stroke(&sp_adj, ctx.effective_dpi);
6185 let paint = if params.paint_type == 1 {
6186 to_paint(&sp.color)
6187 } else {
6188 to_paint(
6189 params
6190 .underlying_color
6191 .as_ref()
6192 .unwrap_or(&DeviceColor::black()),
6193 )
6194 };
6195 let t = to_transform(&sp.ctm);
6196 let combined = t.post_concat(tile_transform);
6197 tile_buf.stroke_path(&skp, &paint, &stroke, combined, clip_ref);
6198 }
6199 }
6200 DisplayElement::Image { .. } => {
6201 if let Some(ref pi) = preprocessed[img_idx] {
6202 let combined = pi.img_transform.post_concat(tile_transform);
6203 if let Some(img_ref) = stet_tiny_skia::PixmapRef::from_bytes(
6204 &pi.rgba, pi.width, pi.height,
6205 ) {
6206 let paint = stet_tiny_skia::PixmapPaint {
6207 opacity: 1.0,
6208 blend_mode: BlendMode::SourceOver,
6209 quality: stet_tiny_skia::FilterQuality::Nearest,
6210 };
6211 tile_buf.draw_pixmap(0, 0, img_ref, &paint, combined, clip_ref);
6212 }
6213 }
6214 img_idx += 1;
6215 }
6216 _ => {}
6217 }
6218 }
6219 }
6220 }
6221 }
6222
6223 let Some(fill_skia_path) = build_skia_path(¶ms.path) else {
6225 return;
6226 };
6227 let fill_rule = to_fill_rule(¶ms.fill_rule);
6228 let mut fill_mask = Mask::new(ctx.out_w, ctx.out_h).expect("mask");
6229 let path_transform = viewport_transform(
6230 Transform::identity(),
6231 ctx.vp_x,
6232 ctx.vp_y,
6233 ctx.scale_x,
6234 ctx.scale_y,
6235 );
6236 if let Some(ref sp) = params.stroke_params {
6237 let stroke = build_stroke(sp, ctx.effective_dpi);
6241 let ctm_transform = to_transform(&sp.ctm);
6242 let combined = ctm_transform.post_concat(path_transform);
6243 let res_scale = stet_tiny_skia::PathStroker::compute_resolution_scale(&combined);
6244 let dashed;
6245 let stroke_path = if let Some(ref dash) = stroke.dash {
6246 dashed = fill_skia_path.dash(dash, res_scale);
6247 match dashed.as_ref() {
6248 Some(p) => p,
6249 None => &fill_skia_path,
6250 }
6251 } else {
6252 &fill_skia_path
6253 };
6254 if let Some(outline) = stroke_path.stroke(&stroke, res_scale) {
6255 fill_mask.fill_path(
6256 &outline,
6257 stet_tiny_skia::FillRule::Winding,
6258 !ctx.no_aa,
6259 combined,
6260 );
6261 }
6262 } else {
6263 fill_mask.fill_path(&fill_skia_path, fill_rule, !ctx.no_aa, path_transform);
6264 }
6265
6266 if let Some(clip_mask) = mask_ref {
6267 intersect_masks(&mut fill_mask, clip_mask);
6268 }
6269
6270 let img_paint = stet_tiny_skia::PixmapPaint::default();
6271 pixmap.draw_pixmap(
6272 0,
6273 0,
6274 tile_buf.as_ref(),
6275 &img_paint,
6276 Transform::identity(),
6277 Some(&fill_mask),
6278 );
6279}
6280
6281fn clip_path_unified(
6286 band_state: &mut BandState,
6287 path: &PsPath,
6288 params: &ClipParams,
6289 ctx: &RenderContext<'_>,
6290) {
6291 let is_unit_scale = ctx.scale_x == 1.0 && ctx.scale_y == 1.0;
6292
6293 if is_unit_scale {
6295 let y_start = ctx.vp_y as u32;
6296 let x_start = ctx.vp_x as u32;
6297
6298 if x_start == 0
6303 && params.stroke_params.is_none()
6304 && params.ctm.a == 1.0
6305 && params.ctm.d == 1.0
6306 && params.ctm.tx == 0.0
6307 && params.ctm.ty == 0.0
6308 && let Some(bbox) = path_y_bbox(path)
6309 && (bbox.y_max <= y_start as f64 || bbox.y_min >= (y_start + ctx.out_h) as f64)
6310 {
6311 if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
6312 band_state.recycle_mask(mask);
6313 }
6314 band_state.clip_region = Some(ClipRegion::Rect(ClipRect {
6315 x0: 0,
6316 y0: 0,
6317 x1: 0,
6318 y1: 0,
6319 }));
6320 return;
6321 }
6322
6323 let ctm_is_identity = params.ctm.a == 1.0
6327 && params.ctm.b == 0.0
6328 && params.ctm.c == 0.0
6329 && params.ctm.d == 1.0
6330 && params.ctm.tx == 0.0
6331 && params.ctm.ty == 0.0;
6332 if x_start == 0
6333 && ctm_is_identity
6334 && params.stroke_params.is_none()
6335 && let Some(dev_rect) = detect_rect(path, ctx.out_w, u32::MAX)
6336 {
6337 let new_rect = translate_clip_rect(&dev_rect, y_start, ctx.out_h);
6338 match band_state.clip_region.take() {
6339 None => {
6340 band_state.clip_region = Some(ClipRegion::Rect(new_rect));
6341 }
6342 Some(ClipRegion::Rect(existing)) => {
6343 band_state.clip_region = Some(ClipRegion::Rect(existing.intersect(&new_rect)));
6344 }
6345 Some(ClipRegion::Mask(mut mask)) => {
6346 intersect_mask_with_rect(&mut mask, &new_rect, ctx.out_w, ctx.out_h);
6347 band_state.clip_region = Some(ClipRegion::Mask(mask));
6348 }
6349 }
6350 return;
6351 }
6352 }
6353
6354 let fill_rule = to_fill_rule(¶ms.fill_rule);
6356 let path_hash = hash_clip_path(path, ¶ms.fill_rule);
6357 let prev_region = band_state.clip_region.take();
6358
6359 let mut mask = band_state.take_mask(ctx.out_w, ctx.out_h);
6360
6361 let path_mask = if let Some(cached) = band_state.clip_mask_cache.get(&path_hash) {
6362 mask.data_mut().copy_from_slice(cached.data());
6363 mask
6364 } else {
6365 let Some(skia_path) = build_skia_path(path) else {
6366 band_state.recycle_mask(mask);
6367 band_state.clip_region = prev_region;
6368 return;
6369 };
6370 mask.data_mut().fill(0);
6371 if let Some(ref sp) = params.stroke_params {
6372 let stroke = build_stroke(sp, ctx.effective_dpi);
6375 let transform = ctx.transform(&sp.ctm);
6376 let res_scale = stet_tiny_skia::PathStroker::compute_resolution_scale(&transform);
6377 let dashed;
6378 let stroke_path = if let Some(ref dash) = stroke.dash {
6379 dashed = skia_path.dash(dash, res_scale);
6380 match dashed.as_ref() {
6381 Some(p) => p,
6382 None => &skia_path,
6383 }
6384 } else {
6385 &skia_path
6386 };
6387 if let Some(outline) = stroke_path.stroke(&stroke, res_scale) {
6388 mask.fill_path(
6389 &outline,
6390 stet_tiny_skia::FillRule::Winding,
6391 false,
6392 transform,
6393 );
6394 }
6395 } else {
6396 let transform = ctx.transform(¶ms.ctm);
6397 mask.fill_path(&skia_path, fill_rule, false, transform);
6398 }
6399 if !band_state.clip_mask_seen.insert(path_hash) {
6400 band_state.clip_mask_cache.insert(path_hash, mask.clone());
6401 }
6402 mask
6403 };
6404
6405 match prev_region {
6406 None => {
6407 band_state.clip_region = Some(ClipRegion::Mask(path_mask));
6408 }
6409 Some(ClipRegion::Rect(rect)) => {
6410 if rect.is_empty() {
6411 band_state.recycle_mask(path_mask);
6412 band_state.clip_region = Some(ClipRegion::Rect(rect));
6415 } else {
6416 let mut mask = path_mask;
6417 intersect_mask_with_rect(&mut mask, &rect, ctx.out_w, ctx.out_h);
6418 band_state.clip_region = Some(ClipRegion::Mask(mask));
6419 }
6420 }
6421 Some(ClipRegion::Mask(mut existing)) => {
6422 intersect_masks(&mut existing, &path_mask);
6423 band_state.recycle_mask(path_mask);
6424 band_state.clip_region = Some(ClipRegion::Mask(existing));
6425 }
6426 }
6427}
6428impl OutputDevice for SkiaDevice {
6429 fn fill_path(&mut self, path: &PsPath, params: &FillParams) {
6430 self.ensure_full_pixmap();
6431 let Some(skia_path) = build_skia_path(path) else {
6432 return;
6433 };
6434 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6435 let mut temp_mask = None;
6436 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6437 return; };
6439
6440 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, self.no_aa);
6441 let transform = to_transform(¶ms.ctm);
6442 let fill_rule = to_fill_rule(¶ms.fill_rule);
6443
6444 self.pixmap
6445 .fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
6446 }
6447
6448 fn stroke_path(&mut self, path: &PsPath, params: &StrokeParams) {
6449 self.ensure_full_pixmap();
6450 let stroke = build_stroke(params, self.dpi);
6451 let adjusted;
6452 let draw_path =
6453 if params.stroke_adjust && stroke.width <= 2.0 && ctm_is_device_space(¶ms.ctm) {
6454 adjusted =
6455 stroke_adjust_path_viewport(path, stroke.width as f64, 1.0, 1.0, 0.0, 0.0);
6456 &adjusted
6457 } else {
6458 path
6459 };
6460 let Some(skia_path) = build_skia_path(draw_path) else {
6461 return;
6462 };
6463 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, self.no_aa);
6464 let transform = to_transform(¶ms.ctm);
6465
6466 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6467 let mut temp_mask = None;
6468 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6469 return; };
6471
6472 self.pixmap
6473 .stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
6474 }
6475
6476 fn clip_path(&mut self, path: &PsPath, params: &ClipParams) {
6477 self.ensure_full_pixmap();
6478 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6479
6480 if let Some(new_rect) = detect_rect(path, w, h) {
6482 match self.clip_region.take() {
6483 None => {
6484 self.clip_region = Some(ClipRegion::Rect(new_rect));
6485 }
6486 Some(ClipRegion::Rect(existing)) => {
6487 self.clip_region = Some(ClipRegion::Rect(existing.intersect(&new_rect)));
6489 }
6490 Some(ClipRegion::Mask(mut mask)) => {
6491 intersect_mask_with_rect(&mut mask, &new_rect, w, h);
6493 self.clip_region = Some(ClipRegion::Mask(mask));
6494 }
6495 }
6496 return;
6497 }
6498
6499 let fill_rule = to_fill_rule(¶ms.fill_rule);
6501 let path_hash = hash_clip_path(path, ¶ms.fill_rule);
6502 let prev_region = self.clip_region.take();
6503
6504 macro_rules! take_spare {
6506 ($self:expr, $w:expr, $h:expr) => {
6507 $self
6508 .spare_mask
6509 .take()
6510 .unwrap_or_else(|| Mask::new($w, $h).expect("Failed to create mask"))
6511 };
6512 }
6513
6514 let path_mask = if let Some(cached) = self.clip_mask_cache.get(&path_hash) {
6516 let mut mask = take_spare!(self, w, h);
6518 mask.data_mut().copy_from_slice(cached.data());
6519 mask
6520 } else {
6521 let Some(skia_path) = build_skia_path(path) else {
6522 self.clip_region = prev_region;
6523 return;
6524 };
6525 let transform = to_transform(¶ms.ctm);
6526 let mut mask = take_spare!(self, w, h);
6527 mask.data_mut().fill(0); mask.fill_path(&skia_path, fill_rule, false, transform);
6529 if !self.clip_mask_seen.insert(path_hash) {
6531 self.clip_mask_cache.insert(path_hash, mask.clone());
6533 }
6534 mask
6535 };
6536
6537 match prev_region {
6538 None => {
6539 self.clip_region = Some(ClipRegion::Mask(path_mask));
6540 }
6541 Some(ClipRegion::Rect(rect)) => {
6542 if rect.is_empty() {
6543 self.spare_mask = Some(path_mask); } else {
6545 let mut mask = path_mask;
6546 intersect_mask_with_rect(&mut mask, &rect, w, h);
6547 self.clip_region = Some(ClipRegion::Mask(mask));
6548 }
6549 }
6550 Some(ClipRegion::Mask(mut existing)) => {
6551 intersect_masks(&mut existing, &path_mask);
6552 self.spare_mask = Some(path_mask); self.clip_region = Some(ClipRegion::Mask(existing));
6554 }
6555 }
6556 }
6557
6558 fn init_clip(&mut self) {
6559 if let Some(ClipRegion::Mask(mask)) = self.clip_region.take() {
6560 self.spare_mask = Some(mask);
6561 }
6562 self.clip_region = None;
6563 }
6564
6565 fn erase_page(&mut self) {
6566 self.pixmap.fill(Color::WHITE);
6569 if let Some(ClipRegion::Mask(mask)) = self.clip_region.take() {
6570 self.spare_mask = Some(mask);
6571 }
6572 self.clip_region = None;
6573 }
6574
6575 fn show_page(&mut self, output_path: &str) -> Result<(), String> {
6576 let w = self.pixmap.width();
6577 let h = self.pixmap.height();
6578 composite_onto_white(self.pixmap.data_mut());
6580 let mut sink = self.sink_factory.create_sink(output_path)?;
6581 sink.begin_page(w, h)?;
6582 sink.write_rows(self.pixmap.data(), h)?;
6583 sink.end_page()
6584 }
6585
6586 fn draw_image(&mut self, sample_data: &[u8], params: &ImageParams) {
6587 self.ensure_full_pixmap();
6588 let w = params.width;
6589 let h = params.height;
6590 if w == 0 || h == 0 {
6591 return;
6592 }
6593 let mut rgba_data =
6594 samples_to_rgba(sample_data, params, self.render_icc_cache.as_ref(), false);
6595 if params.mask_color.is_some() {
6596 apply_mask_color_rgba(&mut rgba_data, sample_data, params);
6597 }
6598 let expected = (w * h * 4) as usize;
6599 if rgba_data.len() < expected {
6600 return;
6601 }
6602
6603 let Some(image_inv) = params.image_matrix.invert() else {
6604 return;
6605 };
6606 let combined = params.ctm.concat(&image_inv);
6607 let raw_transform = enforce_min_image_size(to_transform(&combined), w, h);
6608
6609 let prescaled = prescale_image(&rgba_data, w, h, raw_transform, params.interpolate);
6610 let (img_data, img_w, img_h, transform) = match &prescaled {
6611 Some((data, pw, ph, t)) => (data.as_slice(), *pw, *ph, *t),
6612 None => (rgba_data.as_slice(), w, h, raw_transform),
6613 };
6614
6615 let Some(img_pixmap) = stet_tiny_skia::PixmapRef::from_bytes(img_data, img_w, img_h) else {
6616 return;
6617 };
6618
6619 let (pw, ph) = (self.pixmap.width(), self.pixmap.height());
6620 let mut temp_mask = None;
6621 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, pw, ph) else {
6622 return;
6623 };
6624
6625 let paint = stet_tiny_skia::PixmapPaint {
6626 quality: image_filter_quality(transform, params.interpolate),
6627 opacity: params.alpha as f32,
6628 blend_mode: u8_to_blend_mode(params.blend_mode),
6629 };
6630 self.pixmap
6631 .draw_pixmap(0, 0, img_pixmap, &paint, transform, mask_ref);
6632 }
6633
6634 fn paint_axial_shading(&mut self, params: &AxialShadingParams) {
6635 self.ensure_full_pixmap();
6636 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6637 let mut temp_mask = None;
6638 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6639 return;
6640 };
6641 render_axial_shading(
6642 &mut self.pixmap,
6643 params,
6644 0.0,
6645 0.0,
6646 1.0,
6647 1.0,
6648 mask_ref,
6649 self.no_aa,
6650 None,
6651 None,
6652 );
6653 }
6654
6655 fn paint_radial_shading(&mut self, params: &RadialShadingParams) {
6656 self.ensure_full_pixmap();
6657 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6658 let mut temp_mask = None;
6659 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6660 return;
6661 };
6662 render_radial_shading(
6663 &mut self.pixmap,
6664 params,
6665 0.0,
6666 0.0,
6667 1.0,
6668 1.0,
6669 mask_ref,
6670 self.no_aa,
6671 None,
6672 None,
6673 );
6674 }
6675
6676 fn paint_mesh_shading(&mut self, params: &MeshShadingParams) {
6677 self.ensure_full_pixmap();
6678 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6679 let mut temp_mask = None;
6680 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6681 return;
6682 };
6683 render_mesh_shading(
6684 &mut self.pixmap,
6685 params,
6686 0.0,
6687 0.0,
6688 1.0,
6689 1.0,
6690 mask_ref,
6691 None,
6692 None,
6693 );
6694 }
6695
6696 fn paint_patch_shading(&mut self, params: &PatchShadingParams) {
6697 self.ensure_full_pixmap();
6698 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6699 let mut temp_mask = None;
6700 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6701 return;
6702 };
6703 render_patch_shading(
6704 &mut self.pixmap,
6705 params,
6706 0.0,
6707 0.0,
6708 1.0,
6709 1.0,
6710 mask_ref,
6711 None,
6712 None,
6713 );
6714 }
6715
6716 fn paint_pattern_fill(&mut self, params: &stet_graphics::device::PatternFillParams) {
6717 self.ensure_full_pixmap();
6718 let w = self.pixmap.width();
6719 let h = self.pixmap.height();
6720 let mut band_state = BandState {
6721 clip_region: self.clip_region.take(),
6722 spare_mask: self.spare_mask.take(),
6723 clip_mask_cache: HashMap::new(),
6724 clip_mask_seen: HashSet::new(),
6725 mask_pool: Vec::new(),
6726 cmyk_buffer: None,
6727 op_bg_snapshot: None,
6728 op_touched: None,
6729 spot_mask: None,
6730 };
6731 {
6732 let ctx = RenderContext {
6733 vp_x: 0.0,
6734 vp_y: 0.0,
6735 scale_x: 1.0,
6736 scale_y: 1.0,
6737 out_w: w,
6738 out_h: h,
6739 effective_dpi: self.dpi,
6740 icc: None,
6741 image_cache: None,
6742 preprocessed: None,
6743 elem_idx: 0,
6744 no_aa: self.no_aa,
6745 opm_zero_transparent: false,
6746 knockout_painter_pass: KnockoutPainterPass::None,
6747 parent_group_isolated: false,
6748 alpha_extraction_pass: false,
6749 layer_set: &self.layer_set,
6750 };
6751 render_pattern_fill(&mut self.pixmap, &mut band_state, params, &ctx);
6752 }
6753 self.clip_region = band_state.clip_region.take();
6754 if let Some(mask) = band_state.spare_mask.take() {
6755 self.spare_mask = Some(mask);
6756 }
6757 }
6758
6759 fn page_size(&self) -> (u32, u32) {
6760 (self.page_w, self.page_h)
6761 }
6762
6763 fn replay_and_show(&mut self, list: DisplayList, output_path: &str) -> Result<(), String> {
6764 self.join_pending()?;
6766
6767 let (page_w, page_h) = self.page_size();
6768
6769 if self.use_viewport_path {
6774 let icc_cache = build_icc_cache_for_list(&list, self.system_cmyk_bytes.as_ref(), false);
6775 let rgba = render_to_rgba_viewport(
6776 &list,
6777 page_w,
6778 page_h,
6779 self.dpi,
6780 Some(&icc_cache),
6781 self.no_aa,
6782 );
6783 let mut sink = self.sink_factory.create_sink(output_path)?;
6784 sink.begin_page(page_w, page_h)?;
6785 sink.write_rows(&rgba, page_h)?;
6786 sink.end_page()?;
6787 return Ok(());
6788 }
6789
6790 let band_h = select_band_height(page_w, page_h);
6791
6792 let icc_cache = build_icc_cache_for_list(&list, self.system_cmyk_bytes.as_ref(), false);
6794
6795 if band_h >= page_h {
6799 self.ensure_full_pixmap();
6800 let ctx = RenderContext {
6801 vp_x: 0.0,
6802 vp_y: 0.0,
6803 scale_x: 1.0,
6804 scale_y: 1.0,
6805 out_w: page_w,
6806 out_h: page_h,
6807 effective_dpi: self.dpi,
6808 icc: Some(&icc_cache),
6809 image_cache: None,
6810 preprocessed: None,
6811 elem_idx: 0,
6812 no_aa: self.no_aa,
6813 opm_zero_transparent: false,
6814 knockout_painter_pass: KnockoutPainterPass::None,
6815 parent_group_isolated: false,
6816 alpha_extraction_pass: false,
6817 layer_set: &self.layer_set,
6818 };
6819 let mut band_state = BandState {
6820 clip_region: None,
6821 spare_mask: None,
6822 clip_mask_cache: HashMap::new(),
6823 clip_mask_seen: HashSet::new(),
6824 mask_pool: Vec::new(),
6825 cmyk_buffer: None,
6826 op_bg_snapshot: None,
6827 op_touched: None,
6828 spot_mask: None,
6829 };
6830 for (idx, elem) in list.elements().iter().enumerate() {
6831 let elem_ctx = RenderContext {
6832 elem_idx: idx,
6833 ..ctx
6834 };
6835 render_element(&mut self.pixmap, &mut band_state, elem, &elem_ctx);
6836 }
6837 return self.show_page(output_path);
6838 }
6839
6840 if self.pixmap.width() > 1 {
6844 self.pixmap = Pixmap::new(1, 1).expect("Failed to create placeholder pixmap");
6845 }
6846
6847 let mut sink = self.sink_factory.create_sink(output_path)?;
6849 let dpi = self.dpi;
6850 let layer_set = self.layer_set.clone();
6851
6852 #[cfg(feature = "parallel")]
6853 {
6854 let no_aa = self.no_aa;
6858 let (tx, rx) = std::sync::mpsc::sync_channel(1);
6859 rayon::spawn(move || {
6860 let result = render_banded_to_sink(
6861 page_w, page_h, band_h, dpi, &list, &mut *sink, &icc_cache, no_aa, &layer_set,
6862 );
6863 let _ = tx.send(result);
6864 });
6865 self.pending_render = Some(rx);
6866 }
6867 #[cfg(not(feature = "parallel"))]
6868 {
6869 render_banded_to_sink(
6870 page_w, page_h, band_h, dpi, &list, &mut *sink, &icc_cache, self.no_aa, &layer_set,
6871 )?;
6872 }
6873
6874 Ok(())
6875 }
6876
6877 fn finish(&mut self) -> Result<(), String> {
6878 self.join_pending()
6879 }
6880}
6881
6882impl Drop for SkiaDevice {
6883 fn drop(&mut self) {
6884 if let Some(rx) = self.pending_render.take() {
6886 let _ = rx.recv();
6887 }
6888 }
6889}
6890
6891impl SkiaDevice {
6892 fn join_pending(&mut self) -> Result<(), String> {
6894 if let Some(rx) = self.pending_render.take() {
6895 match rx.recv() {
6896 Ok(result) => result?,
6897 Err(_) => return Err("Background render task failed".to_string()),
6898 }
6899 }
6900 Ok(())
6901 }
6902}
6903
6904fn has_cmyk_group(list: &DisplayList) -> bool {
6909 use stet_graphics::display_list::GroupColorSpace;
6910 for elem in list.elements() {
6911 match elem {
6912 DisplayElement::Group { elements, params } => {
6913 if params.color_space == GroupColorSpace::DeviceCMYK {
6914 return true;
6915 }
6916 if has_cmyk_group(elements) {
6917 return true;
6918 }
6919 }
6920 DisplayElement::SoftMasked { content, mask, .. } => {
6921 if has_cmyk_group(content) || has_cmyk_group(mask) {
6922 return true;
6923 }
6924 }
6925 DisplayElement::OcgGroup { elements, .. } => {
6926 if has_cmyk_group(elements) {
6927 return true;
6928 }
6929 }
6930 _ => {}
6931 }
6932 }
6933 false
6934}
6935
6936fn group_only_native_cmyk_fills(elements: &DisplayList) -> bool {
6943 let mut found_paint = false;
6944 for elem in elements.elements() {
6945 match elem {
6946 DisplayElement::InitClip => continue,
6947 DisplayElement::Clip { .. } => continue,
6948 DisplayElement::Fill { params, .. } => {
6949 if params.color.native_cmyk.is_none() {
6950 return false;
6951 }
6952 found_paint = true;
6953 }
6954 DisplayElement::Stroke { params, .. } => {
6955 if params.color.native_cmyk.is_none() {
6961 return false;
6962 }
6963 found_paint = true;
6964 }
6965 _ => return false,
6966 }
6967 }
6968 found_paint
6969}
6970
6971fn group_content_is_native_cmyk(elements: &DisplayList) -> bool {
6992 let mut found_paint = false;
6993 for elem in elements.elements() {
6994 match elem {
6995 DisplayElement::InitClip => continue,
6996 DisplayElement::Clip { .. } => continue,
6997 DisplayElement::Text { .. } => continue,
6998 DisplayElement::ErasePage => continue,
6999 DisplayElement::Fill { params, .. } => {
7000 if params.color.native_cmyk.is_none() {
7001 return false;
7002 }
7003 found_paint = true;
7004 }
7005 DisplayElement::Stroke { params, .. } => {
7006 if params.color.native_cmyk.is_none() {
7007 return false;
7008 }
7009 found_paint = true;
7010 }
7011 DisplayElement::Image { params, .. } => {
7012 if !is_cmyk_color_space(¶ms.color_space) {
7013 return false;
7014 }
7015 found_paint = true;
7016 }
7017 DisplayElement::AxialShading { .. }
7018 | DisplayElement::RadialShading { .. }
7019 | DisplayElement::MeshShading { .. }
7020 | DisplayElement::PatchShading { .. } => {
7021 return false;
7024 }
7025 DisplayElement::PatternFill { .. } => {
7026 return false;
7030 }
7031 DisplayElement::Group { elements: sub, .. } => {
7032 if !group_content_is_native_cmyk(sub) {
7033 return false;
7034 }
7035 found_paint = true;
7036 }
7037 DisplayElement::SoftMasked { .. } => {
7038 return false;
7049 }
7050 DisplayElement::OcgGroup { elements: sub, .. } => {
7051 if !group_content_is_native_cmyk(sub) {
7052 return false;
7053 }
7054 found_paint = true;
7055 }
7056 _ => return false,
7057 }
7058 }
7059 found_paint
7060}
7061
7062fn content_list_is_simple_native_cmyk(list: &DisplayList) -> bool {
7069 let mut found_paint = false;
7070 for elem in list.elements() {
7071 match elem {
7072 DisplayElement::InitClip
7073 | DisplayElement::Clip { .. }
7074 | DisplayElement::Text { .. }
7075 | DisplayElement::ErasePage => continue,
7076 DisplayElement::Fill { params, .. } => {
7077 if params.color.native_cmyk.is_none() {
7078 return false;
7079 }
7080 if params.blend_mode != 0 || params.alpha != 1.0 {
7081 return false;
7082 }
7083 found_paint = true;
7084 }
7085 DisplayElement::Stroke { params, .. } => {
7086 if params.color.native_cmyk.is_none() {
7087 return false;
7088 }
7089 if params.blend_mode != 0 || params.alpha != 1.0 {
7090 return false;
7091 }
7092 found_paint = true;
7093 }
7094 DisplayElement::Group { params, elements } => {
7102 if params.blend_mode != 0 || params.alpha != 1.0 {
7103 return false;
7104 }
7105 if !content_list_is_simple_native_cmyk(elements) {
7106 return false;
7107 }
7108 if elements.elements().iter().any(|e| {
7112 matches!(
7113 e,
7114 DisplayElement::Fill { .. } | DisplayElement::Stroke { .. }
7115 )
7116 }) {
7117 found_paint = true;
7118 }
7119 }
7120 _ => return false,
7121 }
7122 }
7123 found_paint
7124}
7125
7126fn has_overprint_elements(list: &DisplayList) -> bool {
7128 for elem in list.elements() {
7129 match elem {
7130 DisplayElement::Fill { params, .. } => {
7131 if params.overprint {
7132 return true;
7133 }
7134 }
7135 DisplayElement::Stroke { params, .. } => {
7136 if params.overprint {
7137 return true;
7138 }
7139 }
7140 DisplayElement::Image { params, .. } => {
7141 if params.overprint {
7142 return true;
7143 }
7144 }
7145 DisplayElement::AxialShading { params } => {
7146 if params.overprint {
7147 return true;
7148 }
7149 }
7150 DisplayElement::RadialShading { params } => {
7151 if params.overprint {
7152 return true;
7153 }
7154 }
7155 DisplayElement::MeshShading { params } => {
7156 if params.overprint {
7157 return true;
7158 }
7159 }
7160 DisplayElement::PatchShading { params } => {
7161 if params.overprint {
7162 return true;
7163 }
7164 }
7165 DisplayElement::Group { elements, .. } => {
7166 if has_overprint_elements(elements) {
7167 return true;
7168 }
7169 }
7170 DisplayElement::SoftMasked { content, mask, .. } => {
7171 if has_overprint_elements(content) || has_overprint_elements(mask) {
7172 return true;
7173 }
7174 }
7175 DisplayElement::OcgGroup { elements, .. } => {
7176 if has_overprint_elements(elements) {
7177 return true;
7178 }
7179 }
7180 _ => {}
7181 }
7182 }
7183 false
7184}
7185
7186#[allow(clippy::too_many_arguments)]
7189fn render_overprint_fill(
7190 pixmap: &mut Pixmap,
7191 cmyk_buf: &mut [f32],
7192 op_bg: &mut [u8],
7193 op_touched: &mut [u8],
7194 spot_mask: &[u8],
7195 band_state: &mut BandState,
7196 path: &PsPath,
7197 params: &FillParams,
7198 vp_x: f32,
7199 vp_y: f32,
7200 scale_x: f32,
7201 scale_y: f32,
7202 out_w: u32,
7203 out_h: u32,
7204 icc: Option<&IccCache>,
7205 no_aa: bool,
7206) {
7207 let Some(skia_path) = build_skia_path(path) else {
7208 return;
7209 };
7210 let fill_rule = to_fill_rule(¶ms.fill_rule);
7211
7212 let mut coverage_mask = match Mask::new(out_w, out_h) {
7213 Some(m) => m,
7214 None => return,
7215 };
7216 let transform = viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
7217 coverage_mask.fill_path(&skia_path, fill_rule, !no_aa, transform);
7218
7219 let (bbox_x0, bbox_y0, bbox_x1, bbox_y1) =
7221 path_device_bbox(&skia_path, transform, out_w, out_h);
7222
7223 let clip_coverage: Option<&[u8]> = match &band_state.clip_region {
7225 None => None,
7226 Some(ClipRegion::Rect(r)) => {
7227 let data = coverage_mask.data_mut();
7229 let stride = out_w as usize;
7230 for y in bbox_y0..bbox_y1 {
7231 let row_start = y * stride;
7232 for x in bbox_x0..bbox_x1 {
7233 let yu = y as u32;
7234 let xu = x as u32;
7235 if yu < r.y0 || yu >= r.y1 || xu < r.x0 || xu >= r.x1 {
7236 data[row_start + x] = 0;
7237 }
7238 }
7239 }
7240 None
7241 }
7242 Some(ClipRegion::Mask(clip_mask)) => Some(clip_mask.data()),
7243 };
7244
7245 let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
7251
7252 let (src_c, src_m, src_y, src_k) = if !is_custom_spot && let Some(c) = params.color.process_cmyk
7268 {
7269 c
7270 } else if let Some(c) = params.color.native_cmyk {
7271 c
7272 } else {
7273 let r = params.color.r;
7274 let g = params.color.g;
7275 let b = params.color.b;
7276 (1.0 - r, 1.0 - g, 1.0 - b, 0.0)
7277 };
7278
7279 let mut channels = params.painted_channels;
7280 if channels == 0 {
7283 channels = stet_graphics::device::CMYK_ALL;
7284 }
7285 if params.overprint_mode == 1
7287 && channels == stet_graphics::device::CMYK_ALL
7288 && params.is_device_cmyk
7289 {
7290 channels = 0;
7291 if src_c != 0.0 {
7292 channels |= stet_graphics::device::CMYK_C;
7293 }
7294 if src_m != 0.0 {
7295 channels |= stet_graphics::device::CMYK_M;
7296 }
7297 if src_y != 0.0 {
7298 channels |= stet_graphics::device::CMYK_Y;
7299 }
7300 if src_k != 0.0 {
7301 channels |= stet_graphics::device::CMYK_K;
7302 }
7303 if channels == 0 && !params.opm_paired {
7314 channels = stet_graphics::device::CMYK_ALL;
7315 }
7316 }
7317
7318 let is_k_only_cmyk = params.is_device_cmyk
7326 && params.overprint_mode == 0
7327 && src_c == 0.0
7328 && src_m == 0.0
7329 && src_y == 0.0;
7330 if channels == stet_graphics::device::CMYK_ALL && !is_custom_spot && !is_k_only_cmyk {
7331 let cov_data = coverage_mask.data();
7332 let stride = out_w as usize;
7333 for y in bbox_y0..bbox_y1 {
7334 for x in bbox_x0..bbox_x1 {
7335 let mi = y * stride + x;
7336 let mut cov = cov_data[mi] as f32 / 255.0;
7337 if let Some(clip) = clip_coverage {
7338 cov *= clip[mi] as f32 / 255.0;
7339 }
7340 if cov > 0.0 {
7341 let ci = mi * 4;
7342 cmyk_buf[ci] = src_c as f32;
7343 cmyk_buf[ci + 1] = src_m as f32;
7344 cmyk_buf[ci + 2] = src_y as f32;
7345 cmyk_buf[ci + 3] = src_k as f32;
7346 }
7347 }
7348 }
7349 let mut temp_mask = None;
7350 let Some(mask_ref) =
7351 resolve_clip_mask(&band_state.clip_region, &mut temp_mask, out_w, out_h)
7352 else {
7353 return;
7354 };
7355 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, no_aa);
7356 pixmap.fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
7357 return;
7358 }
7359
7360 let cov_data = coverage_mask.data();
7361 let stride = out_w as usize;
7362 let px_data = pixmap.data_mut();
7363 let px_stride = out_w as usize * 4;
7364
7365 for y in bbox_y0..bbox_y1 {
7366 for x in bbox_x0..bbox_x1 {
7367 let mi = y * stride + x;
7368 let mut cov = cov_data[mi] as f32 / 255.0;
7369 if let Some(clip) = clip_coverage {
7370 cov *= clip[mi] as f32 / 255.0;
7371 }
7372 if cov <= 0.0 {
7373 continue;
7374 }
7375
7376 let ci = mi * 4;
7377 let pi = y * px_stride + x * 4;
7378 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
7385 op_bg[pi] = px_data[pi];
7386 op_bg[pi + 1] = px_data[pi + 1];
7387 op_bg[pi + 2] = px_data[pi + 2];
7388 op_bg[pi + 3] = px_data[pi + 3];
7389 op_touched[mi] = 1;
7390 }
7391 let cur_c = cmyk_buf[ci] as f64;
7392 let cur_m = cmyk_buf[ci + 1] as f64;
7393 let cur_y = cmyk_buf[ci + 2] as f64;
7394 let cur_k = cmyk_buf[ci + 3] as f64;
7395 let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
7407 let pixmap_has_colour = px_data[pi + 3] > 0
7408 && (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
7409 let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
7417
7418 let is_promoted_gray = params.painted_channels == stet_graphics::device::CMYK_K
7434 && channels == stet_graphics::device::CMYK_K
7435 && params.is_device_cmyk
7436 && src_c == 0.0
7437 && src_m == 0.0
7438 && src_y == 0.0;
7439 let effective_channels = if is_promoted_gray && spot_mask[mi] == 0 {
7440 stet_graphics::device::CMYK_ALL
7441 } else {
7442 channels
7443 };
7444
7445 let new_c = if effective_channels & stet_graphics::device::CMYK_C != 0 {
7446 src_c
7447 } else {
7448 cur_c
7449 };
7450 let new_m = if effective_channels & stet_graphics::device::CMYK_M != 0 {
7451 src_m
7452 } else {
7453 cur_m
7454 };
7455 let new_y = if effective_channels & stet_graphics::device::CMYK_Y != 0 {
7456 src_y
7457 } else {
7458 cur_y
7459 };
7460 let new_k = if effective_channels & stet_graphics::device::CMYK_K != 0 {
7461 src_k
7462 } else {
7463 cur_k
7464 };
7465
7466 if !is_custom_spot {
7470 cmyk_buf[ci] = new_c as f32;
7471 cmyk_buf[ci + 1] = new_m as f32;
7472 cmyk_buf[ci + 2] = new_y as f32;
7473 cmyk_buf[ci + 3] = new_k as f32;
7474 }
7475
7476 let delta = (new_c - cur_c)
7490 .abs()
7491 .max((new_m - cur_m).abs())
7492 .max((new_y - cur_y).abs())
7493 .max((new_k - cur_k).abs());
7494 if delta < 1e-4 && spot_mask[mi] != 0 && pixmap_has_colour && !is_custom_spot {
7495 continue;
7496 }
7497
7498 let (r, g, b) =
7499 if is_promoted_gray && effective_channels == stet_graphics::device::CMYK_ALL {
7500 (params.color.r, params.color.g, params.color.b)
7515 } else if use_multiplicative {
7516 let bg_r = px_data[pi] as f64 / 255.0;
7522 let bg_g = px_data[pi + 1] as f64 / 255.0;
7523 let bg_b = px_data[pi + 2] as f64 / 255.0;
7524 let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
7525 1.0 - src_c
7526 } else {
7527 1.0
7528 };
7529 let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
7530 1.0 - src_m
7531 } else {
7532 1.0
7533 };
7534 let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
7535 1.0 - src_y
7536 } else {
7537 1.0
7538 };
7539 let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
7540 1.0 - src_k
7541 } else {
7542 1.0
7543 };
7544 (
7545 (bg_r * over_r * k_fac).clamp(0.0, 1.0),
7546 (bg_g * over_g * k_fac).clamp(0.0, 1.0),
7547 (bg_b * over_b * k_fac).clamp(0.0, 1.0),
7548 )
7549 } else if let Some(icc_cache) = icc {
7550 icc_cache
7551 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
7552 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
7553 } else {
7554 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
7555 };
7556
7557 let a = (cov * params.alpha as f32).min(1.0);
7558 let (bk_r, bk_g, bk_b, bk_a) = if op_touched[mi] != 0 {
7571 let new_r = (r as f32 * 255.0).clamp(0.0, 255.0);
7572 let new_g = (g as f32 * 255.0).clamp(0.0, 255.0);
7573 let new_b = (b as f32 * 255.0).clamp(0.0, 255.0);
7574 let dr = (op_bg[pi] as f32 - new_r).abs();
7575 let dg = (op_bg[pi + 1] as f32 - new_g).abs();
7576 let db = (op_bg[pi + 2] as f32 - new_b).abs();
7577 if dr.max(dg).max(db) <= 4.0 {
7578 (op_bg[pi], op_bg[pi + 1], op_bg[pi + 2], op_bg[pi + 3])
7579 } else {
7580 (
7581 px_data[pi],
7582 px_data[pi + 1],
7583 px_data[pi + 2],
7584 px_data[pi + 3],
7585 )
7586 }
7587 } else {
7588 (
7589 px_data[pi],
7590 px_data[pi + 1],
7591 px_data[pi + 2],
7592 px_data[pi + 3],
7593 )
7594 };
7595 let dst_a = bk_a as f32 / 255.0;
7596 let one_minus_a = 1.0 - a;
7597 let out_a = a + dst_a * one_minus_a;
7598 if out_a > 0.0 {
7599 px_data[pi] = ((r as f32 * a + (bk_r as f32 / 255.0) * one_minus_a) * 255.0)
7604 .clamp(0.0, 255.0)
7605 .round() as u8;
7606 px_data[pi + 1] = ((g as f32 * a + (bk_g as f32 / 255.0) * one_minus_a) * 255.0)
7607 .clamp(0.0, 255.0)
7608 .round() as u8;
7609 px_data[pi + 2] = ((b as f32 * a + (bk_b as f32 / 255.0) * one_minus_a) * 255.0)
7610 .clamp(0.0, 255.0)
7611 .round() as u8;
7612 px_data[pi + 3] = (out_a * 255.0).round() as u8;
7613 }
7614 }
7615 }
7616}
7617fn cmyk_to_rgb_plrm(c: f64, m: f64, y: f64, k: f64) -> (f64, f64, f64) {
7619 (
7620 1.0 - (c + k).min(1.0),
7621 1.0 - (m + k).min(1.0),
7622 1.0 - (y + k).min(1.0),
7623 )
7624}
7625
7626#[allow(clippy::too_many_arguments)]
7628fn path_device_bbox(
7631 skia_path: &stet_tiny_skia::Path,
7632 transform: Transform,
7633 w: u32,
7634 h: u32,
7635) -> (usize, usize, usize, usize) {
7636 let b = skia_path.bounds();
7637 let mut corners = [
7638 stet_tiny_skia::Point {
7639 x: b.left(),
7640 y: b.top(),
7641 },
7642 stet_tiny_skia::Point {
7643 x: b.right(),
7644 y: b.top(),
7645 },
7646 stet_tiny_skia::Point {
7647 x: b.right(),
7648 y: b.bottom(),
7649 },
7650 stet_tiny_skia::Point {
7651 x: b.left(),
7652 y: b.bottom(),
7653 },
7654 ];
7655 transform.map_points(&mut corners);
7656 let min_x = corners.iter().map(|p| p.x).fold(f32::INFINITY, f32::min);
7657 let min_y = corners.iter().map(|p| p.y).fold(f32::INFINITY, f32::min);
7658 let max_x = corners
7659 .iter()
7660 .map(|p| p.x)
7661 .fold(f32::NEG_INFINITY, f32::max);
7662 let max_y = corners
7663 .iter()
7664 .map(|p| p.y)
7665 .fold(f32::NEG_INFINITY, f32::max);
7666 let x0 = (min_x.floor() as i32 - 1).max(0) as usize;
7668 let y0 = (min_y.floor() as i32 - 1).max(0) as usize;
7669 let x1 = (max_x.ceil() as i32 + 1).clamp(0, w as i32) as usize;
7670 let y1 = (max_y.ceil() as i32 + 1).clamp(0, h as i32) as usize;
7671 (x0, y0, x1, y1)
7672}
7673
7674fn update_cmyk_buffer_for_fill(
7675 cmyk_buf: &mut [f32],
7676 spot_mask: &mut [u8],
7677 path: &PsPath,
7678 params: &FillParams,
7679 vp_x: f32,
7680 vp_y: f32,
7681 scale_x: f32,
7682 scale_y: f32,
7683 out_w: u32,
7684 out_h: u32,
7685 clip_region: &Option<ClipRegion>,
7686 no_aa: bool,
7687 icc: Option<&IccCache>,
7688) {
7689 let is_custom_spot = params.painted_channels == 0
7709 && !params.is_device_cmyk
7710 && params.color.process_cmyk.is_some();
7711
7712 let has_spot_contrib = (is_custom_spot && params.color.native_cmyk.is_some())
7726 || matches!(
7727 (params.color.native_cmyk, params.color.process_cmyk),
7728 (Some(nat), Some(proc_))
7729 if (nat.0 - proc_.0).abs() > 1e-6
7730 || (nat.1 - proc_.1).abs() > 1e-6
7731 || (nat.2 - proc_.2).abs() > 1e-6
7732 || (nat.3 - proc_.3).abs() > 1e-6
7733 );
7734
7735 let (src_c, src_m, src_y, src_k) = if is_custom_spot {
7743 (0.0, 0.0, 0.0, 0.0)
7744 } else if let Some(c) = params.color.process_cmyk {
7745 c
7746 } else if let Some(c) = params.color.native_cmyk {
7747 c
7748 } else if let Some(cmyk) = icc.and_then(|i| {
7749 i.convert_rgb_to_cmyk_readonly(params.color.r, params.color.g, params.color.b)
7750 }) {
7751 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
7752 } else {
7753 (
7754 (1.0 - params.color.r).clamp(0.0, 1.0),
7755 (1.0 - params.color.g).clamp(0.0, 1.0),
7756 (1.0 - params.color.b).clamp(0.0, 1.0),
7757 0.0,
7758 )
7759 };
7760 let Some(skia_path) = build_skia_path(path) else {
7761 return;
7762 };
7763
7764 let mut coverage_mask = match Mask::new(out_w, out_h) {
7765 Some(m) => m,
7766 None => return,
7767 };
7768 let transform = viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
7769 let fill_rule = to_fill_rule(¶ms.fill_rule);
7770 coverage_mask.fill_path(&skia_path, fill_rule, !no_aa, transform);
7771
7772 let cov_data = coverage_mask.data();
7773 let clip_data: Option<&[u8]> = match clip_region {
7774 Some(ClipRegion::Mask(m)) => Some(m.data()),
7775 _ => None,
7776 };
7777
7778 let (mut bx0, mut by0, mut bx1, mut by1) =
7780 path_device_bbox(&skia_path, transform, out_w, out_h);
7781 if let Some(ClipRegion::Rect(r)) = clip_region {
7782 bx0 = bx0.max(r.x0 as usize);
7783 by0 = by0.max(r.y0 as usize);
7784 bx1 = bx1.min(r.x1 as usize);
7785 by1 = by1.min(r.y1 as usize);
7786 }
7787
7788 let stride = out_w as usize;
7789 for y in by0..by1 {
7790 for x in bx0..bx1 {
7791 let mi = y * stride + x;
7792 let mut cov = cov_data[mi] as f32 / 255.0;
7793 if let Some(clip) = clip_data {
7794 cov *= clip[mi] as f32 / 255.0;
7795 }
7796 if cov > 0.0 {
7797 let ci = mi * 4;
7798 cmyk_buf[ci] = src_c as f32;
7799 cmyk_buf[ci + 1] = src_m as f32;
7800 cmyk_buf[ci + 2] = src_y as f32;
7801 cmyk_buf[ci + 3] = src_k as f32;
7802 if has_spot_contrib {
7803 spot_mask[mi] = 1;
7804 }
7805 }
7806 }
7807 }
7808}
7809
7810#[allow(clippy::too_many_arguments)]
7815fn render_overprint_stroke(
7816 pixmap: &mut Pixmap,
7817 cmyk_buf: &mut [f32],
7818 op_bg: &mut [u8],
7819 op_touched: &mut [u8],
7820 spot_mask: &[u8],
7821 band_state: &mut BandState,
7822 skia_path: &stet_tiny_skia::Path,
7823 stroke: &Stroke,
7824 transform: Transform,
7825 params: &StrokeParams,
7826 out_w: u32,
7827 out_h: u32,
7828 icc: Option<&IccCache>,
7829 no_aa: bool,
7830) {
7831 let resolution_scale = (transform.sx * transform.sx + transform.sy * transform.sy)
7833 .sqrt()
7834 .max(1.0);
7835 let dashed_op;
7836 let stroke_src = if let Some(ref dash) = stroke.dash {
7837 dashed_op = skia_path.dash(dash, resolution_scale);
7838 match dashed_op.as_ref() {
7839 Some(p) => p,
7840 None => skia_path,
7841 }
7842 } else {
7843 skia_path
7844 };
7845 let Some(stroked_user) = stroke_src.stroke(stroke, resolution_scale) else {
7846 return;
7847 };
7848 let Some(stroked) = stroked_user.transform(transform) else {
7849 return;
7850 };
7851
7852 let mut coverage_mask = match Mask::new(out_w, out_h) {
7853 Some(m) => m,
7854 None => return,
7855 };
7856 coverage_mask.fill_path(
7857 &stroked,
7858 SkiaFillRule::Winding,
7859 !no_aa,
7860 Transform::identity(),
7861 );
7862
7863 let (bbox_x0, bbox_y0, bbox_x1, bbox_y1) =
7864 path_device_bbox(&stroked, Transform::identity(), out_w, out_h);
7865
7866 let clip_coverage: Option<&[u8]> = match &band_state.clip_region {
7868 None => None,
7869 Some(ClipRegion::Rect(r)) => {
7870 let data = coverage_mask.data_mut();
7871 let stride = out_w as usize;
7872 for y in bbox_y0..bbox_y1 {
7873 let row_start = y * stride;
7874 for x in bbox_x0..bbox_x1 {
7875 let yu = y as u32;
7876 let xu = x as u32;
7877 if yu < r.y0 || yu >= r.y1 || xu < r.x0 || xu >= r.x1 {
7878 data[row_start + x] = 0;
7879 }
7880 }
7881 }
7882 None
7883 }
7884 Some(ClipRegion::Mask(clip_mask)) => Some(clip_mask.data()),
7885 };
7886
7887 let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
7891
7892 let (src_c, src_m, src_y, src_k) = if !is_custom_spot && let Some(c) = params.color.process_cmyk
7899 {
7900 c
7901 } else if let Some(c) = params.color.native_cmyk {
7902 c
7903 } else {
7904 let r = params.color.r;
7905 let g = params.color.g;
7906 let b = params.color.b;
7907 (1.0 - r, 1.0 - g, 1.0 - b, 0.0)
7908 };
7909
7910 let mut channels = params.painted_channels;
7911 if channels == 0 {
7912 channels = stet_graphics::device::CMYK_ALL;
7913 }
7914 if params.overprint_mode == 1
7915 && channels == stet_graphics::device::CMYK_ALL
7916 && params.is_device_cmyk
7917 {
7918 channels = 0;
7919 if src_c != 0.0 {
7920 channels |= stet_graphics::device::CMYK_C;
7921 }
7922 if src_m != 0.0 {
7923 channels |= stet_graphics::device::CMYK_M;
7924 }
7925 if src_y != 0.0 {
7926 channels |= stet_graphics::device::CMYK_Y;
7927 }
7928 if src_k != 0.0 {
7929 channels |= stet_graphics::device::CMYK_K;
7930 }
7931 if channels == 0 && !params.opm_paired {
7936 channels = stet_graphics::device::CMYK_ALL;
7937 }
7938 }
7939
7940 let is_k_only_cmyk = params.is_device_cmyk
7941 && params.overprint_mode == 0
7942 && src_c == 0.0
7943 && src_m == 0.0
7944 && src_y == 0.0;
7945 if channels == stet_graphics::device::CMYK_ALL && !is_custom_spot && !is_k_only_cmyk {
7946 let cov_data = coverage_mask.data();
7951 let stride = out_w as usize;
7952 for y in bbox_y0..bbox_y1 {
7953 for x in bbox_x0..bbox_x1 {
7954 let mi = y * stride + x;
7955 let mut cov = cov_data[mi] as f32 / 255.0;
7956 if let Some(clip) = clip_coverage {
7957 cov *= clip[mi] as f32 / 255.0;
7958 }
7959 if cov > 0.0 {
7960 let ci = mi * 4;
7961 cmyk_buf[ci] = src_c as f32;
7962 cmyk_buf[ci + 1] = src_m as f32;
7963 cmyk_buf[ci + 2] = src_y as f32;
7964 cmyk_buf[ci + 3] = src_k as f32;
7965 }
7966 }
7967 }
7968 let mut temp_mask = None;
7969 let Some(mask_ref) =
7970 resolve_clip_mask(&band_state.clip_region, &mut temp_mask, out_w, out_h)
7971 else {
7972 return;
7973 };
7974 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, no_aa);
7975 pixmap.stroke_path(skia_path, &paint, stroke, transform, mask_ref);
7976 return;
7977 }
7978
7979 let cov_data = coverage_mask.data();
7980 let stride = out_w as usize;
7981 let px_data = pixmap.data_mut();
7982 let px_stride = out_w as usize * 4;
7983
7984 for y in bbox_y0..bbox_y1 {
7985 for x in bbox_x0..bbox_x1 {
7986 let mi = y * stride + x;
7987 let mut cov = cov_data[mi] as f32 / 255.0;
7988 if let Some(clip) = clip_coverage {
7989 cov *= clip[mi] as f32 / 255.0;
7990 }
7991 if cov <= 0.0 {
7992 continue;
7993 }
7994
7995 let ci = mi * 4;
7996 let pi = y * px_stride + x * 4;
7997 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
8002 op_bg[pi] = px_data[pi];
8003 op_bg[pi + 1] = px_data[pi + 1];
8004 op_bg[pi + 2] = px_data[pi + 2];
8005 op_bg[pi + 3] = px_data[pi + 3];
8006 op_touched[mi] = 1;
8007 }
8008 let cur_c = cmyk_buf[ci] as f64;
8009 let cur_m = cmyk_buf[ci + 1] as f64;
8010 let cur_y = cmyk_buf[ci + 2] as f64;
8011 let cur_k = cmyk_buf[ci + 3] as f64;
8012 let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
8013 let pixmap_has_colour = px_data[pi + 3] > 0
8014 && (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
8015 let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
8023
8024 let is_promoted_gray = params.painted_channels == stet_graphics::device::CMYK_K
8027 && channels == stet_graphics::device::CMYK_K
8028 && params.is_device_cmyk
8029 && src_c == 0.0
8030 && src_m == 0.0
8031 && src_y == 0.0;
8032 let effective_channels = if is_promoted_gray && spot_mask[mi] == 0 {
8033 stet_graphics::device::CMYK_ALL
8034 } else {
8035 channels
8036 };
8037
8038 let new_c = if effective_channels & stet_graphics::device::CMYK_C != 0 {
8039 src_c
8040 } else {
8041 cur_c
8042 };
8043 let new_m = if effective_channels & stet_graphics::device::CMYK_M != 0 {
8044 src_m
8045 } else {
8046 cur_m
8047 };
8048 let new_y = if effective_channels & stet_graphics::device::CMYK_Y != 0 {
8049 src_y
8050 } else {
8051 cur_y
8052 };
8053 let new_k = if effective_channels & stet_graphics::device::CMYK_K != 0 {
8054 src_k
8055 } else {
8056 cur_k
8057 };
8058
8059 if !is_custom_spot {
8060 cmyk_buf[ci] = new_c as f32;
8061 cmyk_buf[ci + 1] = new_m as f32;
8062 cmyk_buf[ci + 2] = new_y as f32;
8063 cmyk_buf[ci + 3] = new_k as f32;
8064 }
8065
8066 let delta = (new_c - cur_c)
8068 .abs()
8069 .max((new_m - cur_m).abs())
8070 .max((new_y - cur_y).abs())
8071 .max((new_k - cur_k).abs());
8072 if delta < 1e-4 && spot_mask[mi] != 0 && pixmap_has_colour && !is_custom_spot {
8073 continue;
8074 }
8075
8076 let (r, g, b) =
8077 if is_promoted_gray && effective_channels == stet_graphics::device::CMYK_ALL {
8078 (params.color.r, params.color.g, params.color.b)
8084 } else if use_multiplicative {
8085 let bg_r = px_data[pi] as f64 / 255.0;
8086 let bg_g = px_data[pi + 1] as f64 / 255.0;
8087 let bg_b = px_data[pi + 2] as f64 / 255.0;
8088 let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
8089 1.0 - src_c
8090 } else {
8091 1.0
8092 };
8093 let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
8094 1.0 - src_m
8095 } else {
8096 1.0
8097 };
8098 let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
8099 1.0 - src_y
8100 } else {
8101 1.0
8102 };
8103 let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
8104 1.0 - src_k
8105 } else {
8106 1.0
8107 };
8108 (
8109 (bg_r * over_r * k_fac).clamp(0.0, 1.0),
8110 (bg_g * over_g * k_fac).clamp(0.0, 1.0),
8111 (bg_b * over_b * k_fac).clamp(0.0, 1.0),
8112 )
8113 } else if let Some(icc_cache) = icc {
8114 icc_cache
8115 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
8116 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
8117 } else {
8118 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
8119 };
8120
8121 let a = (cov * params.alpha as f32).min(1.0);
8122 let (bk_r, bk_g, bk_b, bk_a) = if op_touched[mi] != 0 {
8127 let new_r = (r as f32 * 255.0).clamp(0.0, 255.0);
8128 let new_g = (g as f32 * 255.0).clamp(0.0, 255.0);
8129 let new_b = (b as f32 * 255.0).clamp(0.0, 255.0);
8130 let dr = (op_bg[pi] as f32 - new_r).abs();
8131 let dg = (op_bg[pi + 1] as f32 - new_g).abs();
8132 let db = (op_bg[pi + 2] as f32 - new_b).abs();
8133 if dr.max(dg).max(db) <= 4.0 {
8134 (op_bg[pi], op_bg[pi + 1], op_bg[pi + 2], op_bg[pi + 3])
8135 } else {
8136 (
8137 px_data[pi],
8138 px_data[pi + 1],
8139 px_data[pi + 2],
8140 px_data[pi + 3],
8141 )
8142 }
8143 } else {
8144 (
8145 px_data[pi],
8146 px_data[pi + 1],
8147 px_data[pi + 2],
8148 px_data[pi + 3],
8149 )
8150 };
8151 let dst_a = bk_a as f32 / 255.0;
8152 let one_minus_a = 1.0 - a;
8153 let out_a = a + dst_a * one_minus_a;
8154 if out_a > 0.0 {
8155 px_data[pi] = ((r as f32 * a + (bk_r as f32 / 255.0) * one_minus_a) * 255.0)
8157 .clamp(0.0, 255.0)
8158 .round() as u8;
8159 px_data[pi + 1] = ((g as f32 * a + (bk_g as f32 / 255.0) * one_minus_a) * 255.0)
8160 .clamp(0.0, 255.0)
8161 .round() as u8;
8162 px_data[pi + 2] = ((b as f32 * a + (bk_b as f32 / 255.0) * one_minus_a) * 255.0)
8163 .clamp(0.0, 255.0)
8164 .round() as u8;
8165 px_data[pi + 3] = (out_a * 255.0).round() as u8;
8166 }
8167 }
8168 }
8169}
8170
8171#[allow(clippy::too_many_arguments)]
8176fn update_cmyk_buffer_for_stroke(
8177 cmyk_buf: &mut [f32],
8178 spot_mask: &mut [u8],
8179 path: &PsPath,
8180 params: &StrokeParams,
8181 stroke: &Stroke,
8182 transform: Transform,
8183 out_w: u32,
8184 out_h: u32,
8185 clip_region: &Option<ClipRegion>,
8186 no_aa: bool,
8187 icc: Option<&IccCache>,
8188) {
8189 let is_custom_spot = params.painted_channels == 0
8196 && !params.is_device_cmyk
8197 && params.color.process_cmyk.is_some();
8198 let has_spot_contrib = (is_custom_spot && params.color.native_cmyk.is_some())
8200 || matches!(
8201 (params.color.native_cmyk, params.color.process_cmyk),
8202 (Some(nat), Some(proc_))
8203 if (nat.0 - proc_.0).abs() > 1e-6
8204 || (nat.1 - proc_.1).abs() > 1e-6
8205 || (nat.2 - proc_.2).abs() > 1e-6
8206 || (nat.3 - proc_.3).abs() > 1e-6
8207 );
8208
8209 let (src_c, src_m, src_y, src_k) = if is_custom_spot {
8210 (0.0, 0.0, 0.0, 0.0)
8211 } else if let Some(c) = params.color.process_cmyk {
8212 c
8213 } else if let Some(c) = params.color.native_cmyk {
8214 c
8215 } else if let Some(cmyk) = icc.and_then(|i| {
8216 i.convert_rgb_to_cmyk_readonly(params.color.r, params.color.g, params.color.b)
8217 }) {
8218 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
8219 } else {
8220 (
8221 (1.0 - params.color.r).clamp(0.0, 1.0),
8222 (1.0 - params.color.g).clamp(0.0, 1.0),
8223 (1.0 - params.color.b).clamp(0.0, 1.0),
8224 0.0,
8225 )
8226 };
8227
8228 let Some(skia_path) = build_skia_path(path) else {
8229 return;
8230 };
8231
8232 let resolution_scale = (transform.sx * transform.sx + transform.sy * transform.sy)
8236 .sqrt()
8237 .max(1.0);
8238 let dashed_op;
8239 let stroke_src = if let Some(ref dash) = stroke.dash {
8240 dashed_op = skia_path.dash(dash, resolution_scale);
8241 match dashed_op.as_ref() {
8242 Some(p) => p,
8243 None => &skia_path,
8244 }
8245 } else {
8246 &skia_path
8247 };
8248 let Some(stroked_user) = stroke_src.stroke(stroke, resolution_scale) else {
8249 return;
8250 };
8251 let Some(stroked) = stroked_user.transform(transform) else {
8252 return;
8253 };
8254
8255 let mut coverage_mask = match Mask::new(out_w, out_h) {
8256 Some(m) => m,
8257 None => return,
8258 };
8259 coverage_mask.fill_path(
8260 &stroked,
8261 SkiaFillRule::Winding,
8262 !no_aa,
8263 Transform::identity(),
8264 );
8265
8266 let cov_data = coverage_mask.data();
8267 let clip_data: Option<&[u8]> = match clip_region {
8268 Some(ClipRegion::Mask(m)) => Some(m.data()),
8269 _ => None,
8270 };
8271
8272 let (mut bx0, mut by0, mut bx1, mut by1) =
8273 path_device_bbox(&stroked, Transform::identity(), out_w, out_h);
8274 if let Some(ClipRegion::Rect(r)) = clip_region {
8275 bx0 = bx0.max(r.x0 as usize);
8276 by0 = by0.max(r.y0 as usize);
8277 bx1 = bx1.min(r.x1 as usize);
8278 by1 = by1.min(r.y1 as usize);
8279 }
8280
8281 let stride = out_w as usize;
8282 for y in by0..by1 {
8283 for x in bx0..bx1 {
8284 let mi = y * stride + x;
8285 let mut cov = cov_data[mi] as f32 / 255.0;
8286 if let Some(clip) = clip_data {
8287 cov *= clip[mi] as f32 / 255.0;
8288 }
8289 if cov > 0.0 {
8290 let ci = mi * 4;
8291 cmyk_buf[ci] = src_c as f32;
8292 cmyk_buf[ci + 1] = src_m as f32;
8293 cmyk_buf[ci + 2] = src_y as f32;
8294 cmyk_buf[ci + 3] = src_k as f32;
8295 if has_spot_contrib {
8296 spot_mask[mi] = 1;
8297 }
8298 }
8299 }
8300 }
8301}
8302
8303#[allow(clippy::too_many_arguments)]
8305fn render_overprint_image(
8306 pixmap: &mut Pixmap,
8307 cmyk_buf: &mut [f32],
8308 op_bg: &mut [u8],
8309 op_touched: &mut [u8],
8310 band_state: &mut BandState,
8311 sample_data: &[u8],
8312 params: &ImageParams,
8313 vp_x: f32,
8314 vp_y: f32,
8315 scale_x: f32,
8316 scale_y: f32,
8317 out_w: u32,
8318 out_h: u32,
8319 icc: Option<&IccCache>,
8320) {
8321 let iw = params.width as usize;
8322 let ih = params.height as usize;
8323 let Some(image_inv) = params.image_matrix.invert() else {
8324 return;
8325 };
8326 let combined = params.ctm.concat(&image_inv);
8327 let Some(inv_combined) = combined.invert() else {
8328 return;
8329 };
8330
8331 let px_data = pixmap.data_mut();
8332 let stride = out_w as usize;
8333 let inv_sx = 1.0 / scale_x as f64;
8334 let inv_sy = 1.0 / scale_y as f64;
8335
8336 let clip_data: Option<&[u8]> = match &band_state.clip_region {
8337 Some(ClipRegion::Mask(m)) => Some(m.data()),
8338 _ => None,
8339 };
8340 let clip_rect = match &band_state.clip_region {
8341 Some(ClipRegion::Rect(r)) => Some(*r),
8342 _ => None,
8343 };
8344
8345 let mask_info = if let ImageColorSpace::Mask {
8346 color, polarity, ..
8347 } = ¶ms.color_space
8348 {
8349 let (src_c, src_m, src_y, src_k) = color.native_cmyk.unwrap_or_else(|| {
8350 let r = color.r;
8351 let g = color.g;
8352 let b = color.b;
8353 (1.0 - r, 1.0 - g, 1.0 - b, 0.0)
8354 });
8355 Some((src_c, src_m, src_y, src_k, *polarity, iw.div_ceil(8)))
8356 } else {
8357 None
8358 };
8359
8360 for by in 0..out_h as usize {
8361 for bx in 0..out_w as usize {
8362 if let Some(ref r) = clip_rect
8363 && ((by as u32) < r.y0
8364 || (by as u32) >= r.y1
8365 || (bx as u32) < r.x0
8366 || (bx as u32) >= r.x1)
8367 {
8368 continue;
8369 }
8370 if let Some(clip) = clip_data {
8371 let ci_clip = by * stride + bx;
8372 if clip[ci_clip] == 0 {
8373 let bh = out_h as usize;
8374 let has_neighbor = (bx > 0 && clip[ci_clip - 1] != 0)
8375 || (bx + 1 < stride && clip[ci_clip + 1] != 0)
8376 || (by > 0 && clip[ci_clip - stride] != 0)
8377 || (by + 1 < bh && clip[ci_clip + stride] != 0)
8378 || (bx > 0 && by > 0 && clip[ci_clip - stride - 1] != 0)
8379 || (bx + 1 < stride && by > 0 && clip[ci_clip - stride + 1] != 0)
8380 || (bx > 0 && by + 1 < bh && clip[ci_clip + stride - 1] != 0)
8381 || (bx + 1 < stride && by + 1 < bh && clip[ci_clip + stride + 1] != 0);
8382 if !has_neighbor {
8383 continue;
8384 }
8385 }
8386 }
8387
8388 let dx = (bx as f64 + 0.5) * inv_sx + vp_x as f64;
8390 let dy = (by as f64 + 0.5) * inv_sy + vp_y as f64;
8391 let ix = inv_combined.a * dx + inv_combined.c * dy + inv_combined.tx;
8392 let iy = inv_combined.b * dx + inv_combined.d * dy + inv_combined.ty;
8393
8394 let col = ix.floor() as i64;
8395 let row = iy.floor() as i64;
8396 if col < 0 || col >= iw as i64 || row < 0 || row >= ih as i64 {
8397 continue;
8398 }
8399 let col = col as usize;
8400 let row = row as usize;
8401
8402 let (src_c, src_m, src_y, src_k) =
8403 if let Some((mc, mm, my, mk, polarity, bytes_per_row)) = mask_info {
8404 let byte_idx = row * bytes_per_row + col / 8;
8405 let bit_offset = 7 - (col % 8);
8406 let bit = if byte_idx < sample_data.len() {
8407 (sample_data[byte_idx] >> bit_offset) & 1
8408 } else {
8409 0
8410 };
8411 let paint = if polarity { bit == 1 } else { bit == 0 };
8412 if !paint {
8413 continue;
8414 }
8415 (mc, mm, my, mk)
8416 } else if let Some(cmyk) =
8417 sample_pixel_cmyk(sample_data, ¶ms.color_space, iw, row, col)
8418 {
8419 cmyk
8420 } else {
8421 continue;
8422 };
8423
8424 let mi = by * stride + bx;
8425 let ci = mi * 4;
8426 let pi = mi * 4;
8427
8428 if image_cs_has_spot_tint_transform(¶ms.color_space) {
8447 let cur_c = cmyk_buf[ci] as f64;
8448 let cur_m = cmyk_buf[ci + 1] as f64;
8449 let cur_y = cmyk_buf[ci + 2] as f64;
8450 let cur_k = cmyk_buf[ci + 3] as f64;
8451 let cur_is_zero = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
8452 let named = params.painted_channels;
8453 let opm1 = params.overprint_mode == 1;
8460 let (new_c, new_m, new_y, new_k) = if cur_is_zero {
8461 (src_c, src_m, src_y, src_k)
8462 } else {
8463 let nc =
8464 if named & stet_graphics::device::CMYK_C != 0 && !(opm1 && src_c == 0.0) {
8465 src_c
8466 } else {
8467 cur_c
8468 };
8469 let nm =
8470 if named & stet_graphics::device::CMYK_M != 0 && !(opm1 && src_m == 0.0) {
8471 src_m
8472 } else {
8473 cur_m
8474 };
8475 let ny =
8476 if named & stet_graphics::device::CMYK_Y != 0 && !(opm1 && src_y == 0.0) {
8477 src_y
8478 } else {
8479 cur_y
8480 };
8481 let nk =
8482 if named & stet_graphics::device::CMYK_K != 0 && !(opm1 && src_k == 0.0) {
8483 src_k
8484 } else {
8485 cur_k
8486 };
8487 (nc, nm, ny, nk)
8488 };
8489 cmyk_buf[ci] = new_c as f32;
8490 cmyk_buf[ci + 1] = new_m as f32;
8491 cmyk_buf[ci + 2] = new_y as f32;
8492 cmyk_buf[ci + 3] = new_k as f32;
8493 let alt_is_non_cmyk = image_cs_alt_is_non_cmyk(¶ms.color_space);
8502 let (r, g, b) = if cur_is_zero
8503 && alt_is_non_cmyk
8504 && let Some(rgb) =
8505 sample_pixel_visual_rgb(sample_data, ¶ms.color_space, iw, row, col)
8506 {
8507 rgb
8508 } else if let Some(icc_cache) = icc {
8509 icc_cache
8510 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
8511 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
8512 } else {
8513 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
8514 };
8515 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
8516 op_bg[pi] = px_data[pi];
8517 op_bg[pi + 1] = px_data[pi + 1];
8518 op_bg[pi + 2] = px_data[pi + 2];
8519 op_bg[pi + 3] = px_data[pi + 3];
8520 op_touched[mi] = 1;
8521 }
8522 px_data[pi] = (r * 255.0).round() as u8;
8523 px_data[pi + 1] = (g * 255.0).round() as u8;
8524 px_data[pi + 2] = (b * 255.0).round() as u8;
8525 px_data[pi + 3] = 255;
8526 continue;
8527 }
8528
8529 let mut channels = params.painted_channels;
8530 if channels == 0 {
8533 channels = stet_graphics::device::CMYK_ALL;
8534 }
8535 let is_direct_cmyk = matches!(
8536 ¶ms.color_space,
8537 ImageColorSpace::DeviceCMYK
8538 | ImageColorSpace::ICCBased { n: 4, .. }
8539 | ImageColorSpace::Mask { .. }
8540 );
8541 let is_custom_spot = params.painted_channels == 0
8551 && !is_cmyk_color_space(¶ms.color_space)
8552 && match ¶ms.color_space {
8553 ImageColorSpace::Mask { color, .. } => color.native_cmyk.is_some(),
8554 _ => true,
8555 };
8556 if params.overprint_mode == 1
8557 && channels == stet_graphics::device::CMYK_ALL
8558 && is_direct_cmyk
8559 {
8560 channels = 0;
8561 if src_c != 0.0 {
8562 channels |= stet_graphics::device::CMYK_C;
8563 }
8564 if src_m != 0.0 {
8565 channels |= stet_graphics::device::CMYK_M;
8566 }
8567 if src_y != 0.0 {
8568 channels |= stet_graphics::device::CMYK_Y;
8569 }
8570 if src_k != 0.0 {
8571 channels |= stet_graphics::device::CMYK_K;
8572 }
8573 }
8574
8575 let cur_c = cmyk_buf[ci] as f64;
8576 let cur_m = cmyk_buf[ci + 1] as f64;
8577 let cur_y = cmyk_buf[ci + 2] as f64;
8578 let cur_k = cmyk_buf[ci + 3] as f64;
8579 let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
8580 let pixmap_has_colour = px_data[pi + 3] > 0
8581 && (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
8582 let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
8590
8591 let new_c = if channels & stet_graphics::device::CMYK_C != 0 {
8592 src_c
8593 } else {
8594 cur_c
8595 };
8596 let new_m = if channels & stet_graphics::device::CMYK_M != 0 {
8597 src_m
8598 } else {
8599 cur_m
8600 };
8601 let new_y = if channels & stet_graphics::device::CMYK_Y != 0 {
8602 src_y
8603 } else {
8604 cur_y
8605 };
8606 let new_k = if channels & stet_graphics::device::CMYK_K != 0 {
8607 src_k
8608 } else {
8609 cur_k
8610 };
8611
8612 if !is_custom_spot {
8613 cmyk_buf[ci] = new_c as f32;
8614 cmyk_buf[ci + 1] = new_m as f32;
8615 cmyk_buf[ci + 2] = new_y as f32;
8616 cmyk_buf[ci + 3] = new_k as f32;
8617 }
8618
8619 let (r, g, b) = if use_multiplicative {
8620 let bg_r = px_data[pi] as f64 / 255.0;
8621 let bg_g = px_data[pi + 1] as f64 / 255.0;
8622 let bg_b = px_data[pi + 2] as f64 / 255.0;
8623 let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
8624 1.0 - src_c
8625 } else {
8626 1.0
8627 };
8628 let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
8629 1.0 - src_m
8630 } else {
8631 1.0
8632 };
8633 let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
8634 1.0 - src_y
8635 } else {
8636 1.0
8637 };
8638 let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
8639 1.0 - src_k
8640 } else {
8641 1.0
8642 };
8643 (
8644 (bg_r * over_r * k_fac).clamp(0.0, 1.0),
8645 (bg_g * over_g * k_fac).clamp(0.0, 1.0),
8646 (bg_b * over_b * k_fac).clamp(0.0, 1.0),
8647 )
8648 } else if let Some(icc_cache) = icc {
8649 icc_cache
8650 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
8651 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
8652 } else {
8653 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
8654 };
8655
8656 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
8659 op_bg[pi] = px_data[pi];
8660 op_bg[pi + 1] = px_data[pi + 1];
8661 op_bg[pi + 2] = px_data[pi + 2];
8662 op_bg[pi + 3] = px_data[pi + 3];
8663 op_touched[mi] = 1;
8664 }
8665
8666 px_data[pi] = (r * 255.0).round() as u8;
8667 px_data[pi + 1] = (g * 255.0).round() as u8;
8668 px_data[pi + 2] = (b * 255.0).round() as u8;
8669 px_data[pi + 3] = 255;
8670 }
8671 }
8672}
8673
8674#[allow(clippy::too_many_arguments)]
8684fn update_cmyk_buffer_for_image(
8685 cmyk_buf: &mut [f32],
8686 sample_data: &[u8],
8687 pixmap_rgba: &[u8],
8688 params: &ImageParams,
8689 vp_x: f32,
8690 vp_y: f32,
8691 scale_x: f32,
8692 scale_y: f32,
8693 out_w: u32,
8694 out_h: u32,
8695 clip_region: &Option<ClipRegion>,
8696 icc: Option<&IccCache>,
8697) {
8698 let iw = params.width as usize;
8699 let ih = params.height as usize;
8700 let Some(image_inv) = params.image_matrix.invert() else {
8701 return;
8702 };
8703 let combined = params.ctm.concat(&image_inv);
8704 let Some(inv_combined) = combined.invert() else {
8705 return;
8706 };
8707 let stride = out_w as usize;
8708 let inv_sx = 1.0 / scale_x as f64;
8709 let inv_sy = 1.0 / scale_y as f64;
8710
8711 let mask_info = if let ImageColorSpace::Mask {
8712 color, polarity, ..
8713 } = ¶ms.color_space
8714 {
8715 let Some((c, m, y, k)) = color.native_cmyk else {
8716 return;
8717 };
8718 Some((
8719 c as f32,
8720 m as f32,
8721 y as f32,
8722 k as f32,
8723 *polarity,
8724 iw.div_ceil(8),
8725 ))
8726 } else {
8727 None
8728 };
8729
8730 let clip_data: Option<&[u8]> = match clip_region {
8731 Some(ClipRegion::Mask(m)) => Some(m.data()),
8732 _ => None,
8733 };
8734 let clip_rect = match clip_region {
8735 Some(ClipRegion::Rect(r)) => Some(*r),
8736 _ => None,
8737 };
8738
8739 for by in 0..out_h as usize {
8740 for bx in 0..out_w as usize {
8741 if let Some(ref r) = clip_rect
8742 && ((by as u32) < r.y0
8743 || (by as u32) >= r.y1
8744 || (bx as u32) < r.x0
8745 || (bx as u32) >= r.x1)
8746 {
8747 continue;
8748 }
8749 if let Some(clip) = clip_data
8750 && clip[by * stride + bx] == 0
8751 {
8752 continue;
8753 }
8754
8755 let dx = (bx as f64 + 0.5) * inv_sx + vp_x as f64;
8756 let dy = (by as f64 + 0.5) * inv_sy + vp_y as f64;
8757 let ix = inv_combined.a * dx + inv_combined.c * dy + inv_combined.tx;
8758 let iy = inv_combined.b * dx + inv_combined.d * dy + inv_combined.ty;
8759
8760 let col = ix.floor() as i64;
8761 let row = iy.floor() as i64;
8762 if col < 0 || col >= iw as i64 || row < 0 || row >= ih as i64 {
8763 continue;
8764 }
8765 let col = col as usize;
8766 let row = row as usize;
8767
8768 let ci = (by * stride + bx) * 4;
8769 if let Some((sc, sm, sy, sk, polarity, bytes_per_row)) = mask_info {
8770 let byte_idx = row * bytes_per_row + col / 8;
8771 let bit_offset = 7 - (col % 8);
8772 let bit = if byte_idx < sample_data.len() {
8773 (sample_data[byte_idx] >> bit_offset) & 1
8774 } else {
8775 0
8776 };
8777 let paint = if polarity { bit == 1 } else { bit == 0 };
8778 if paint {
8779 cmyk_buf[ci] = sc;
8780 cmyk_buf[ci + 1] = sm;
8781 cmyk_buf[ci + 2] = sy;
8782 cmyk_buf[ci + 3] = sk;
8783 }
8784 } else if let Some((sc, sm, sy, sk)) =
8785 sample_pixel_cmyk(sample_data, ¶ms.color_space, iw, row, col)
8786 {
8787 cmyk_buf[ci] = sc as f32;
8788 cmyk_buf[ci + 1] = sm as f32;
8789 cmyk_buf[ci + 2] = sy as f32;
8790 cmyk_buf[ci + 3] = sk as f32;
8791 } else if ci + 3 < pixmap_rgba.len() && pixmap_rgba[ci + 3] > 0 {
8792 let r = pixmap_rgba[ci] as f64 / 255.0;
8796 let g = pixmap_rgba[ci + 1] as f64 / 255.0;
8797 let b = pixmap_rgba[ci + 2] as f64 / 255.0;
8798 let cmyk =
8799 if let Some(c) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(r, g, b)) {
8800 c
8801 } else {
8802 [
8803 (1.0 - r).clamp(0.0, 1.0),
8804 (1.0 - g).clamp(0.0, 1.0),
8805 (1.0 - b).clamp(0.0, 1.0),
8806 0.0,
8807 ]
8808 };
8809 cmyk_buf[ci] = cmyk[0] as f32;
8810 cmyk_buf[ci + 1] = cmyk[1] as f32;
8811 cmyk_buf[ci + 2] = cmyk[2] as f32;
8812 cmyk_buf[ci + 3] = cmyk[3] as f32;
8813 }
8814 }
8815 }
8816}
8817fn image_supports_overprint(cs: &ImageColorSpace) -> bool {
8821 use stet_graphics::device::cmyk_channel_for_name;
8822 match cs {
8823 ImageColorSpace::Mask { .. } => true,
8824 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. } => true,
8825 ImageColorSpace::Separation {
8826 alt_space, name, ..
8827 } => {
8828 matches!(
8829 alt_space.as_ref(),
8830 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8831 ) || cmyk_channel_for_name(name) != 0
8832 }
8833 ImageColorSpace::DeviceN {
8834 alt_space, names, ..
8835 } => {
8836 matches!(
8837 alt_space.as_ref(),
8838 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8839 ) || names.iter().any(|n| cmyk_channel_for_name(n) != 0)
8840 }
8841 ImageColorSpace::Indexed { base, .. } => image_supports_overprint(base),
8842 _ => false,
8843 }
8844}
8845
8846fn is_cmyk_color_space(cs: &ImageColorSpace) -> bool {
8848 match cs {
8849 ImageColorSpace::DeviceCMYK => true,
8850 ImageColorSpace::ICCBased { n: 4, .. } => true,
8851 ImageColorSpace::Indexed { base, .. } => is_cmyk_color_space(base),
8852 _ => false,
8853 }
8854}
8855
8856fn image_cs_has_spot_tint_transform(cs: &ImageColorSpace) -> bool {
8863 use stet_graphics::device::cmyk_channel_for_name;
8864 let is_cmyk_alt = |alt: &ImageColorSpace| {
8865 matches!(
8866 alt,
8867 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8868 )
8869 };
8870 match cs {
8871 ImageColorSpace::Separation {
8872 name, alt_space, ..
8873 } => cmyk_channel_for_name(name) == 0 && is_cmyk_alt(alt_space.as_ref()),
8874 ImageColorSpace::DeviceN {
8875 names, alt_space, ..
8876 } => {
8877 let has_spot = names.iter().any(|n| cmyk_channel_for_name(n) == 0);
8878 let has_process = names.iter().any(|n| cmyk_channel_for_name(n) != 0);
8879 has_spot && (is_cmyk_alt(alt_space.as_ref()) || has_process)
8880 }
8881 ImageColorSpace::Indexed { base, .. } => image_cs_has_spot_tint_transform(base),
8882 _ => false,
8883 }
8884}
8885
8886fn image_cs_alt_is_non_cmyk(cs: &ImageColorSpace) -> bool {
8892 let is_cmyk_alt = |alt: &ImageColorSpace| {
8893 matches!(
8894 alt,
8895 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8896 )
8897 };
8898 match cs {
8899 ImageColorSpace::Separation { alt_space, .. }
8900 | ImageColorSpace::DeviceN { alt_space, .. } => !is_cmyk_alt(alt_space.as_ref()),
8901 ImageColorSpace::Indexed { base, .. } => image_cs_alt_is_non_cmyk(base),
8902 _ => false,
8903 }
8904}
8905
8906fn sample_pixel_visual_rgb(
8911 sample_data: &[u8],
8912 cs: &ImageColorSpace,
8913 iw: usize,
8914 row: usize,
8915 col: usize,
8916) -> Option<(f64, f64, f64)> {
8917 let to_f64 = |(r, g, b): (u8, u8, u8)| (r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0);
8918 match cs {
8919 ImageColorSpace::Separation {
8920 alt_space,
8921 tint_table,
8922 ..
8923 } => {
8924 let si = row * iw + col;
8925 if si >= sample_data.len() {
8926 return None;
8927 }
8928 let tint = sample_data[si] as f32 / 255.0;
8929 let no = tint_table.num_outputs as usize;
8930 let mut comps = vec![0.0f32; no];
8931 tint_table.lookup_1d(tint, &mut comps);
8932 Some(to_f64(alt_comps_to_rgb(&comps, alt_space)))
8933 }
8934 ImageColorSpace::DeviceN {
8935 alt_space,
8936 tint_table,
8937 ..
8938 } => {
8939 let ni = tint_table.num_inputs as usize;
8940 let si = (row * iw + col) * ni;
8941 if si + ni > sample_data.len() {
8942 return None;
8943 }
8944 let mut inputs = vec![0.0f32; ni];
8945 for (c, inp) in inputs.iter_mut().enumerate() {
8946 *inp = sample_data[si + c] as f32 / 255.0;
8947 }
8948 let no = tint_table.num_outputs as usize;
8949 let mut comps = vec![0.0f32; no];
8950 tint_table.lookup_nd(&inputs, &mut comps);
8951 Some(to_f64(alt_comps_to_rgb(&comps, alt_space)))
8952 }
8953 ImageColorSpace::Indexed {
8954 base,
8955 hival,
8956 lookup,
8957 } => {
8958 let pi = row * iw + col;
8959 if pi >= sample_data.len() {
8960 return None;
8961 }
8962 let idx = (sample_data[pi] as usize).min(*hival as usize);
8963 let base_ncomp = base.num_components() as usize;
8964 let li = idx * base_ncomp;
8965 if li + base_ncomp > lookup.len() {
8966 return None;
8967 }
8968 match base.as_ref() {
8969 ImageColorSpace::Separation {
8970 alt_space,
8971 tint_table,
8972 ..
8973 } => {
8974 let tint = lookup[li] as f32 / 255.0;
8975 let no = tint_table.num_outputs as usize;
8976 let mut comps = vec![0.0f32; no];
8977 tint_table.lookup_1d(tint, &mut comps);
8978 Some(to_f64(alt_comps_to_rgb(&comps, alt_space)))
8979 }
8980 ImageColorSpace::DeviceN {
8981 alt_space,
8982 tint_table,
8983 ..
8984 } => {
8985 let ni = tint_table.num_inputs as usize;
8986 let mut inputs = vec![0.0f32; ni];
8987 for (c, inp) in inputs.iter_mut().enumerate() {
8988 if c < base_ncomp {
8989 *inp = lookup[li + c] as f32 / 255.0;
8990 }
8991 }
8992 let no = tint_table.num_outputs as usize;
8993 let mut comps = vec![0.0f32; no];
8994 tint_table.lookup_nd(&inputs, &mut comps);
8995 Some(to_f64(alt_comps_to_rgb(&comps, alt_space)))
8996 }
8997 _ => None,
8998 }
8999 }
9000 _ => None,
9001 }
9002}
9003
9004fn devicen_named_cmyk(names: &[Vec<u8>], inputs: &[u8]) -> (f64, f64, f64, f64) {
9010 use stet_graphics::device::{CMYK_C, CMYK_K, CMYK_M, CMYK_Y, cmyk_channel_for_name};
9011 let mut c = 0.0;
9012 let mut m = 0.0;
9013 let mut y = 0.0;
9014 let mut k = 0.0;
9015 for (i, name) in names.iter().enumerate() {
9016 let bit = cmyk_channel_for_name(name);
9017 if bit == 0 {
9018 continue;
9019 }
9020 let v = inputs.get(i).copied().unwrap_or(0) as f64 / 255.0;
9021 if bit & CMYK_C != 0 {
9022 c = v;
9023 }
9024 if bit & CMYK_M != 0 {
9025 m = v;
9026 }
9027 if bit & CMYK_Y != 0 {
9028 y = v;
9029 }
9030 if bit & CMYK_K != 0 {
9031 k = v;
9032 }
9033 }
9034 (c, m, y, k)
9035}
9036
9037fn sample_pixel_cmyk(
9042 sample_data: &[u8],
9043 cs: &ImageColorSpace,
9044 iw: usize,
9045 row: usize,
9046 col: usize,
9047) -> Option<(f64, f64, f64, f64)> {
9048 use stet_graphics::device::cmyk_channel_for_name;
9049 let is_cmyk_alt = |alt: &ImageColorSpace| {
9050 matches!(
9051 alt,
9052 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
9053 )
9054 };
9055 match cs {
9056 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. } => {
9057 let si = (row * iw + col) * 4;
9058 if si + 3 < sample_data.len() {
9059 Some((
9060 sample_data[si] as f64 / 255.0,
9061 sample_data[si + 1] as f64 / 255.0,
9062 sample_data[si + 2] as f64 / 255.0,
9063 sample_data[si + 3] as f64 / 255.0,
9064 ))
9065 } else {
9066 None
9067 }
9068 }
9069 ImageColorSpace::Separation {
9070 alt_space,
9071 tint_table,
9072 name,
9073 } => {
9074 let si = row * iw + col;
9075 if si >= sample_data.len() {
9076 return None;
9077 }
9078 let tint = sample_data[si] as f32 / 255.0;
9079 if is_cmyk_alt(alt_space.as_ref()) {
9080 let mut alt = [0.0f32; 4];
9081 tint_table.lookup_1d(tint, &mut alt);
9082 return Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64));
9083 }
9084 let bit = cmyk_channel_for_name(name);
9086 if bit == 0 {
9087 return None;
9088 }
9089 let names = vec![name.clone()];
9090 let inputs = [(tint * 255.0).round() as u8];
9091 Some(devicen_named_cmyk(&names, &inputs))
9092 }
9093 ImageColorSpace::DeviceN {
9094 alt_space,
9095 tint_table,
9096 names,
9097 } => {
9098 let ni = tint_table.num_inputs as usize;
9099 let si = (row * iw + col) * ni;
9100 if si + ni > sample_data.len() {
9101 return None;
9102 }
9103 if is_cmyk_alt(alt_space.as_ref()) {
9104 let mut inputs = vec![0.0f32; ni];
9105 for (c, inp) in inputs.iter_mut().enumerate() {
9106 *inp = sample_data[si + c] as f32 / 255.0;
9107 }
9108 let mut alt = [0.0f32; 4];
9109 tint_table.lookup_nd(&inputs, &mut alt);
9110 return Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64));
9111 }
9112 if !names.iter().any(|n| cmyk_channel_for_name(n) != 0) {
9114 return None;
9115 }
9116 Some(devicen_named_cmyk(names, &sample_data[si..si + ni]))
9117 }
9118 ImageColorSpace::Indexed {
9119 base,
9120 hival,
9121 lookup,
9122 } => {
9123 let pi = row * iw + col;
9124 if pi >= sample_data.len() {
9125 return None;
9126 }
9127 let idx = sample_data[pi] as usize;
9128 let idx = idx.min(*hival as usize);
9129 let base_ncomp = base.num_components() as usize;
9130 let li = idx * base_ncomp;
9131 if is_cmyk_color_space(base) && base_ncomp == 4 && li + 3 < lookup.len() {
9133 return Some((
9134 lookup[li] as f64 / 255.0,
9135 lookup[li + 1] as f64 / 255.0,
9136 lookup[li + 2] as f64 / 255.0,
9137 lookup[li + 3] as f64 / 255.0,
9138 ));
9139 }
9140 if li + base_ncomp <= lookup.len() {
9142 match base.as_ref() {
9143 ImageColorSpace::Separation {
9144 alt_space,
9145 tint_table,
9146 name,
9147 } => {
9148 let tint = lookup[li] as f32 / 255.0;
9149 if is_cmyk_alt(alt_space.as_ref()) {
9150 let mut alt = [0.0f32; 4];
9151 tint_table.lookup_1d(tint, &mut alt);
9152 return Some((
9153 alt[0] as f64,
9154 alt[1] as f64,
9155 alt[2] as f64,
9156 alt[3] as f64,
9157 ));
9158 }
9159 let bit = cmyk_channel_for_name(name);
9161 if bit == 0 {
9162 return None;
9163 }
9164 let names = vec![name.clone()];
9165 let inputs = [(tint * 255.0).round() as u8];
9166 return Some(devicen_named_cmyk(&names, &inputs));
9167 }
9168 ImageColorSpace::DeviceN {
9169 alt_space,
9170 tint_table,
9171 names,
9172 } => {
9173 let ni = tint_table.num_inputs as usize;
9174 if is_cmyk_alt(alt_space.as_ref()) {
9175 let mut inputs = vec![0.0f32; ni];
9176 for (c, inp) in inputs.iter_mut().enumerate() {
9177 if c < base_ncomp {
9178 *inp = lookup[li + c] as f32 / 255.0;
9179 }
9180 }
9181 let mut alt = [0.0f32; 4];
9182 tint_table.lookup_nd(&inputs, &mut alt);
9183 return Some((
9184 alt[0] as f64,
9185 alt[1] as f64,
9186 alt[2] as f64,
9187 alt[3] as f64,
9188 ));
9189 }
9190 if !names.iter().any(|n| cmyk_channel_for_name(n) != 0) {
9192 return None;
9193 }
9194 let take = ni.min(base_ncomp);
9195 return Some(devicen_named_cmyk(names, &lookup[li..li + take]));
9196 }
9197 _ => {}
9198 }
9199 }
9200 None
9201 }
9202 _ => None,
9203 }
9204}
9205#[allow(clippy::too_many_arguments)]
9211fn render_banded_to_sink(
9212 page_w: u32,
9213 page_h: u32,
9214 band_h: u32,
9215 dpi: f64,
9216 list: &DisplayList,
9217 sink: &mut dyn stet_graphics::device::PageSink,
9218 icc_cache: &IccCache,
9219 no_aa: bool,
9220 layer_set: &LayerSet,
9221) -> Result<(), String> {
9222 let bboxes = precompute_bboxes(list, dpi);
9224
9225 let epochs = build_clip_epochs(list, &bboxes);
9229
9230 let clip_seen = precompute_clip_seen(list);
9232
9233 use stet_graphics::display_list::GroupColorSpace;
9238 let needs_cmyk_buffer = has_overprint_elements(list)
9239 || list.page_group_color_space() == GroupColorSpace::DeviceCMYK
9240 || has_cmyk_group(list);
9241
9242 let preprocessed_images = preprocess_images_for_bands(list, Some(icc_cache));
9244
9245 const BAND_OVERLAP: u32 = 6;
9249
9250 let render_h = band_h + 2 * BAND_OVERLAP;
9251
9252 sink.begin_page(page_w, page_h)?;
9254
9255 let num_bands = page_h.div_ceil(band_h);
9256 let elements = list.elements();
9257 let row_bytes = page_w as usize * 4;
9258 let icc_ref = Some(icc_cache);
9259
9260 let render_band = |band_idx: u32| -> Vec<u8> {
9262 let y_start = band_idx * band_h;
9263 let actual_h = (page_h - y_start).min(band_h);
9264
9265 let render_y_start = y_start.saturating_sub(BAND_OVERLAP);
9266 let render_y_end_f = ((y_start + actual_h + BAND_OVERLAP).min(page_h)) as f64;
9267 let band_offset = y_start - render_y_start;
9268
9269 let mut band_pixmap = Pixmap::new(page_w, render_h).expect("Failed to create band pixmap");
9270 band_pixmap.as_mut().data_mut().fill(0x00);
9272
9273 let cmyk_buf = if needs_cmyk_buffer {
9274 Some(vec![0.0f32; page_w as usize * render_h as usize * 4])
9276 } else {
9277 None
9278 };
9279
9280 let mut band_state = BandState {
9281 clip_region: None,
9282 spare_mask: None,
9283 clip_mask_cache: HashMap::new(),
9284 clip_mask_seen: clip_seen.clone(),
9285 mask_pool: Vec::new(),
9286 cmyk_buffer: cmyk_buf,
9287 op_bg_snapshot: None,
9288 op_touched: None,
9289 spot_mask: None,
9290 };
9291
9292 for epoch in &epochs {
9294 if !epoch.has_erase_page {
9295 match epoch.paint_bbox {
9296 Some(ref pb)
9297 if pb.y_max <= render_y_start as f64 || pb.y_min >= render_y_end_f =>
9298 {
9299 continue;
9300 }
9301 None => continue,
9302 _ => {}
9303 }
9304 }
9305
9306 for i in epoch.start_idx..epoch.end_idx {
9307 let force_process = matches!(
9313 &elements[i],
9314 DisplayElement::OcgGroup { elements: inner, .. }
9315 if contains_clip_op(inner)
9316 );
9317 if !force_process
9318 && let Some(ref bbox) = bboxes[i]
9319 && (bbox.y_max <= render_y_start as f64 || bbox.y_min >= render_y_end_f)
9320 {
9321 continue;
9322 }
9323 let ctx = RenderContext {
9324 vp_x: 0.0,
9325 vp_y: render_y_start as f32,
9326 scale_x: 1.0,
9327 scale_y: 1.0,
9328 out_w: page_w,
9329 out_h: render_h,
9330 effective_dpi: dpi,
9331 icc: icc_ref,
9332 image_cache: None,
9333 preprocessed: Some(&preprocessed_images),
9334 elem_idx: i,
9335 no_aa,
9336 opm_zero_transparent: false,
9337 knockout_painter_pass: KnockoutPainterPass::None,
9338 parent_group_isolated: false,
9339 alpha_extraction_pass: false,
9340 layer_set,
9341 };
9342 render_element(&mut band_pixmap, &mut band_state, &elements[i], &ctx);
9343 }
9344 }
9345
9346 composite_onto_white(band_pixmap.data_mut());
9348
9349 let start_byte = band_offset as usize * row_bytes;
9351 let total_bytes = actual_h as usize * row_bytes;
9352 band_pixmap.data()[start_byte..start_byte + total_bytes].to_vec()
9353 };
9354
9355 #[cfg(feature = "parallel")]
9357 {
9358 let chunk_size = rayon::current_num_threads().max(1);
9363
9364 for chunk_start in (0..num_bands).step_by(chunk_size) {
9365 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
9366
9367 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
9368 .into_par_iter()
9369 .map(&render_band)
9370 .collect();
9371
9372 for (i, band_data) in rendered.iter().enumerate() {
9373 let band_idx = chunk_start + i as u32;
9374 let y_start = band_idx * band_h;
9375 let actual_h = (page_h - y_start).min(band_h);
9376 sink.write_rows(band_data, actual_h)?;
9377 }
9378 }
9379 }
9380 #[cfg(not(feature = "parallel"))]
9381 {
9382 for band_idx in 0..num_bands {
9384 let band_data = render_band(band_idx);
9385 let y_start = band_idx * band_h;
9386 let actual_h = (page_h - y_start).min(band_h);
9387 sink.write_rows(&band_data, actual_h)?;
9388 }
9389 }
9390
9391 sink.end_page()
9392}
9393
9394#[derive(Clone, Copy)]
9396struct BBox2D {
9397 x_min: f64,
9398 y_min: f64,
9399 x_max: f64,
9400 y_max: f64,
9401}
9402
9403fn precompute_full_bboxes(list: &DisplayList, dpi: f64) -> Vec<Option<BBox2D>> {
9405 list.elements()
9406 .iter()
9407 .map(|elem| match elem {
9408 DisplayElement::Fill { path, params } => fill_device_full_bbox(path, ¶ms.ctm),
9409 DisplayElement::Stroke { path, params } => {
9410 path_full_bbox(path).map(|mut bbox| {
9411 let effective_lw = params.line_width.max(hairline_min_width(¶ms.ctm, dpi));
9413 let expand = effective_lw * params.miter_limit * 0.5;
9414 let m = ¶ms.ctm;
9415 let is_identity = m.a == 1.0
9416 && m.b == 0.0
9417 && m.c == 0.0
9418 && m.d == 1.0
9419 && m.tx == 0.0
9420 && m.ty == 0.0;
9421 if is_identity {
9422 bbox.x_min -= expand;
9423 bbox.x_max += expand;
9424 bbox.y_min -= expand;
9425 bbox.y_max += expand;
9426 } else {
9427 let col_x_len = (m.a * m.a + m.b * m.b).sqrt().max(1.0);
9430 let col_y_len = (m.c * m.c + m.d * m.d).sqrt().max(1.0);
9431 let expand_x = effective_lw * col_x_len * params.miter_limit * 0.5;
9432 let expand_y = effective_lw * col_y_len * params.miter_limit * 0.5;
9433 bbox.x_min -= expand_x;
9434 bbox.x_max += expand_x;
9435 bbox.y_min -= expand_y;
9436 bbox.y_max += expand_y;
9437 let corners = [
9439 (
9440 m.a * bbox.x_min + m.c * bbox.y_min + m.tx,
9441 m.b * bbox.x_min + m.d * bbox.y_min + m.ty,
9442 ),
9443 (
9444 m.a * bbox.x_max + m.c * bbox.y_min + m.tx,
9445 m.b * bbox.x_max + m.d * bbox.y_min + m.ty,
9446 ),
9447 (
9448 m.a * bbox.x_min + m.c * bbox.y_max + m.tx,
9449 m.b * bbox.x_min + m.d * bbox.y_max + m.ty,
9450 ),
9451 (
9452 m.a * bbox.x_max + m.c * bbox.y_max + m.tx,
9453 m.b * bbox.x_max + m.d * bbox.y_max + m.ty,
9454 ),
9455 ];
9456 bbox.x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
9457 bbox.x_max = corners
9458 .iter()
9459 .map(|c| c.0)
9460 .fold(f64::NEG_INFINITY, f64::max);
9461 bbox.y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
9462 bbox.y_max = corners
9463 .iter()
9464 .map(|c| c.1)
9465 .fold(f64::NEG_INFINITY, f64::max);
9466 }
9467 bbox
9468 })
9469 }
9470 DisplayElement::Image { params, .. } => image_full_bbox(params),
9471 DisplayElement::AxialShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9472 DisplayElement::RadialShading { params } => {
9473 shading_full_bbox(¶ms.bbox, ¶ms.ctm)
9474 }
9475 DisplayElement::MeshShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9476 DisplayElement::PatchShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9477 DisplayElement::PatternFill { params } => pattern_fill_full_bbox(params),
9478 DisplayElement::Group { params, .. } => Some(BBox2D {
9479 x_min: params.bbox[0],
9480 y_min: params.bbox[1],
9481 x_max: params.bbox[2],
9482 y_max: params.bbox[3],
9483 }),
9484 DisplayElement::SoftMasked { params, .. } => Some(BBox2D {
9485 x_min: params.bbox[0],
9486 y_min: params.bbox[1],
9487 x_max: params.bbox[2],
9488 y_max: params.bbox[3],
9489 }),
9490 DisplayElement::OcgGroup {
9491 elements,
9492 visibility,
9493 } => {
9494 if !visibility.default_visible() && !contains_clip_op(elements) {
9499 return None;
9500 }
9501 let child_bboxes = precompute_full_bboxes(elements, dpi);
9502 let mut x_min = f64::INFINITY;
9503 let mut y_min = f64::INFINITY;
9504 let mut x_max = f64::NEG_INFINITY;
9505 let mut y_max = f64::NEG_INFINITY;
9506 for cb in child_bboxes.into_iter().flatten() {
9507 x_min = x_min.min(cb.x_min);
9508 y_min = y_min.min(cb.y_min);
9509 x_max = x_max.max(cb.x_max);
9510 y_max = y_max.max(cb.y_max);
9511 }
9512 if x_min <= x_max && y_min <= y_max {
9513 Some(BBox2D {
9514 x_min,
9515 y_min,
9516 x_max,
9517 y_max,
9518 })
9519 } else {
9520 None
9521 }
9522 }
9523 _ => None, })
9525 .collect()
9526}
9527
9528fn clip_path_bbox(path: &PsPath, params: &ClipParams) -> Option<BBox2D> {
9537 let mut bbox = path_full_bbox(path)?;
9538 let ctm = ¶ms.ctm;
9539 let is_identity = ctm.a == 1.0
9540 && ctm.b == 0.0
9541 && ctm.c == 0.0
9542 && ctm.d == 1.0
9543 && ctm.tx == 0.0
9544 && ctm.ty == 0.0;
9545 if !is_identity {
9546 let corners = [
9547 ctm.transform_point(bbox.x_min, bbox.y_min),
9548 ctm.transform_point(bbox.x_max, bbox.y_min),
9549 ctm.transform_point(bbox.x_min, bbox.y_max),
9550 ctm.transform_point(bbox.x_max, bbox.y_max),
9551 ];
9552 bbox.x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
9553 bbox.x_max = corners
9554 .iter()
9555 .map(|c| c.0)
9556 .fold(f64::NEG_INFINITY, f64::max);
9557 bbox.y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
9558 bbox.y_max = corners
9559 .iter()
9560 .map(|c| c.1)
9561 .fold(f64::NEG_INFINITY, f64::max);
9562 }
9563 if let Some(sp) = ¶ms.stroke_params {
9564 let scale = (ctm.a * ctm.a + ctm.b * ctm.b)
9565 .sqrt()
9566 .max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt())
9567 .max(1.0);
9568 let expand = sp.line_width * 0.5 * scale;
9569 bbox.x_min -= expand;
9570 bbox.x_max += expand;
9571 bbox.y_min -= expand;
9572 bbox.y_max += expand;
9573 }
9574 Some(bbox)
9575}
9576
9577fn intersect_bbox(a: &BBox2D, b: &BBox2D) -> Option<BBox2D> {
9579 let x_min = a.x_min.max(b.x_min);
9580 let y_min = a.y_min.max(b.y_min);
9581 let x_max = a.x_max.min(b.x_max);
9582 let y_max = a.y_max.min(b.y_max);
9583 if x_min < x_max && y_min < y_max {
9584 Some(BBox2D {
9585 x_min,
9586 y_min,
9587 x_max,
9588 y_max,
9589 })
9590 } else {
9591 None
9592 }
9593}
9594
9595fn compute_paint_bounds(list: &DisplayList, _dpi: f64) -> Option<BBox2D> {
9616 let mut clip_stack: Vec<BBox2D> = Vec::new();
9620 let mut union: Option<BBox2D> = None;
9621
9622 let push_paint = |union: &mut Option<BBox2D>, clip_stack: &[BBox2D], bbox: BBox2D| {
9623 let visible = match clip_stack.last() {
9627 Some(clip) => match intersect_bbox(clip, &bbox) {
9628 Some(b) => b,
9629 None => return,
9630 },
9631 None => bbox,
9632 };
9633 *union = Some(match union.take() {
9634 None => visible,
9635 Some(u) => BBox2D {
9636 x_min: u.x_min.min(visible.x_min),
9637 y_min: u.y_min.min(visible.y_min),
9638 x_max: u.x_max.max(visible.x_max),
9639 y_max: u.y_max.max(visible.y_max),
9640 },
9641 });
9642 };
9643
9644 for elem in list.elements() {
9645 match elem {
9646 DisplayElement::Clip { path, params } => {
9647 if let Some(cb) = clip_path_bbox(path, params) {
9648 let new_top = match clip_stack.last() {
9649 Some(prev) => match intersect_bbox(prev, &cb) {
9650 Some(b) => b,
9651 None => BBox2D {
9655 x_min: 0.0,
9656 y_min: 0.0,
9657 x_max: 0.0,
9658 y_max: 0.0,
9659 },
9660 },
9661 None => cb,
9662 };
9663 clip_stack.push(new_top);
9664 }
9665 }
9666 DisplayElement::InitClip | DisplayElement::ErasePage => {
9667 clip_stack.clear();
9668 }
9669 DisplayElement::Fill { path, .. } => {
9670 if let Some(b) = path_full_bbox(path) {
9671 push_paint(&mut union, &clip_stack, b);
9672 }
9673 }
9674 DisplayElement::Stroke { path, params } => {
9675 if let Some(mut b) = path_full_bbox(path) {
9676 let expand = params.line_width * params.miter_limit * 0.5;
9677 b.x_min -= expand;
9678 b.x_max += expand;
9679 b.y_min -= expand;
9680 b.y_max += expand;
9681 push_paint(&mut union, &clip_stack, b);
9682 }
9683 }
9684 DisplayElement::Image { params, .. } => {
9685 if let Some(b) = image_full_bbox(params) {
9686 push_paint(&mut union, &clip_stack, b);
9687 }
9688 }
9689 DisplayElement::AxialShading { params } => {
9690 let b = match ¶ms.bbox {
9691 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9692 None => clip_stack.last().copied(),
9693 };
9694 if let Some(b) = b {
9695 push_paint(&mut union, &clip_stack, b);
9696 }
9697 }
9698 DisplayElement::RadialShading { params } => {
9699 let b = match ¶ms.bbox {
9700 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9701 None => clip_stack.last().copied(),
9702 };
9703 if let Some(b) = b {
9704 push_paint(&mut union, &clip_stack, b);
9705 }
9706 }
9707 DisplayElement::MeshShading { params } => {
9708 let b = match ¶ms.bbox {
9709 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9710 None => clip_stack.last().copied(),
9711 };
9712 if let Some(b) = b {
9713 push_paint(&mut union, &clip_stack, b);
9714 }
9715 }
9716 DisplayElement::PatchShading { params } => {
9717 let b = match ¶ms.bbox {
9718 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9719 None => clip_stack.last().copied(),
9720 };
9721 if let Some(b) = b {
9722 push_paint(&mut union, &clip_stack, b);
9723 }
9724 }
9725 DisplayElement::PatternFill { params } => {
9726 if let Some(b) = pattern_fill_full_bbox(params) {
9727 push_paint(&mut union, &clip_stack, b);
9728 }
9729 }
9730 DisplayElement::Group { params, .. } => {
9731 push_paint(
9732 &mut union,
9733 &clip_stack,
9734 BBox2D {
9735 x_min: params.bbox[0],
9736 y_min: params.bbox[1],
9737 x_max: params.bbox[2],
9738 y_max: params.bbox[3],
9739 },
9740 );
9741 }
9742 DisplayElement::SoftMasked { params, .. } => {
9743 push_paint(
9744 &mut union,
9745 &clip_stack,
9746 BBox2D {
9747 x_min: params.bbox[0],
9748 y_min: params.bbox[1],
9749 x_max: params.bbox[2],
9750 y_max: params.bbox[3],
9751 },
9752 );
9753 }
9754 DisplayElement::Text { .. } => {} DisplayElement::OcgGroup { .. } => {
9756 }
9761 _ => {}
9762 }
9763 }
9764 union
9765}
9766
9767fn fill_device_full_bbox(path: &PsPath, ctm: &Matrix) -> Option<BBox2D> {
9772 let bbox = path_full_bbox(path)?;
9773 let is_identity = ctm.a == 1.0
9774 && ctm.b == 0.0
9775 && ctm.c == 0.0
9776 && ctm.d == 1.0
9777 && ctm.tx == 0.0
9778 && ctm.ty == 0.0;
9779 if is_identity {
9780 return Some(bbox);
9781 }
9782 let corners = [
9783 (bbox.x_min, bbox.y_min),
9784 (bbox.x_max, bbox.y_min),
9785 (bbox.x_min, bbox.y_max),
9786 (bbox.x_max, bbox.y_max),
9787 ];
9788 let mut x_min = f64::INFINITY;
9789 let mut x_max = f64::NEG_INFINITY;
9790 let mut y_min = f64::INFINITY;
9791 let mut y_max = f64::NEG_INFINITY;
9792 for (x, y) in &corners {
9793 let dx = ctm.a * x + ctm.c * y + ctm.tx;
9794 let dy = ctm.b * x + ctm.d * y + ctm.ty;
9795 x_min = x_min.min(dx);
9796 x_max = x_max.max(dx);
9797 y_min = y_min.min(dy);
9798 y_max = y_max.max(dy);
9799 }
9800 Some(BBox2D {
9801 x_min,
9802 y_min,
9803 x_max,
9804 y_max,
9805 })
9806}
9807
9808fn path_full_bbox(path: &PsPath) -> Option<BBox2D> {
9809 let mut x_min = f64::INFINITY;
9810 let mut x_max = f64::NEG_INFINITY;
9811 let mut y_min = f64::INFINITY;
9812 let mut y_max = f64::NEG_INFINITY;
9813 for seg in &path.segments {
9814 match seg {
9815 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => {
9816 x_min = x_min.min(*x);
9817 x_max = x_max.max(*x);
9818 y_min = y_min.min(*y);
9819 y_max = y_max.max(*y);
9820 }
9821 PathSegment::CurveTo {
9822 x1,
9823 y1,
9824 x2,
9825 y2,
9826 x3,
9827 y3,
9828 } => {
9829 x_min = x_min.min(*x1).min(*x2).min(*x3);
9830 x_max = x_max.max(*x1).max(*x2).max(*x3);
9831 y_min = y_min.min(*y1).min(*y2).min(*y3);
9832 y_max = y_max.max(*y1).max(*y2).max(*y3);
9833 }
9834 PathSegment::ClosePath => {}
9835 }
9836 }
9837 if x_min <= x_max {
9838 Some(BBox2D {
9839 x_min,
9840 y_min,
9841 x_max,
9842 y_max,
9843 })
9844 } else {
9845 None
9846 }
9847}
9848
9849fn pattern_fill_full_bbox(params: &stet_graphics::device::PatternFillParams) -> Option<BBox2D> {
9854 if let Some(ref sp) = params.stroke_params {
9855 let bbox = path_full_bbox(¶ms.path)?;
9856 let ctm = &sp.ctm;
9857 let corners = [
9858 ctm.transform_point(bbox.x_min, bbox.y_min),
9859 ctm.transform_point(bbox.x_max, bbox.y_min),
9860 ctm.transform_point(bbox.x_min, bbox.y_max),
9861 ctm.transform_point(bbox.x_max, bbox.y_max),
9862 ];
9863 let mut dev_bbox = BBox2D {
9864 x_min: f64::INFINITY,
9865 y_min: f64::INFINITY,
9866 x_max: f64::NEG_INFINITY,
9867 y_max: f64::NEG_INFINITY,
9868 };
9869 for (x, y) in &corners {
9870 dev_bbox.x_min = dev_bbox.x_min.min(*x);
9871 dev_bbox.y_min = dev_bbox.y_min.min(*y);
9872 dev_bbox.x_max = dev_bbox.x_max.max(*x);
9873 dev_bbox.y_max = dev_bbox.y_max.max(*y);
9874 }
9875 let half_w = sp.line_width
9876 * 0.5
9877 * (ctm.a * ctm.a + ctm.b * ctm.b)
9878 .sqrt()
9879 .max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt());
9880 dev_bbox.x_min -= half_w;
9881 dev_bbox.y_min -= half_w;
9882 dev_bbox.x_max += half_w;
9883 dev_bbox.y_max += half_w;
9884 Some(dev_bbox)
9885 } else {
9886 path_full_bbox(¶ms.path)
9887 }
9888}
9889
9890fn pattern_fill_y_bbox(params: &stet_graphics::device::PatternFillParams) -> Option<YBBox> {
9892 let bbox = pattern_fill_full_bbox(params)?;
9893 Some(YBBox {
9894 y_min: bbox.y_min,
9895 y_max: bbox.y_max,
9896 })
9897}
9898
9899fn image_full_bbox(params: &ImageParams) -> Option<BBox2D> {
9901 let m = ¶ms.ctm;
9902 let im = ¶ms.image_matrix;
9903 let im_inv = im.invert()?;
9904 let combined = m.concat(&im_inv);
9905 let w = params.width as f64;
9907 let h = params.height as f64;
9908 let corners = [
9909 combined.transform_point(0.0, 0.0),
9910 combined.transform_point(w, 0.0),
9911 combined.transform_point(0.0, h),
9912 combined.transform_point(w, h),
9913 ];
9914 let mut x_min = f64::INFINITY;
9915 let mut x_max = f64::NEG_INFINITY;
9916 let mut y_min = f64::INFINITY;
9917 let mut y_max = f64::NEG_INFINITY;
9918 for (x, y) in &corners {
9919 x_min = x_min.min(*x);
9920 x_max = x_max.max(*x);
9921 y_min = y_min.min(*y);
9922 y_max = y_max.max(*y);
9923 }
9924 Some(BBox2D {
9925 x_min,
9926 y_min,
9927 x_max,
9928 y_max,
9929 })
9930}
9931
9932fn shading_full_bbox(bbox: &Option<[f64; 4]>, ctm: &Matrix) -> Option<BBox2D> {
9934 if let Some(bbox) = bbox {
9935 let corners = [
9936 ctm.transform_point(bbox[0], bbox[1]),
9937 ctm.transform_point(bbox[2], bbox[1]),
9938 ctm.transform_point(bbox[0], bbox[3]),
9939 ctm.transform_point(bbox[2], bbox[3]),
9940 ];
9941 let mut x_min = f64::INFINITY;
9942 let mut x_max = f64::NEG_INFINITY;
9943 let mut y_min = f64::INFINITY;
9944 let mut y_max = f64::NEG_INFINITY;
9945 for (x, y) in &corners {
9946 x_min = x_min.min(*x);
9947 x_max = x_max.max(*x);
9948 y_min = y_min.min(*y);
9949 y_max = y_max.max(*y);
9950 }
9951 Some(BBox2D {
9952 x_min,
9953 y_min,
9954 x_max,
9955 y_max,
9956 })
9957 } else {
9958 Some(BBox2D {
9959 x_min: 0.0,
9960 y_min: 0.0,
9961 x_max: 1e9,
9962 y_max: 1e9,
9963 })
9964 }
9965}
9966
9967fn build_viewport_epochs(list: &DisplayList, bboxes: &[Option<BBox2D>]) -> Vec<ViewportEpoch> {
9969 let elements = list.elements();
9970 let mut epochs = Vec::new();
9971 let mut epoch_start = 0;
9972 let mut x_min = f64::INFINITY;
9973 let mut x_max = f64::NEG_INFINITY;
9974 let mut y_min = f64::INFINITY;
9975 let mut y_max = f64::NEG_INFINITY;
9976 let mut has_erase = false;
9977
9978 for (i, element) in elements.iter().enumerate() {
9979 if matches!(element, DisplayElement::InitClip) && i > epoch_start {
9980 epochs.push(ViewportEpoch {
9981 start_idx: epoch_start,
9982 end_idx: i,
9983 paint_bbox: if x_min <= x_max {
9984 Some(BBox2D {
9985 x_min,
9986 y_min,
9987 x_max,
9988 y_max,
9989 })
9990 } else {
9991 None
9992 },
9993 has_erase_page: has_erase,
9994 });
9995 epoch_start = i;
9996 x_min = f64::INFINITY;
9997 x_max = f64::NEG_INFINITY;
9998 y_min = f64::INFINITY;
9999 y_max = f64::NEG_INFINITY;
10000 has_erase = false;
10001 }
10002 if matches!(element, DisplayElement::ErasePage) {
10003 has_erase = true;
10004 }
10005 if let Some(ref bbox) = bboxes[i] {
10006 x_min = x_min.min(bbox.x_min);
10007 x_max = x_max.max(bbox.x_max);
10008 y_min = y_min.min(bbox.y_min);
10009 y_max = y_max.max(bbox.y_max);
10010 }
10011 }
10012 if epoch_start < elements.len() {
10013 epochs.push(ViewportEpoch {
10014 start_idx: epoch_start,
10015 end_idx: elements.len(),
10016 paint_bbox: if x_min <= x_max {
10017 Some(BBox2D {
10018 x_min,
10019 y_min,
10020 x_max,
10021 y_max,
10022 })
10023 } else {
10024 None
10025 },
10026 has_erase_page: has_erase,
10027 });
10028 }
10029 epochs
10030}
10031
10032struct ViewportEpoch {
10034 start_idx: usize,
10035 end_idx: usize,
10036 paint_bbox: Option<BBox2D>,
10037 has_erase_page: bool,
10038}
10039
10040pub struct PreparedDisplayList {
10046 bboxes: Vec<Option<BBox2D>>,
10047 epochs: Vec<ViewportEpoch>,
10048 clip_seen: HashSet<u64>,
10049}
10050
10051pub fn prepare_display_list(list: &DisplayList) -> PreparedDisplayList {
10056 let bboxes = precompute_full_bboxes(list, 72.0);
10057 let epochs = build_viewport_epochs(list, &bboxes);
10058 let clip_seen = precompute_clip_seen(list);
10059 PreparedDisplayList {
10060 bboxes,
10061 epochs,
10062 clip_seen,
10063 }
10064}
10065
10066struct PreprocessedImage {
10071 data: Vec<u8>,
10073 width: u32,
10075 height: u32,
10076 adj_sx: f32,
10080 adj_ky: f32,
10081 adj_kx: f32,
10082 adj_sy: f32,
10083 quality: stet_tiny_skia::FilterQuality,
10085}
10086
10087pub struct ImageCache {
10093 entries: Vec<Option<Vec<u8>>>,
10095}
10096
10097impl ImageCache {
10098 pub fn build(list: &DisplayList, icc: Option<&IccCache>) -> Self {
10100 let entries = list
10101 .elements()
10102 .iter()
10103 .map(|elem| {
10104 if let DisplayElement::Image {
10105 sample_data,
10106 params,
10107 } = elem
10108 {
10109 if params.width == 0 || params.height == 0 {
10110 return None;
10111 }
10112 let mut rgba = samples_to_rgba(sample_data, params, icc, false);
10113 if params.mask_color.is_some() {
10114 apply_mask_color_rgba(&mut rgba, sample_data, params);
10115 }
10116 Some(rgba)
10117 } else {
10118 None
10119 }
10120 })
10121 .collect();
10122 Self { entries }
10123 }
10124
10125 pub fn get(&self, index: usize) -> Option<&[u8]> {
10127 self.entries.get(index).and_then(|e| e.as_deref())
10128 }
10129}
10130
10131fn preprocess_images_for_bands(
10136 list: &DisplayList,
10137 icc: Option<&IccCache>,
10138) -> Vec<Option<PreprocessedImage>> {
10139 list.elements()
10140 .iter()
10141 .map(|elem| {
10142 let DisplayElement::Image {
10143 sample_data,
10144 params,
10145 } = elem
10146 else {
10147 return None;
10148 };
10149 let iw = params.width;
10150 let ih = params.height;
10151 if iw == 0 || ih == 0 {
10152 return None;
10153 }
10154 if params.overprint {
10156 return None;
10157 }
10158
10159 let mut rgba = samples_to_rgba(sample_data, params, icc, false);
10161 if params.mask_color.is_some() {
10162 apply_mask_color_rgba(&mut rgba, sample_data, params);
10163 }
10164
10165 let image_inv = params.image_matrix.invert()?;
10167 let combined = params.ctm.concat(&image_inv);
10168 let base_transform = enforce_min_image_size(to_transform(&combined), iw, ih);
10169
10170 let (data, width, height, adj_t) =
10172 match prescale_image(&rgba, iw, ih, base_transform, params.interpolate) {
10173 Some((d, w, h, t)) => {
10174 drop(rgba); (d, w, h, t)
10176 }
10177 None => (rgba, iw, ih, base_transform),
10178 };
10179
10180 let quality = image_filter_quality(adj_t, params.interpolate);
10181
10182 Some(PreprocessedImage {
10183 data,
10184 width,
10185 height,
10186 adj_sx: adj_t.sx,
10187 adj_ky: adj_t.ky,
10188 adj_kx: adj_t.kx,
10189 adj_sy: adj_t.sy,
10190 quality,
10191 })
10192 })
10193 .collect()
10194}
10195
10196#[allow(clippy::too_many_arguments)]
10202pub fn render_region_prepared(
10203 list: &DisplayList,
10204 prepared: &PreparedDisplayList,
10205 vp_x: f64,
10206 vp_y: f64,
10207 vp_w: f64,
10208 vp_h: f64,
10209 pixel_w: u32,
10210 pixel_h: u32,
10211 dpi: f64,
10212 icc: Option<&IccCache>,
10213 image_cache: Option<&ImageCache>,
10214 no_aa: bool,
10215) -> Vec<u8> {
10216 if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
10217 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10218 }
10219
10220 let layer_set = LayerSet::new();
10221 let scale_x = pixel_w as f64 / vp_w;
10222 let scale_y = pixel_h as f64 / vp_h;
10223 let effective_dpi = dpi * scale_x;
10224
10225 const OVERLAP: u32 = 6;
10236 let render_h = pixel_h + 2 * OVERLAP;
10237 let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create viewport pixmap");
10238 pixmap.fill(Color::TRANSPARENT);
10240
10241 let cmyk_buf = if has_overprint_elements(list)
10242 || list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
10243 || has_cmyk_group(list)
10244 {
10245 Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
10246 } else {
10247 None
10248 };
10249
10250 let mut state = BandState {
10251 clip_region: None,
10252 spare_mask: None,
10253 clip_mask_cache: HashMap::new(),
10254 clip_mask_seen: prepared.clip_seen.clone(),
10255 mask_pool: Vec::new(),
10256 cmyk_buffer: cmyk_buf,
10257 op_bg_snapshot: None,
10258 op_touched: None,
10259 spot_mask: None,
10260 };
10261
10262 let elements = list.elements();
10263 let vp_x_f = vp_x as f32;
10264 let vp_y_f = vp_y as f32;
10265 let sx = scale_x as f32;
10266 let sy = scale_y as f32;
10267 let vp_x_max = vp_x + vp_w;
10268 let vp_y_max = vp_y + vp_h;
10269
10270 for epoch in &prepared.epochs {
10271 if !epoch.has_erase_page {
10272 match epoch.paint_bbox {
10273 Some(ref pb)
10274 if pb.x_max <= vp_x
10275 || pb.x_min >= vp_x_max
10276 || pb.y_max <= vp_y
10277 || pb.y_min >= vp_y_max =>
10278 {
10279 continue;
10280 }
10281 None => continue,
10282 _ => {}
10283 }
10284 }
10285
10286 #[allow(clippy::needless_range_loop)]
10287 for i in epoch.start_idx..epoch.end_idx {
10288 let force_process = matches!(
10291 &elements[i],
10292 DisplayElement::OcgGroup { elements: inner, .. }
10293 if contains_clip_op(inner)
10294 );
10295 if !force_process
10296 && let Some(ref bbox) = prepared.bboxes[i]
10297 && (bbox.x_max <= vp_x
10298 || bbox.x_min >= vp_x_max
10299 || bbox.y_max <= vp_y
10300 || bbox.y_min >= vp_y_max)
10301 {
10302 continue;
10303 }
10304 let ctx = RenderContext {
10305 vp_x: vp_x_f,
10306 vp_y: vp_y_f,
10307 scale_x: sx,
10308 scale_y: sy,
10309 out_w: pixel_w,
10310 out_h: render_h,
10311 effective_dpi,
10312 icc,
10313 image_cache,
10314 preprocessed: None,
10315 elem_idx: i,
10316 no_aa,
10317 opm_zero_transparent: false,
10318 knockout_painter_pass: KnockoutPainterPass::None,
10319 parent_group_isolated: false,
10320 alpha_extraction_pass: false,
10321 layer_set: &layer_set,
10322 };
10323 render_element(&mut pixmap, &mut state, &elements[i], &ctx);
10324 }
10325 }
10326
10327 composite_onto_white(pixmap.data_mut());
10329 let row_bytes = pixel_w as usize * 4;
10331 let end = pixel_h as usize * row_bytes;
10332 pixmap.data()[..end].to_vec()
10333}
10334
10335pub fn viewport_band_count(pixel_w: u32, pixel_h: u32) -> (u32, u32) {
10340 let band_h = select_band_height(pixel_w, pixel_h);
10341 let num_bands = if band_h >= pixel_h {
10342 1
10343 } else {
10344 pixel_h.div_ceil(band_h)
10345 };
10346 (num_bands, band_h)
10347}
10348
10349#[allow(clippy::too_many_arguments)]
10358pub fn render_region_single_band(
10359 list: &DisplayList,
10360 prepared: &PreparedDisplayList,
10361 vp_x: f64,
10362 vp_y: f64,
10363 vp_w: f64,
10364 vp_h: f64,
10365 pixel_w: u32,
10366 pixel_h: u32,
10367 band_idx: u32,
10368 band_h: u32,
10369 num_bands: u32,
10370 dpi: f64,
10371 icc: Option<&IccCache>,
10372 image_cache: Option<&ImageCache>,
10373 no_aa: bool,
10374) -> Vec<u8> {
10375 if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
10376 let actual_h = if band_idx < num_bands - 1 {
10377 band_h
10378 } else {
10379 pixel_h - band_idx * band_h
10380 };
10381 return vec![0xFF; pixel_w as usize * actual_h as usize * 4];
10382 }
10383
10384 let layer_set = LayerSet::new();
10385 let scale_x = pixel_w as f64 / vp_w;
10386 let scale_y = pixel_h as f64 / vp_h;
10387 let effective_dpi = dpi * scale_x;
10388
10389 let out_y_start = band_idx * band_h;
10391 let actual_h = if band_idx < num_bands - 1 {
10392 band_h
10393 } else {
10394 pixel_h - out_y_start
10395 };
10396
10397 const OVERLAP: u32 = 6;
10408 let render_y_start = out_y_start.saturating_sub(OVERLAP);
10409 let render_y_end = (out_y_start + actual_h + OVERLAP).min(pixel_h);
10410 let render_h = band_h + 2 * OVERLAP;
10411 let overlap_top = out_y_start - render_y_start;
10412
10413 let src_y_min = vp_y + render_y_start as f64 / scale_y;
10415 let src_y_max = vp_y + render_y_end as f64 / scale_y;
10416
10417 let band_vp_y = vp_y + render_y_start as f64 / scale_y;
10419
10420 let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create band pixmap");
10421 pixmap.fill(Color::TRANSPARENT);
10422
10423 let cmyk_buf = if has_overprint_elements(list)
10424 || list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
10425 || has_cmyk_group(list)
10426 {
10427 Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
10428 } else {
10429 None
10430 };
10431
10432 let mut state = BandState {
10433 clip_region: None,
10434 spare_mask: None,
10435 clip_mask_cache: HashMap::new(),
10436 clip_mask_seen: prepared.clip_seen.clone(),
10437 mask_pool: Vec::new(),
10438 cmyk_buffer: cmyk_buf,
10439 op_bg_snapshot: None,
10440 op_touched: None,
10441 spot_mask: None,
10442 };
10443
10444 let elements = list.elements();
10445 let vp_x_f = vp_x as f32;
10446 let band_vp_y_f = band_vp_y as f32;
10447 let sx = scale_x as f32;
10448 let sy = scale_y as f32;
10449 let vp_x_max = vp_x + vp_w;
10450
10451 for epoch in &prepared.epochs {
10452 if !epoch.has_erase_page {
10453 match epoch.paint_bbox {
10454 Some(ref pb)
10455 if pb.x_max <= vp_x
10456 || pb.x_min >= vp_x_max
10457 || pb.y_max <= src_y_min
10458 || pb.y_min >= src_y_max =>
10459 {
10460 continue;
10461 }
10462 None => continue,
10463 _ => {}
10464 }
10465 }
10466
10467 #[allow(clippy::needless_range_loop)]
10468 for i in epoch.start_idx..epoch.end_idx {
10469 let force_process = matches!(
10473 &elements[i],
10474 DisplayElement::OcgGroup { elements: inner, .. }
10475 if contains_clip_op(inner)
10476 );
10477 if !force_process
10478 && let Some(ref bbox) = prepared.bboxes[i]
10479 && (bbox.x_max <= vp_x
10480 || bbox.x_min >= vp_x_max
10481 || bbox.y_max <= src_y_min
10482 || bbox.y_min >= src_y_max)
10483 {
10484 continue;
10485 }
10486 let ctx = RenderContext {
10487 vp_x: vp_x_f,
10488 vp_y: band_vp_y_f,
10489 scale_x: sx,
10490 scale_y: sy,
10491 out_w: pixel_w,
10492 out_h: render_h,
10493 effective_dpi,
10494 icc,
10495 image_cache,
10496 preprocessed: None,
10497 elem_idx: i,
10498 no_aa,
10499 opm_zero_transparent: false,
10500 knockout_painter_pass: KnockoutPainterPass::None,
10501 parent_group_isolated: false,
10502 alpha_extraction_pass: false,
10503 layer_set: &layer_set,
10504 };
10505 render_element(&mut pixmap, &mut state, &elements[i], &ctx);
10506 }
10507 }
10508
10509 composite_onto_white(pixmap.data_mut());
10511
10512 let row_bytes = pixel_w as usize * 4;
10514 let start = overlap_top as usize * row_bytes;
10515 let end = start + actual_h as usize * row_bytes;
10516 pixmap.data()[start..end].to_vec()
10517}
10518
10519#[allow(clippy::too_many_arguments)]
10528pub fn render_region_prepared_parallel(
10529 list: &DisplayList,
10530 prepared: &PreparedDisplayList,
10531 vp_x: f64,
10532 vp_y: f64,
10533 vp_w: f64,
10534 vp_h: f64,
10535 pixel_w: u32,
10536 pixel_h: u32,
10537 dpi: f64,
10538 icc: Option<&IccCache>,
10539 image_cache: Option<&ImageCache>,
10540 no_aa: bool,
10541) -> Vec<u8> {
10542 let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
10543
10544 if num_bands <= 1 {
10545 return render_region_prepared(
10547 list,
10548 prepared,
10549 vp_x,
10550 vp_y,
10551 vp_w,
10552 vp_h,
10553 pixel_w,
10554 pixel_h,
10555 dpi,
10556 icc,
10557 image_cache,
10558 no_aa,
10559 );
10560 }
10561
10562 let render_band = |band_idx: u32| -> Vec<u8> {
10563 render_region_single_band(
10564 list,
10565 prepared,
10566 vp_x,
10567 vp_y,
10568 vp_w,
10569 vp_h,
10570 pixel_w,
10571 pixel_h,
10572 band_idx,
10573 band_h,
10574 num_bands,
10575 dpi,
10576 icc,
10577 image_cache,
10578 no_aa,
10579 )
10580 };
10581
10582 let row_bytes = pixel_w as usize * 4;
10583 let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
10584
10585 #[cfg(feature = "parallel")]
10586 {
10587 let chunk_size = rayon::current_num_threads().max(1);
10588
10589 for chunk_start in (0..num_bands).step_by(chunk_size) {
10590 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
10591
10592 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
10593 .into_par_iter()
10594 .map(&render_band)
10595 .collect();
10596
10597 for (i, band_data) in rendered.iter().enumerate() {
10598 let band_idx = chunk_start + i as u32;
10599 let y_start = (band_idx * band_h) as usize;
10600 let dest_start = y_start * row_bytes;
10601 let len = band_data.len();
10602 result[dest_start..dest_start + len].copy_from_slice(band_data);
10603 }
10604 }
10605 }
10606 #[cfg(not(feature = "parallel"))]
10607 {
10608 for band_idx in 0..num_bands {
10609 let band_data = render_band(band_idx);
10610 let y_start = (band_idx * band_h) as usize;
10611 let dest_start = y_start * row_bytes;
10612 let len = band_data.len();
10613 result[dest_start..dest_start + len].copy_from_slice(&band_data);
10614 }
10615 }
10616
10617 result
10618}
10619
10620#[allow(clippy::too_many_arguments)]
10625pub fn render_region_prepared_parallel_with_progress(
10626 list: &DisplayList,
10627 prepared: &PreparedDisplayList,
10628 vp_x: f64,
10629 vp_y: f64,
10630 vp_w: f64,
10631 vp_h: f64,
10632 pixel_w: u32,
10633 pixel_h: u32,
10634 dpi: f64,
10635 icc: Option<&IccCache>,
10636 image_cache: Option<&ImageCache>,
10637 no_aa: bool,
10638 progress: &std::sync::atomic::AtomicU32,
10639) -> Vec<u8> {
10640 let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
10641
10642 if num_bands <= 1 {
10643 let result = render_region_prepared(
10644 list,
10645 prepared,
10646 vp_x,
10647 vp_y,
10648 vp_w,
10649 vp_h,
10650 pixel_w,
10651 pixel_h,
10652 dpi,
10653 icc,
10654 image_cache,
10655 no_aa,
10656 );
10657 progress.store(1, std::sync::atomic::Ordering::Relaxed);
10658 return result;
10659 }
10660
10661 let render_band = |band_idx: u32| -> Vec<u8> {
10662 render_region_single_band(
10663 list,
10664 prepared,
10665 vp_x,
10666 vp_y,
10667 vp_w,
10668 vp_h,
10669 pixel_w,
10670 pixel_h,
10671 band_idx,
10672 band_h,
10673 num_bands,
10674 dpi,
10675 icc,
10676 image_cache,
10677 no_aa,
10678 )
10679 };
10680
10681 let row_bytes = pixel_w as usize * 4;
10682 let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
10683
10684 #[cfg(feature = "parallel")]
10685 {
10686 let chunk_size = rayon::current_num_threads().max(1);
10687
10688 for chunk_start in (0..num_bands).step_by(chunk_size) {
10689 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
10690
10691 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
10692 .into_par_iter()
10693 .map(&render_band)
10694 .collect();
10695
10696 for (i, band_data) in rendered.iter().enumerate() {
10697 let band_idx = chunk_start + i as u32;
10698 let y_start = (band_idx * band_h) as usize;
10699 let dest_start = y_start * row_bytes;
10700 let len = band_data.len();
10701 result[dest_start..dest_start + len].copy_from_slice(band_data);
10702 }
10703 progress.store(chunk_end, std::sync::atomic::Ordering::Relaxed);
10704 }
10705 }
10706 #[cfg(not(feature = "parallel"))]
10707 {
10708 for band_idx in 0..num_bands {
10709 let band_data = render_band(band_idx);
10710 let y_start = (band_idx * band_h) as usize;
10711 let dest_start = y_start * row_bytes;
10712 let len = band_data.len();
10713 result[dest_start..dest_start + len].copy_from_slice(&band_data);
10714 progress.store(band_idx + 1, std::sync::atomic::Ordering::Relaxed);
10715 }
10716 }
10717
10718 result
10719}
10720
10721#[allow(clippy::too_many_arguments)]
10724pub fn render_region_prepared_parallel_cancellable(
10725 list: &DisplayList,
10726 prepared: &PreparedDisplayList,
10727 vp_x: f64,
10728 vp_y: f64,
10729 vp_w: f64,
10730 vp_h: f64,
10731 pixel_w: u32,
10732 pixel_h: u32,
10733 dpi: f64,
10734 icc: Option<&IccCache>,
10735 image_cache: Option<&ImageCache>,
10736 no_aa: bool,
10737 cancelled: &std::sync::atomic::AtomicBool,
10738) -> Option<Vec<u8>> {
10739 if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
10740 return None;
10741 }
10742
10743 let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
10744
10745 if num_bands <= 1 {
10746 return Some(render_region_prepared(
10747 list,
10748 prepared,
10749 vp_x,
10750 vp_y,
10751 vp_w,
10752 vp_h,
10753 pixel_w,
10754 pixel_h,
10755 dpi,
10756 icc,
10757 image_cache,
10758 no_aa,
10759 ));
10760 }
10761
10762 let render_band = |band_idx: u32| -> Vec<u8> {
10763 render_region_single_band(
10764 list,
10765 prepared,
10766 vp_x,
10767 vp_y,
10768 vp_w,
10769 vp_h,
10770 pixel_w,
10771 pixel_h,
10772 band_idx,
10773 band_h,
10774 num_bands,
10775 dpi,
10776 icc,
10777 image_cache,
10778 no_aa,
10779 )
10780 };
10781
10782 let row_bytes = pixel_w as usize * 4;
10783 let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
10784
10785 #[cfg(feature = "parallel")]
10786 {
10787 let chunk_size = rayon::current_num_threads().max(1);
10788
10789 for chunk_start in (0..num_bands).step_by(chunk_size) {
10790 if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
10791 return None;
10792 }
10793 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
10794
10795 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
10796 .into_par_iter()
10797 .map(&render_band)
10798 .collect();
10799
10800 for (i, band_data) in rendered.iter().enumerate() {
10801 let band_idx = chunk_start + i as u32;
10802 let y_start = (band_idx * band_h) as usize;
10803 let dest_start = y_start * row_bytes;
10804 let len = band_data.len();
10805 result[dest_start..dest_start + len].copy_from_slice(band_data);
10806 }
10807 }
10808 }
10809 #[cfg(not(feature = "parallel"))]
10810 {
10811 for band_idx in 0..num_bands {
10812 if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
10813 return None;
10814 }
10815 let band_data = render_band(band_idx);
10816 let y_start = (band_idx * band_h) as usize;
10817 let dest_start = y_start * row_bytes;
10818 let len = band_data.len();
10819 result[dest_start..dest_start + len].copy_from_slice(&band_data);
10820 }
10821 }
10822
10823 Some(result)
10824}
10825
10826pub fn render_to_rgba(
10834 list: &DisplayList,
10835 pixel_w: u32,
10836 pixel_h: u32,
10837 dpi: f64,
10838 icc: Option<&IccCache>,
10839 no_aa: bool,
10840) -> Vec<u8> {
10841 render_to_rgba_with_layers(list, pixel_w, pixel_h, dpi, icc, no_aa, &LayerSet::new())
10842}
10843
10844#[allow(clippy::too_many_arguments)]
10851pub fn render_to_rgba_with_layers(
10852 list: &DisplayList,
10853 pixel_w: u32,
10854 pixel_h: u32,
10855 dpi: f64,
10856 icc: Option<&IccCache>,
10857 no_aa: bool,
10858 layer_set: &LayerSet,
10859) -> Vec<u8> {
10860 if pixel_w == 0 || pixel_h == 0 {
10861 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10862 }
10863
10864 let mut icc_cache = match icc {
10865 Some(c) => c.clone(),
10866 None => IccCache::new(),
10867 };
10868 register_shading_icc_profiles(list, &mut icc_cache);
10871
10872 let mut sink = MemorySink {
10873 data: Vec::new(),
10874 width: 0,
10875 };
10876
10877 let band_h = select_band_height(pixel_w, pixel_h);
10878 if let Err(e) = render_banded_to_sink(
10879 pixel_w, pixel_h, band_h, dpi, list, &mut sink, &icc_cache, no_aa, layer_set,
10880 ) {
10881 eprintln!("render_to_rgba: banded render failed: {e}");
10882 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10883 }
10884
10885 sink.data
10886}
10887
10888pub fn render_to_rgba_viewport(
10901 list: &DisplayList,
10902 pixel_w: u32,
10903 pixel_h: u32,
10904 dpi: f64,
10905 icc: Option<&IccCache>,
10906 no_aa: bool,
10907) -> Vec<u8> {
10908 if pixel_w == 0 || pixel_h == 0 {
10909 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10910 }
10911
10912 let mut icc_cache = match icc {
10913 Some(c) => c.clone(),
10914 None => IccCache::new(),
10915 };
10916 register_shading_icc_profiles(list, &mut icc_cache);
10917
10918 let prepared = prepare_display_list(list);
10919 render_region_prepared_parallel(
10920 list,
10921 &prepared,
10922 0.0,
10923 0.0,
10924 pixel_w as f64,
10925 pixel_h as f64,
10926 pixel_w,
10927 pixel_h,
10928 dpi,
10929 Some(&icc_cache),
10930 None,
10931 no_aa,
10932 )
10933}
10934
10935fn debug_bbox_lines(list: &DisplayList, dpi: f64, depth: usize, out: &mut Vec<String>) {
10942 let y_bboxes = precompute_bboxes(list, dpi);
10943 let full_bboxes = precompute_full_bboxes(list, dpi);
10944 let elements = list.elements();
10945 let indent = " ".repeat(depth);
10946 for (i, elem) in elements.iter().enumerate() {
10947 let kind = match elem {
10948 DisplayElement::Fill { .. } => "Fill",
10949 DisplayElement::Stroke { .. } => "Stroke",
10950 DisplayElement::Image { .. } => "Image",
10951 DisplayElement::AxialShading { .. } => "AxialShading",
10952 DisplayElement::RadialShading { .. } => "RadialShading",
10953 DisplayElement::MeshShading { .. } => "MeshShading",
10954 DisplayElement::PatchShading { .. } => "PatchShading",
10955 DisplayElement::PatternFill { .. } => "PatternFill",
10956 DisplayElement::Group { .. } => "Group",
10957 DisplayElement::SoftMasked { .. } => "SoftMasked",
10958 DisplayElement::OcgGroup { .. } => "OcgGroup",
10959 DisplayElement::Clip { .. } => "Clip",
10960 DisplayElement::InitClip => "InitClip",
10961 DisplayElement::ErasePage => "ErasePage",
10962 DisplayElement::Text { .. } => "Text",
10963 _ => "Unknown",
10964 };
10965 let yb = &y_bboxes[i];
10966 let fb = &full_bboxes[i];
10967 let mut diff = false;
10968 if yb.is_some() != fb.is_some() {
10969 diff = true;
10970 }
10971 if let (Some(yb), Some(fb)) = (yb, fb)
10972 && ((yb.y_min - fb.y_min).abs() > 1e-9 || (yb.y_max - fb.y_max).abs() > 1e-9)
10973 {
10974 diff = true;
10975 }
10976 let yb_s = match yb {
10977 Some(b) => format!("Y[{:8.3}..{:8.3}]", b.y_min, b.y_max),
10978 None => "Y[None]".to_string(),
10979 };
10980 let fb_s = match fb {
10981 Some(b) => format!(
10982 "2D[x {:8.3}..{:8.3} y {:8.3}..{:8.3}]",
10983 b.x_min, b.x_max, b.y_min, b.y_max
10984 ),
10985 None => "2D[None]".to_string(),
10986 };
10987 out.push(format!(
10988 "{}{:4} {:15} {:30} {:55} {}",
10989 indent,
10990 i,
10991 kind,
10992 yb_s,
10993 fb_s,
10994 if diff { "DIFF" } else { "" }
10995 ));
10996 if let DisplayElement::Stroke { path, params } = elem {
10997 let rp = path_full_bbox(path);
10998 let m = ¶ms.ctm;
10999 out.push(format!(
11000 "{} ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] lw={:.4} miter={:.4} raw={}",
11001 indent,
11002 m.a,
11003 m.b,
11004 m.c,
11005 m.d,
11006 m.tx,
11007 m.ty,
11008 params.line_width,
11009 params.miter_limit,
11010 match rp {
11011 Some(b) => format!(
11012 "x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
11013 b.x_min, b.x_max, b.y_min, b.y_max
11014 ),
11015 None => "None".to_string(),
11016 }
11017 ));
11018 }
11019 if let DisplayElement::Clip { path, params } = elem {
11020 let rp = path_full_bbox(path);
11021 let m = ¶ms.ctm;
11022 out.push(format!(
11023 "{} clip ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] rule={:?} raw={}",
11024 indent,
11025 m.a,
11026 m.b,
11027 m.c,
11028 m.d,
11029 m.tx,
11030 m.ty,
11031 params.fill_rule,
11032 match rp {
11033 Some(b) => format!(
11034 "x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
11035 b.x_min, b.x_max, b.y_min, b.y_max
11036 ),
11037 None => "None".to_string(),
11038 }
11039 ));
11040 }
11041 if let DisplayElement::PatchShading { params } = elem {
11042 out.push(format!(
11043 "{} patch ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] bbox={:?} patches={}",
11044 indent,
11045 params.ctm.a,
11046 params.ctm.b,
11047 params.ctm.c,
11048 params.ctm.d,
11049 params.ctm.tx,
11050 params.ctm.ty,
11051 params.bbox,
11052 params.patches.len()
11053 ));
11054 if !params.patches.is_empty() {
11055 let patch = ¶ms.patches[0];
11056 let mut x_min = f64::INFINITY;
11058 let mut y_min = f64::INFINITY;
11059 let mut x_max = f64::NEG_INFINITY;
11060 let mut y_max = f64::NEG_INFINITY;
11061 for &(px, py) in &patch.points {
11062 let (dx, dy) = params.ctm.transform_point(px, py);
11063 x_min = x_min.min(dx);
11064 y_min = y_min.min(dy);
11065 x_max = x_max.max(dx);
11066 y_max = y_max.max(dy);
11067 }
11068 out.push(format!(
11069 "{} patch[0] pts={} dev x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
11070 indent,
11071 patch.points.len(),
11072 x_min,
11073 x_max,
11074 y_min,
11075 y_max
11076 ));
11077 }
11078 }
11079 if let DisplayElement::Group {
11080 elements: inner,
11081 params,
11082 } = elem
11083 {
11084 out.push(format!(
11085 "{} group bbox={:?} iso={} ko={} alpha={} bm={} cs={:?}",
11086 indent,
11087 params.bbox,
11088 params.isolated,
11089 params.knockout,
11090 params.alpha,
11091 params.blend_mode,
11092 params.color_space
11093 ));
11094 debug_bbox_lines(inner, dpi, depth + 1, out);
11095 }
11096 if let DisplayElement::SoftMasked {
11097 content, params, ..
11098 } = elem
11099 {
11100 out.push(format!(
11101 "{} softmasked bbox={:?}",
11102 indent, params.bbox
11103 ));
11104 debug_bbox_lines(content, dpi, depth + 1, out);
11105 }
11106 if let DisplayElement::OcgGroup {
11107 elements: inner,
11108 visibility,
11109 } = elem
11110 {
11111 out.push(format!(
11112 "{} ocg default_visible={}",
11113 indent,
11114 visibility.default_visible()
11115 ));
11116 debug_bbox_lines(inner, dpi, depth + 1, out);
11117 }
11118 }
11119}
11120
11121pub fn debug_bbox_comparison(list: &DisplayList, dpi: f64) -> Vec<String> {
11122 let mut out = Vec::new();
11123 debug_bbox_lines(list, dpi, 0, &mut out);
11124 out
11125}
11126
11127struct MemorySink {
11129 data: Vec<u8>,
11130 width: u32,
11131}
11132
11133impl stet_graphics::device::PageSink for MemorySink {
11134 fn begin_page(&mut self, width: u32, height: u32) -> Result<(), String> {
11135 self.width = width;
11136 self.data.reserve(width as usize * height as usize * 4);
11137 Ok(())
11138 }
11139
11140 fn write_rows(&mut self, rgba_rows: &[u8], _num_rows: u32) -> Result<(), String> {
11141 self.data.extend_from_slice(rgba_rows);
11142 Ok(())
11143 }
11144
11145 fn end_page(&mut self) -> Result<(), String> {
11146 Ok(())
11147 }
11148}
11149
11150#[allow(clippy::too_many_arguments)]
11159pub fn render_region(
11160 list: &DisplayList,
11161 vp_x: f64,
11162 vp_y: f64,
11163 vp_w: f64,
11164 vp_h: f64,
11165 pixel_w: u32,
11166 pixel_h: u32,
11167 dpi: f64,
11168 icc: Option<&IccCache>,
11169 image_cache: Option<&ImageCache>,
11170 no_aa: bool,
11171) -> Vec<u8> {
11172 if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
11173 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
11174 }
11175
11176 let layer_set = LayerSet::new();
11177 let scale_x = pixel_w as f64 / vp_w;
11178 let scale_y = pixel_h as f64 / vp_h;
11179 let effective_dpi = dpi * scale_x;
11181
11182 let bboxes = precompute_full_bboxes(list, effective_dpi);
11183 let epochs = build_viewport_epochs(list, &bboxes);
11184 let clip_seen = precompute_clip_seen(list);
11185
11186 const OVERLAP: u32 = 6;
11190 let render_h = pixel_h + 2 * OVERLAP;
11191
11192 let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create viewport pixmap");
11193 pixmap.fill(Color::TRANSPARENT);
11194
11195 let cmyk_buf = if has_overprint_elements(list)
11196 || list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
11197 || has_cmyk_group(list)
11198 {
11199 Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
11200 } else {
11201 None
11202 };
11203
11204 let mut state = BandState {
11205 clip_region: None,
11206 spare_mask: None,
11207 clip_mask_cache: HashMap::new(),
11208 clip_mask_seen: clip_seen,
11209 mask_pool: Vec::new(),
11210 cmyk_buffer: cmyk_buf,
11211 op_bg_snapshot: None,
11212 op_touched: None,
11213 spot_mask: None,
11214 };
11215
11216 let elements = list.elements();
11217 let vp_x_f = vp_x as f32;
11218 let vp_y_f = vp_y as f32;
11219 let sx = scale_x as f32;
11220 let sy = scale_y as f32;
11221 let vp_x_max = vp_x + vp_w;
11222 let vp_y_max = vp_y + vp_h;
11223
11224 for epoch in &epochs {
11225 if !epoch.has_erase_page {
11227 match epoch.paint_bbox {
11228 Some(ref pb)
11229 if pb.x_max <= vp_x
11230 || pb.x_min >= vp_x_max
11231 || pb.y_max <= vp_y
11232 || pb.y_min >= vp_y_max =>
11233 {
11234 continue;
11235 }
11236 None => continue,
11237 _ => {}
11238 }
11239 }
11240
11241 for i in epoch.start_idx..epoch.end_idx {
11242 let force_process = matches!(
11245 &elements[i],
11246 DisplayElement::OcgGroup { elements: inner, .. }
11247 if contains_clip_op(inner)
11248 );
11249 if !force_process
11251 && let Some(ref bbox) = bboxes[i]
11252 && (bbox.x_max <= vp_x
11253 || bbox.x_min >= vp_x_max
11254 || bbox.y_max <= vp_y
11255 || bbox.y_min >= vp_y_max)
11256 {
11257 continue;
11258 }
11259 let ctx = RenderContext {
11260 vp_x: vp_x_f,
11261 vp_y: vp_y_f,
11262 scale_x: sx,
11263 scale_y: sy,
11264 out_w: pixel_w,
11265 out_h: render_h,
11266 effective_dpi,
11267 icc,
11268 image_cache,
11269 preprocessed: None,
11270 elem_idx: i,
11271 no_aa,
11272 opm_zero_transparent: false,
11273 knockout_painter_pass: KnockoutPainterPass::None,
11274 parent_group_isolated: false,
11275 alpha_extraction_pass: false,
11276 layer_set: &layer_set,
11277 };
11278 render_element(&mut pixmap, &mut state, &elements[i], &ctx);
11279 }
11280 }
11281
11282 composite_onto_white(pixmap.data_mut());
11283 let row_bytes = pixel_w as usize * 4;
11285 let end = pixel_h as usize * row_bytes;
11286 pixmap.data()[..end].to_vec()
11287}
11288fn copy_backdrop_crop(
11290 parent: &Pixmap,
11291 crop_x: i32,
11292 crop_y: i32,
11293 crop_w: u32,
11294 crop_h: u32,
11295) -> Vec<u8> {
11296 let pw = parent.width() as usize;
11297 let src = parent.data();
11298 let cw = crop_w as usize;
11299 let ch = crop_h as usize;
11300 let cx = crop_x as usize;
11301 let cy = crop_y as usize;
11302 let mut backdrop = vec![0u8; cw * ch * 4];
11303 for row in 0..ch {
11304 let src_off = ((cy + row) * pw + cx) * 4;
11305 let dst_off = row * cw * 4;
11306 backdrop[dst_off..dst_off + cw * 4].copy_from_slice(&src[src_off..src_off + cw * 4]);
11307 }
11308 backdrop
11309}
11310fn clip_polygon_halfplane(
11315 poly: &[(f32, f32)],
11316 nx: f32,
11317 ny: f32,
11318 px: f32,
11319 py: f32,
11320) -> Vec<(f32, f32)> {
11321 if poly.is_empty() {
11322 return vec![];
11323 }
11324 let dot = |x: f32, y: f32| nx * (x - px) + ny * (y - py);
11325 let mut out = Vec::with_capacity(poly.len() + 1);
11326 let n = poly.len();
11327 for i in 0..n {
11328 let (ax, ay) = poly[i];
11329 let (bx, by) = poly[(i + 1) % n];
11330 let da = dot(ax, ay);
11331 let db = dot(bx, by);
11332 if da >= 0.0 {
11333 out.push((ax, ay));
11334 }
11335 if (da >= 0.0) != (db >= 0.0) {
11336 let t = da / (da - db);
11338 out.push((ax + t * (bx - ax), ay + t * (by - ay)));
11339 }
11340 }
11341 out
11342}
11343
11344#[allow(clippy::too_many_arguments)]
11346fn render_axial_shading(
11347 pixmap: &mut Pixmap,
11348 params: &AxialShadingParams,
11349 vp_x: f32,
11350 vp_y: f32,
11351 scale_x: f32,
11352 scale_y: f32,
11353 clip_mask: Option<&Mask>,
11354 no_aa: bool,
11355 cmyk_buf: Option<&mut [f32]>,
11356 icc: Option<&IccCache>,
11357) {
11358 let pw = pixmap.width();
11359 let ph = pixmap.height();
11360 if params.color_stops.is_empty() || pw == 0 || ph == 0 {
11361 return;
11362 }
11363
11364 let (mut rx_min, mut ry_min, mut rx_max, mut ry_max) = if let Some(bbox) = ¶ms.bbox {
11365 let corners = [
11366 params.ctm.transform_point(bbox[0], bbox[1]),
11367 params.ctm.transform_point(bbox[2], bbox[1]),
11368 params.ctm.transform_point(bbox[0], bbox[3]),
11369 params.ctm.transform_point(bbox[2], bbox[3]),
11370 ];
11371 let x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
11372 let y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
11373 let x_max = corners
11374 .iter()
11375 .map(|c| c.0)
11376 .fold(f64::NEG_INFINITY, f64::max);
11377 let y_max = corners
11378 .iter()
11379 .map(|c| c.1)
11380 .fold(f64::NEG_INFINITY, f64::max);
11381 (
11382 ((x_min as f32 - vp_x) * scale_x).max(0.0),
11383 ((y_min as f32 - vp_y) * scale_y).max(0.0),
11384 ((x_max as f32 - vp_x) * scale_x).min(pw as f32),
11385 ((y_max as f32 - vp_y) * scale_y).min(ph as f32),
11386 )
11387 } else {
11388 (0.0, 0.0, pw as f32, ph as f32)
11389 };
11390
11391 if rx_max <= rx_min || ry_max <= ry_min {
11392 return;
11393 }
11394
11395 let (dx0, dy0) = params.ctm.transform_point(params.x0, params.y0);
11397 let (dx1, dy1) = params.ctm.transform_point(params.x1, params.y1);
11398
11399 let needs_perpendicular_clip = (!params.extend_start || !params.extend_end) && {
11403 let axis_x = dx1 - dx0;
11404 let axis_y = dy1 - dy0;
11405 axis_x.abs() > 1e-6 && axis_y.abs() > 1e-6
11406 };
11407
11408 let bbox_is_rotated =
11411 params.bbox.is_some() && (params.ctm.b.abs() > 1e-10 || params.ctm.c.abs() > 1e-10);
11412
11413 if needs_perpendicular_clip {
11414 let stops = build_gradient_stops(¶ms.color_stops);
11417 if stops.is_empty() {
11418 return;
11419 }
11420 let start = stet_tiny_skia::Point::from_xy(params.x0 as f32, params.y0 as f32);
11421 let end = stet_tiny_skia::Point::from_xy(params.x1 as f32, params.y1 as f32);
11422 let gradient_transform =
11423 viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
11424 let Some(gradient) = stet_tiny_skia::LinearGradient::new(
11425 start,
11426 end,
11427 stops,
11428 stet_tiny_skia::SpreadMode::Pad,
11429 gradient_transform,
11430 ) else {
11431 return;
11432 };
11433 let paint = Paint {
11434 shader: gradient,
11435 anti_alias: !no_aa,
11436 ..Paint::default()
11437 };
11438
11439 let mut poly: Vec<(f32, f32)> = if bbox_is_rotated {
11441 let bbox = params.bbox.as_ref().unwrap();
11442 let corners = [
11443 params.ctm.transform_point(bbox[0], bbox[1]),
11444 params.ctm.transform_point(bbox[2], bbox[1]),
11445 params.ctm.transform_point(bbox[2], bbox[3]),
11446 params.ctm.transform_point(bbox[0], bbox[3]),
11447 ];
11448 corners
11449 .iter()
11450 .map(|(x, y)| ((*x as f32 - vp_x) * scale_x, (*y as f32 - vp_y) * scale_y))
11451 .collect()
11452 } else {
11453 vec![
11454 (rx_min, ry_min),
11455 (rx_max, ry_min),
11456 (rx_max, ry_max),
11457 (rx_min, ry_max),
11458 ]
11459 };
11460 let ax = (dx1 - dx0) as f32 * scale_x;
11461 let ay = (dy1 - dy0) as f32 * scale_y;
11462 if !params.extend_start {
11463 let px = (dx0 as f32 - vp_x) * scale_x;
11464 let py = (dy0 as f32 - vp_y) * scale_y;
11465 poly = clip_polygon_halfplane(&poly, ax, ay, px, py);
11466 }
11467 if !params.extend_end {
11468 let px = (dx1 as f32 - vp_x) * scale_x;
11469 let py = (dy1 as f32 - vp_y) * scale_y;
11470 poly = clip_polygon_halfplane(&poly, -ax, -ay, px, py);
11471 }
11472 if poly.len() >= 3 {
11473 let mut pb = PathBuilder::new();
11474 pb.move_to(poly[0].0, poly[0].1);
11475 for &(x, y) in &poly[1..] {
11476 pb.line_to(x, y);
11477 }
11478 pb.close();
11479 if let Some(path) = pb.finish() {
11480 pixmap.fill_path(
11481 &path,
11482 &paint,
11483 SkiaFillRule::Winding,
11484 Transform::identity(),
11485 clip_mask,
11486 );
11487 }
11488 }
11489 } else {
11490 if !params.extend_start || !params.extend_end {
11493 let axis_x = dx1 - dx0;
11494 let axis_y = dy1 - dy0;
11495 let gx0 = (dx0 as f32 - vp_x) * scale_x;
11496 let gy0 = (dy0 as f32 - vp_y) * scale_y;
11497 let gx1 = (dx1 as f32 - vp_x) * scale_x;
11498 let gy1 = (dy1 as f32 - vp_y) * scale_y;
11499
11500 if axis_x.abs() >= axis_y.abs() {
11501 if !params.extend_start {
11502 if axis_x >= 0.0 {
11503 rx_min = rx_min.max(gx0);
11504 } else {
11505 rx_max = rx_max.min(gx0);
11506 }
11507 }
11508 if !params.extend_end {
11509 if axis_x >= 0.0 {
11510 rx_max = rx_max.min(gx1);
11511 } else {
11512 rx_min = rx_min.max(gx1);
11513 }
11514 }
11515 } else {
11516 if !params.extend_start {
11517 if axis_y >= 0.0 {
11518 ry_min = ry_min.max(gy0);
11519 } else {
11520 ry_max = ry_max.min(gy0);
11521 }
11522 }
11523 if !params.extend_end {
11524 if axis_y >= 0.0 {
11525 ry_max = ry_max.min(gy1);
11526 } else {
11527 ry_min = ry_min.max(gy1);
11528 }
11529 }
11530 }
11531 if rx_max <= rx_min || ry_max <= ry_min {
11532 return;
11533 }
11534 }
11535
11536 let ax = params.x1 - params.x0;
11538 let ay = params.y1 - params.y0;
11539 let axis_sq = ax * ax + ay * ay;
11540 if axis_sq < 1e-20 {
11541 return;
11542 }
11543
11544 let pixel_dx = (dx1 - dx0) * scale_x as f64;
11548 let pixel_dy = (dy1 - dy0) * scale_y as f64;
11549 let pixel_axis_len = (pixel_dx * pixel_dx + pixel_dy * pixel_dy).sqrt();
11550 let lut_size = (pixel_axis_len as usize)
11551 .max(params.color_stops.len())
11552 .max(256)
11553 .min(16384);
11554 let lut = build_gradient_lut(¶ms.color_stops, lut_size);
11555
11556 let Some(inv) = params.ctm.invert() else {
11557 return;
11558 };
11559 let inv_sx = 1.0 / scale_x as f64;
11560 let inv_sy = 1.0 / scale_y as f64;
11561 let dev_origin_x = vp_x as f64;
11562 let dev_origin_y = vp_y as f64;
11563
11564 let sx_base = inv.a * dev_origin_x + inv.c * dev_origin_y + inv.tx;
11568 let sy_base = inv.b * dev_origin_x + inv.d * dev_origin_y + inv.ty;
11569 let dsx_dx = inv.a * inv_sx;
11570 let dsx_dy = inv.c * inv_sy;
11571 let dsy_dx = inv.b * inv_sx;
11572 let dsy_dy = inv.d * inv_sy;
11573
11574 let inv_axis_sq = 1.0 / axis_sq;
11576 let t_origin = ((sx_base - params.x0) * ax + (sy_base - params.y0) * ay) * inv_axis_sq;
11577 let dt_dx = (dsx_dx * ax + dsy_dx * ay) * inv_axis_sq;
11578 let dt_dy = (dsx_dy * ax + dsy_dy * ay) * inv_axis_sq;
11579
11580 let bbox_pixel_clip = if bbox_is_rotated {
11583 let bbox = params.bbox.as_ref().unwrap();
11584 let (bx0, bx1) = (bbox[0].min(bbox[2]), bbox[0].max(bbox[2]));
11585 let (by0, by1) = (bbox[1].min(bbox[3]), bbox[1].max(bbox[3]));
11586 Some((
11587 dsx_dx, dsx_dy, sx_base, dsy_dx, dsy_dy, sy_base, bx0, by0, bx1, by1,
11588 ))
11589 } else {
11590 None
11591 };
11592
11593 let ix_min = rx_min.floor() as u32;
11594 let ix_max = rx_max.ceil().min(pw as f32) as u32;
11595 let iy_min = ry_min.floor() as u32;
11596 let iy_max = ry_max.ceil().min(ph as f32) as u32;
11597
11598 let stride = pw as usize * 4;
11599 let data = pixmap.data_mut();
11600 let mask_data = clip_mask.map(|m| m.data());
11601 let alpha = (params.alpha.clamp(0.0, 1.0) * 255.0 + 0.5) as u16;
11602
11603 for py in iy_min..iy_max {
11604 let t_row = t_origin + dt_dy * py as f64;
11605 let row_offset = py as usize * stride;
11606
11607 let (ux_row, uy_row) =
11609 if let Some((_, dux_dy, ux_base, _, duy_dy, uy_base, ..)) = &bbox_pixel_clip {
11610 (ux_base + dux_dy * py as f64, uy_base + duy_dy * py as f64)
11611 } else {
11612 (0.0, 0.0)
11613 };
11614
11615 for px in ix_min..ix_max {
11616 if let Some(md) = mask_data {
11618 if md[py as usize * pw as usize + px as usize] == 0 {
11619 continue;
11620 }
11621 }
11622
11623 if let Some((dux_dx, _, _, duy_dx, _, _, bx0, by0, bx1, by1)) = &bbox_pixel_clip {
11625 let ux = ux_row + dux_dx * px as f64;
11626 let uy = uy_row + duy_dx * px as f64;
11627 if ux < *bx0 || ux > *bx1 || uy < *by0 || uy > *by1 {
11628 continue;
11629 }
11630 }
11631
11632 let t = t_row + dt_dx * px as f64;
11633 let t_clamped = t.clamp(0.0, 1.0);
11634 let idx = (t_clamped * (lut_size - 1) as f64 + 0.5) as usize;
11635 let [r, g, b, _] = lut[idx.min(lut_size - 1)];
11636
11637 let offset = row_offset + px as usize * 4;
11638 if alpha >= 255 {
11639 data[offset] = r;
11640 data[offset + 1] = g;
11641 data[offset + 2] = b;
11642 data[offset + 3] = 255;
11643 } else {
11644 let a = alpha as u16;
11646 let inv_a = 255 - a;
11647 data[offset] = ((r as u16 * a + data[offset] as u16 * inv_a + 127) / 255) as u8;
11648 data[offset + 1] =
11649 ((g as u16 * a + data[offset + 1] as u16 * inv_a + 127) / 255) as u8;
11650 data[offset + 2] =
11651 ((b as u16 * a + data[offset + 2] as u16 * inv_a + 127) / 255) as u8;
11652 data[offset + 3] = ((a + data[offset + 3] as u16 * inv_a / 255).min(255)) as u8;
11653 }
11654 }
11655 }
11656 }
11657
11658 if let Some(buf) = cmyk_buf {
11660 let pw = pixmap.width();
11661 let inv_sx = 1.0 / scale_x as f64;
11662 let inv_sy = 1.0 / scale_y as f64;
11663 let axis_x = params.x1 - params.x0;
11664 let axis_y = params.y1 - params.y0;
11665 let axis_len_sq = axis_x * axis_x + axis_y * axis_y;
11666 let Some(inv_ctm) = params.ctm.invert() else {
11667 return;
11668 };
11669
11670 let iy_min = ry_min.floor() as u32;
11671 let iy_max = ry_max.ceil().min(pixmap.height() as f32) as u32;
11672 let ix_min = rx_min.floor() as u32;
11673 let ix_max = rx_max.ceil().min(pw as f32) as u32;
11674
11675 for py in iy_min..iy_max {
11676 let dev_y = py as f64 * inv_sy + vp_y as f64;
11677 for px in ix_min..ix_max {
11678 let dev_x = px as f64 * inv_sx + vp_x as f64;
11679 let (ux, uy) = inv_ctm.transform_point(dev_x, dev_y);
11680 let t = if axis_len_sq > 1e-10 {
11681 ((ux - params.x0) * axis_x + (uy - params.y0) * axis_y) / axis_len_sq
11682 } else {
11683 0.0
11684 };
11685 if t < 0.0 && !params.extend_start {
11686 continue;
11687 }
11688 if t > 1.0 && !params.extend_end {
11689 continue;
11690 }
11691 let clamped = t.clamp(0.0, 1.0);
11692
11693 if let Some(mask) = clip_mask {
11694 let mi = py as usize * pw as usize + px as usize;
11695 if mask.data()[mi] == 0 {
11696 continue;
11697 }
11698 }
11699
11700 let color = interpolate_color_stops(¶ms.color_stops, clamped);
11701 let cmyk = interpolate_cmyk_from_stops(
11702 ¶ms.color_stops,
11703 ¶ms.color_space,
11704 clamped,
11705 &color,
11706 icc,
11707 );
11708 let ci = (py as usize * pw as usize + px as usize) * 4;
11709 if ci + 3 < buf.len() {
11710 if params.spot_tint_blend && params.overprint {
11711 let cur_c = buf[ci] as f64;
11744 let cur_m = buf[ci + 1] as f64;
11745 let cur_y = buf[ci + 2] as f64;
11746 let cur_k = buf[ci + 3] as f64;
11747 let cur_is_zero =
11748 cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
11749 let named = params.painted_channels;
11750 if cur_is_zero {
11751 if named & stet_graphics::device::CMYK_C != 0 {
11752 buf[ci] = cmyk.0 as f32;
11753 }
11754 if named & stet_graphics::device::CMYK_M != 0 {
11755 buf[ci + 1] = cmyk.1 as f32;
11756 }
11757 if named & stet_graphics::device::CMYK_Y != 0 {
11758 buf[ci + 2] = cmyk.2 as f32;
11759 }
11760 if named & stet_graphics::device::CMYK_K != 0 {
11761 buf[ci + 3] = cmyk.3 as f32;
11762 }
11763 } else {
11764 let new_c = if named & stet_graphics::device::CMYK_C != 0 {
11765 cmyk.0
11766 } else {
11767 cur_c
11768 };
11769 let new_m = if named & stet_graphics::device::CMYK_M != 0 {
11770 cmyk.1
11771 } else {
11772 cur_m
11773 };
11774 let new_y = if named & stet_graphics::device::CMYK_Y != 0 {
11775 cmyk.2
11776 } else {
11777 cur_y
11778 };
11779 let new_k = if named & stet_graphics::device::CMYK_K != 0 {
11780 cmyk.3
11781 } else {
11782 cur_k
11783 };
11784 buf[ci] = new_c as f32;
11785 buf[ci + 1] = new_m as f32;
11786 buf[ci + 2] = new_y as f32;
11787 buf[ci + 3] = new_k as f32;
11788 let (rv, gv, bv) = if let Some(icc_cache) = icc {
11789 icc_cache
11790 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
11791 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
11792 } else {
11793 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
11794 };
11795 let stride = pixmap.data().len() / pixmap.height() as usize;
11796 let offset = py as usize * stride + px as usize * 4;
11797 let data = pixmap.data_mut();
11798 data[offset] = (rv * 255.0).round().clamp(0.0, 255.0) as u8;
11799 data[offset + 1] = (gv * 255.0).round().clamp(0.0, 255.0) as u8;
11800 data[offset + 2] = (bv * 255.0).round().clamp(0.0, 255.0) as u8;
11801 }
11802 } else if params.overprint
11803 && params.painted_channels != stet_graphics::device::CMYK_ALL
11804 {
11805 if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
11806 buf[ci] = cmyk.0 as f32;
11807 }
11808 if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
11809 buf[ci + 1] = cmyk.1 as f32;
11810 }
11811 if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
11812 buf[ci + 2] = cmyk.2 as f32;
11813 }
11814 if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
11815 buf[ci + 3] = cmyk.3 as f32;
11816 }
11817 let c = buf[ci] as f64;
11819 let m = buf[ci + 1] as f64;
11820 let y = buf[ci + 2] as f64;
11821 let k = buf[ci + 3] as f64;
11822 let (rv, gv, bv) = if let Some(icc_cache) = icc {
11823 icc_cache
11824 .convert_cmyk_readonly(c, m, y, k)
11825 .unwrap_or_else(|| cmyk_to_rgb_plrm(c, m, y, k))
11826 } else {
11827 cmyk_to_rgb_plrm(c, m, y, k)
11828 };
11829 let stride = pixmap.data().len() / pixmap.height() as usize;
11830 let offset = py as usize * stride + px as usize * 4;
11831 let data = pixmap.data_mut();
11832 data[offset] = (rv * 255.0).round().clamp(0.0, 255.0) as u8;
11833 data[offset + 1] = (gv * 255.0).round().clamp(0.0, 255.0) as u8;
11834 data[offset + 2] = (bv * 255.0).round().clamp(0.0, 255.0) as u8;
11835 } else {
11836 buf[ci] = cmyk.0 as f32;
11853 buf[ci + 1] = cmyk.1 as f32;
11854 buf[ci + 2] = cmyk.2 as f32;
11855 buf[ci + 3] = cmyk.3 as f32;
11856 }
11857 }
11858 }
11859 }
11860 }
11861}
11862
11863#[allow(clippy::too_many_arguments)]
11865fn render_radial_shading(
11866 pixmap: &mut Pixmap,
11867 params: &RadialShadingParams,
11868 vp_x: f32,
11869 vp_y: f32,
11870 scale_x: f32,
11871 scale_y: f32,
11872 clip_mask: Option<&Mask>,
11873 _no_aa: bool,
11874 mut cmyk_buf: Option<&mut [f32]>,
11875 icc: Option<&IccCache>,
11876) {
11877 let pw = pixmap.width();
11878 let ph = pixmap.height();
11879 if params.color_stops.is_empty() || pw == 0 || ph == 0 {
11880 return;
11881 }
11882
11883 let Some(inv_ctm) = params.ctm.invert() else {
11884 return;
11885 };
11886
11887 let (px_min, py_min, px_max, py_max) = if let Some(bbox) = ¶ms.bbox {
11888 let corners = [
11889 params.ctm.transform_point(bbox[0], bbox[1]),
11890 params.ctm.transform_point(bbox[2], bbox[1]),
11891 params.ctm.transform_point(bbox[0], bbox[3]),
11892 params.ctm.transform_point(bbox[2], bbox[3]),
11893 ];
11894 let x_min = corners
11895 .iter()
11896 .map(|c| c.0 as f32)
11897 .fold(f32::INFINITY, f32::min);
11898 let y_min = corners
11899 .iter()
11900 .map(|c| c.1 as f32)
11901 .fold(f32::INFINITY, f32::min);
11902 let x_max = corners
11903 .iter()
11904 .map(|c| c.0 as f32)
11905 .fold(f32::NEG_INFINITY, f32::max);
11906 let y_max = corners
11907 .iter()
11908 .map(|c| c.1 as f32)
11909 .fold(f32::NEG_INFINITY, f32::max);
11910 (
11911 ((x_min - vp_x) * scale_x).max(0.0) as u32,
11912 ((y_min - vp_y) * scale_y).max(0.0) as u32,
11913 (((x_max - vp_x) * scale_x).ceil() as u32).min(pw),
11914 (((y_max - vp_y) * scale_y).ceil() as u32).min(ph),
11915 )
11916 } else {
11917 (0, 0, pw, ph)
11918 };
11919
11920 let inv_sx = 1.0 / scale_x as f64;
11921 let inv_sy = 1.0 / scale_y as f64;
11922
11923 let rotated_bbox = if let Some(bbox) = ¶ms.bbox {
11925 if params.ctm.b.abs() > 1e-10 || params.ctm.c.abs() > 1e-10 {
11926 let (bx0, bx1) = (bbox[0].min(bbox[2]), bbox[0].max(bbox[2]));
11927 let (by0, by1) = (bbox[1].min(bbox[3]), bbox[1].max(bbox[3]));
11928 Some((bx0, by0, bx1, by1))
11929 } else {
11930 None
11931 }
11932 } else {
11933 None
11934 };
11935
11936 let data = pixmap.data_mut();
11937 let stride = pw as usize * 4;
11938
11939 for py in py_min..py_max {
11940 let dev_y = py as f64 * inv_sy + vp_y as f64;
11941 for px in px_min..px_max {
11942 let dev_x = px as f64 * inv_sx + vp_x as f64;
11943 let (ux, uy) = inv_ctm.transform_point(dev_x, dev_y);
11944
11945 if let Some((bx0, by0, bx1, by1)) = rotated_bbox {
11947 if ux < bx0 || ux > bx1 || uy < by0 || uy > by1 {
11948 continue;
11949 }
11950 }
11951
11952 let t = solve_radial_t(
11953 ux,
11954 uy,
11955 params.x0,
11956 params.y0,
11957 params.r0,
11958 params.x1,
11959 params.y1,
11960 params.r1,
11961 params.extend_start,
11962 params.extend_end,
11963 );
11964 if let Some(t) = t {
11965 let clamped = t.clamp(0.0, 1.0);
11966 let color = interpolate_color_stops(¶ms.color_stops, clamped);
11967
11968 let clipped = clip_mask
11969 .is_some_and(|mask| mask.data()[py as usize * pw as usize + px as usize] == 0);
11970
11971 if clipped {
11972 continue;
11973 }
11974
11975 let cmyk = interpolate_cmyk_from_stops(
11984 ¶ms.color_stops,
11985 ¶ms.color_space,
11986 clamped,
11987 &color,
11988 icc,
11989 );
11990 let ci = (py as usize * pw as usize + px as usize) * 4;
11991 let buffer_clean = if let Some(ref buf) = cmyk_buf {
11992 if ci + 3 < buf.len() {
11993 buf[ci] == 0.0
11994 && buf[ci + 1] == 0.0
11995 && buf[ci + 2] == 0.0
11996 && buf[ci + 3] == 0.0
11997 } else {
11998 false
11999 }
12000 } else {
12001 false
12002 };
12003 let offset_for_check = py as usize * stride + px as usize * 4;
12004 let pixmap_has_colour = data[offset_for_check + 3] > 0
12005 && (data[offset_for_check] < 250
12006 || data[offset_for_check + 1] < 250
12007 || data[offset_for_check + 2] < 250);
12008 let use_multiplicative = params.overprint
12009 && params.painted_channels != stet_graphics::device::CMYK_ALL
12010 && buffer_clean
12011 && pixmap_has_colour;
12012
12013 if let Some(ref mut buf) = cmyk_buf
12015 && ci + 3 < buf.len()
12016 {
12017 if params.overprint
12018 && params.painted_channels != stet_graphics::device::CMYK_ALL
12019 {
12020 if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
12021 buf[ci] = cmyk.0 as f32;
12022 }
12023 if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
12024 buf[ci + 1] = cmyk.1 as f32;
12025 }
12026 if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
12027 buf[ci + 2] = cmyk.2 as f32;
12028 }
12029 if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
12030 buf[ci + 3] = cmyk.3 as f32;
12031 }
12032 } else {
12033 buf[ci] = cmyk.0 as f32;
12034 buf[ci + 1] = cmyk.1 as f32;
12035 buf[ci + 2] = cmyk.2 as f32;
12036 buf[ci + 3] = cmyk.3 as f32;
12037 }
12038 }
12039
12040 let offset = py as usize * stride + px as usize * 4;
12041 if use_multiplicative {
12042 let bg_r = data[offset] as f64 / 255.0;
12047 let bg_g = data[offset + 1] as f64 / 255.0;
12048 let bg_b = data[offset + 2] as f64 / 255.0;
12049 let over_r = if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
12050 1.0 - cmyk.0
12051 } else {
12052 1.0
12053 };
12054 let over_g = if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
12055 1.0 - cmyk.1
12056 } else {
12057 1.0
12058 };
12059 let over_b = if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
12060 1.0 - cmyk.2
12061 } else {
12062 1.0
12063 };
12064 let k_fac = if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
12065 1.0 - cmyk.3
12066 } else {
12067 1.0
12068 };
12069 data[offset] = ((bg_r * over_r * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
12070 data[offset + 1] =
12071 ((bg_g * over_g * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
12072 data[offset + 2] =
12073 ((bg_b * over_b * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
12074 data[offset + 3] = 255;
12075 } else {
12076 data[offset] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
12077 data[offset + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
12078 data[offset + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
12079 data[offset + 3] = 255;
12080
12081 if params.overprint
12096 && params.painted_channels != stet_graphics::device::CMYK_ALL
12097 && matches!(
12098 params.color_space,
12099 ShadingColorSpace::DeviceCMYK
12100 | ShadingColorSpace::Separation { .. }
12101 | ShadingColorSpace::DeviceN { .. }
12102 )
12103 && let Some(ref mut buf) = cmyk_buf
12104 && ci + 3 < buf.len()
12105 && let Some(icc_cache) = icc
12106 {
12107 let c = buf[ci] as f64;
12108 let m = buf[ci + 1] as f64;
12109 let y = buf[ci + 2] as f64;
12110 let k = buf[ci + 3] as f64;
12111 if let Some((r, g, b)) = icc_cache.convert_cmyk_readonly(c, m, y, k) {
12112 data[offset] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
12113 data[offset + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
12114 data[offset + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
12115 }
12116 }
12117 }
12118 }
12119 }
12120 }
12121}
12122#[allow(clippy::too_many_arguments)]
12127fn solve_radial_t(
12128 px: f64,
12129 py: f64,
12130 x0: f64,
12131 y0: f64,
12132 r0: f64,
12133 x1: f64,
12134 y1: f64,
12135 r1: f64,
12136 extend_start: bool,
12137 extend_end: bool,
12138) -> Option<f64> {
12139 let cdx = x1 - x0;
12142 let cdy = y1 - y0;
12143 let dr = r1 - r0;
12144
12145 let a = cdx * cdx + cdy * cdy - dr * dr;
12146 let dpx = px - x0;
12147 let dpy = py - y0;
12148 let b = -2.0 * (dpx * cdx + dpy * cdy + r0 * dr);
12149 let c = dpx * dpx + dpy * dpy - r0 * r0;
12150
12151 let in_domain = |t: f64| -> bool {
12153 (0.0..=1.0).contains(&t) || (t < 0.0 && extend_start) || (t > 1.0 && extend_end)
12154 };
12155
12156 if a.abs() < 1e-10 {
12157 if b.abs() < 1e-10 {
12159 return None;
12160 }
12161 let t = -c / b;
12162 let radius = r0 + t * dr;
12163 if radius >= 0.0 && in_domain(t) {
12164 return Some(t);
12165 }
12166 return None;
12167 }
12168
12169 let discriminant = b * b - 4.0 * a * c;
12170 if discriminant < 0.0 {
12171 return None;
12172 }
12173 let sqrt_d = discriminant.sqrt();
12174 let t1 = (-b + sqrt_d) / (2.0 * a);
12175 let t2 = (-b - sqrt_d) / (2.0 * a);
12176
12177 let mut best: Option<f64> = None;
12179 for t in [t1, t2] {
12180 let radius = r0 + t * dr;
12181 if radius >= 0.0 && in_domain(t) {
12182 best = Some(match best {
12183 Some(prev) => prev.max(t),
12184 None => t,
12185 });
12186 }
12187 }
12188 best
12189}
12190
12191#[allow(clippy::too_many_arguments)]
12193fn render_mesh_shading(
12194 pixmap: &mut Pixmap,
12195 params: &MeshShadingParams,
12196 vp_x: f32,
12197 vp_y: f32,
12198 scale_x: f32,
12199 scale_y: f32,
12200 clip_mask: Option<&Mask>,
12201 mut cmyk_buf: Option<&mut [f32]>,
12202 icc: Option<&IccCache>,
12203) {
12204 let pw = pixmap.width() as usize;
12205 let ph = pixmap.height() as usize;
12206 if pw == 0 || ph == 0 {
12207 return;
12208 }
12209 let data = pixmap.data_mut();
12210 let stride = pw * 4;
12211
12212 let lut = params.color_lut.as_deref();
12213
12214 for tri in ¶ms.triangles {
12215 let (dx0, dy0) = params.ctm.transform_point(tri.v0.x, tri.v0.y);
12216 let (dx1, dy1) = params.ctm.transform_point(tri.v1.x, tri.v1.y);
12217 let (dx2, dy2) = params.ctm.transform_point(tri.v2.x, tri.v2.y);
12218
12219 let x0 = (dx0 as f32 - vp_x) * scale_x;
12220 let y0 = (dy0 as f32 - vp_y) * scale_y;
12221 let x1 = (dx1 as f32 - vp_x) * scale_x;
12222 let y1 = (dy1 as f32 - vp_y) * scale_y;
12223 let x2 = (dx2 as f32 - vp_x) * scale_x;
12224 let y2 = (dy2 as f32 - vp_y) * scale_y;
12225
12226 let min_x = (x0.min(x1).min(x2).floor().max(0.0)) as usize;
12227 let max_x = (x0.max(x1).max(x2).ceil() as usize).min(pw);
12228 let min_y = (y0.min(y1).min(y2).floor().max(0.0)) as usize;
12229 let max_y = (y0.max(y1).max(y2).ceil() as usize).min(ph);
12230
12231 if min_x >= max_x || min_y >= max_y {
12232 continue;
12233 }
12234
12235 let x0 = x0 as f64;
12236 let y0 = y0 as f64;
12237 let x1 = x1 as f64;
12238 let y1 = y1 as f64;
12239 let x2 = x2 as f64;
12240 let y2 = y2 as f64;
12241 let denom = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2);
12246 if denom.abs() < 1e-10 {
12247 continue;
12248 }
12249 let (x1, y1, x2, y2) = if denom < 0.0 {
12250 (x2, y2, x1, y1)
12251 } else {
12252 (x1, y1, x2, y2)
12253 };
12254 let (v1_ref, v2_ref) = if denom < 0.0 {
12255 (&tri.v2, &tri.v1)
12256 } else {
12257 (&tri.v1, &tri.v2)
12258 };
12259 let denom = denom.abs();
12260 let inv_denom = 1.0 / denom;
12261
12262 for py in min_y..max_y {
12263 for px in min_x..max_x {
12264 let pxf = px as f64 + 0.5;
12265 let pyf = py as f64 + 0.5;
12266
12267 let w0 = ((y1 - y2) * (pxf - x2) + (x2 - x1) * (pyf - y2)) * inv_denom;
12268 let w1 = ((y2 - y0) * (pxf - x2) + (x0 - x2) * (pyf - y2)) * inv_denom;
12269 let w2 = 1.0 - w0 - w1;
12270
12271 if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 {
12272 continue;
12273 }
12274
12275 let clipped = clip_mask.is_some_and(|mask| mask.data()[py * pw + px] == 0);
12276
12277 let w0c = w0.max(0.0);
12278 let w1c = w1.max(0.0);
12279 let w2c = w2.max(0.0);
12280 let wsum = w0c + w1c + w2c;
12281 let w0n = w0c / wsum;
12282 let w1n = w1c / wsum;
12283 let w2n = w2c / wsum;
12284
12285 let (r, g, b) = if let Some(lut) = lut {
12288 let raw = w0n * tri.v0.raw_components[0]
12290 + w1n * v1_ref.raw_components[0]
12291 + w2n * v2_ref.raw_components[0];
12292 let raw = raw.clamp(0.0, 1.0);
12293 let fi = raw * (lut.len() - 1) as f64;
12295 let i0 = (fi as usize).min(lut.len().saturating_sub(2));
12296 let frac = fi - i0 as f64;
12297 let c0 = &lut[i0];
12298 let c1 = &lut[i0 + 1];
12299 (
12300 c0.r + frac * (c1.r - c0.r),
12301 c0.g + frac * (c1.g - c0.g),
12302 c0.b + frac * (c1.b - c0.b),
12303 )
12304 } else {
12305 (
12306 w0n * tri.v0.color.r + w1n * v1_ref.color.r + w2n * v2_ref.color.r,
12307 w0n * tri.v0.color.g + w1n * v1_ref.color.g + w2n * v2_ref.color.g,
12308 w0n * tri.v0.color.b + w1n * v1_ref.color.b + w2n * v2_ref.color.b,
12309 )
12310 };
12311
12312 if let Some(ref mut buf) = cmyk_buf {
12314 let ci = (py * pw + px) * 4;
12315 if ci + 3 < buf.len() {
12316 let cmyk = interpolate_cmyk_from_vertices(
12317 &tri.v0,
12318 v1_ref,
12319 v2_ref,
12320 w0n,
12321 w1n,
12322 w2n,
12323 ¶ms.color_space,
12324 r,
12325 g,
12326 b,
12327 icc,
12328 );
12329 if params.overprint
12330 && params.painted_channels != stet_graphics::device::CMYK_ALL
12331 {
12332 if !clipped {
12333 if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
12334 buf[ci] = cmyk.0 as f32;
12335 }
12336 if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
12337 buf[ci + 1] = cmyk.1 as f32;
12338 }
12339 if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
12340 buf[ci + 2] = cmyk.2 as f32;
12341 }
12342 if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
12343 buf[ci + 3] = cmyk.3 as f32;
12344 }
12345 }
12346 } else {
12347 buf[ci] = cmyk.0 as f32;
12348 buf[ci + 1] = cmyk.1 as f32;
12349 buf[ci + 2] = cmyk.2 as f32;
12350 buf[ci + 3] = cmyk.3 as f32;
12351 }
12352 }
12353 }
12354
12355 if clipped {
12356 continue;
12357 }
12358
12359 let offset = py * stride + px * 4;
12360 data[offset] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
12361 data[offset + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
12362 data[offset + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
12363 data[offset + 3] = 255;
12364 }
12365 }
12366 }
12367}
12368
12369#[allow(clippy::too_many_arguments)]
12371fn render_patch_shading(
12372 pixmap: &mut Pixmap,
12373 params: &PatchShadingParams,
12374 vp_x: f32,
12375 vp_y: f32,
12376 scale_x: f32,
12377 scale_y: f32,
12378 clip_mask: Option<&Mask>,
12379 cmyk_buf: Option<&mut [f32]>,
12380 icc: Option<&IccCache>,
12381) {
12382 let mut triangles = Vec::new();
12383 let scale = scale_x.max(scale_y) as f64;
12384 for patch in ¶ms.patches {
12385 if patch.points.len() >= 12 {
12386 let mut x_min = f64::INFINITY;
12388 let mut y_min = f64::INFINITY;
12389 let mut x_max = f64::NEG_INFINITY;
12390 let mut y_max = f64::NEG_INFINITY;
12391 for &(px, py) in &patch.points {
12392 let (dx, dy) = params.ctm.transform_point(px, py);
12393 x_min = x_min.min(dx);
12394 y_min = y_min.min(dy);
12395 x_max = x_max.max(dx);
12396 y_max = y_max.max(dy);
12397 }
12398 let extent = (x_max - x_min).max(y_max - y_min).abs() * scale;
12399 let n = (extent / 2.0).ceil().clamp(8.0, 64.0) as usize;
12401 let icc_profile_hash = match ¶ms.color_space {
12403 stet_graphics::device::ShadingColorSpace::ICCBased { profile_hash, .. } => {
12404 Some(profile_hash)
12405 }
12406 _ => None,
12407 };
12408 subdivide_patch_to_triangles(patch, &mut triangles, n, icc_profile_hash, icc);
12409 }
12410 }
12411 if !triangles.is_empty() {
12412 let mesh_params = MeshShadingParams {
12413 triangles,
12414 ctm: params.ctm,
12415 bbox: params.bbox,
12416 color_space: params.color_space.clone(),
12417 overprint: params.overprint,
12418 overprint_mode: params.overprint_mode,
12419 painted_channels: params.painted_channels,
12420 color_lut: params.color_lut.clone(),
12421 alpha: params.alpha,
12422 blend_mode: params.blend_mode,
12423 alpha_is_shape: params.alpha_is_shape,
12424 };
12425 render_mesh_shading(
12426 pixmap,
12427 &mesh_params,
12428 vp_x,
12429 vp_y,
12430 scale_x,
12431 scale_y,
12432 clip_mask,
12433 cmyk_buf,
12434 icc,
12435 );
12436 }
12437}
12438fn subdivide_patch_to_triangles(
12443 patch: &stet_graphics::device::ShadingPatch,
12444 triangles: &mut Vec<stet_graphics::device::ShadingTriangle>,
12445 n: usize,
12446 icc_profile_hash: Option<&stet_graphics::icc::ProfileHash>,
12447 icc_cache: Option<&IccCache>,
12448) {
12449 let mut grid: Vec<(f64, f64, DeviceColor, Vec<f64>)> = Vec::with_capacity((n + 1) * (n + 1));
12453 let use_tensor = patch.points.len() >= 16;
12454 let has_raw = !patch.raw_colors[0].is_empty();
12455 let use_icc_interp = has_raw && icc_profile_hash.is_some() && icc_cache.is_some();
12457
12458 for row in 0..=n {
12459 let v = row as f64 / n as f64;
12460 for col in 0..=n {
12461 let u = col as f64 / n as f64;
12462 let (x, y) = if use_tensor {
12463 eval_tensor_patch(patch, u, v)
12464 } else {
12465 eval_coons_patch(patch, u, v)
12466 };
12467 let raw = if has_raw {
12468 bilinear_raw(&patch.raw_colors, u, v)
12469 } else {
12470 vec![]
12471 };
12472 let color = if use_icc_interp {
12478 if let Some((r, g, b)) = icc_cache
12479 .unwrap()
12480 .convert_color_readonly(icc_profile_hash.unwrap(), &raw)
12481 {
12482 DeviceColor::from_rgb(r, g, b)
12483 } else {
12484 bilinear_color(&patch.colors, u, v)
12485 }
12486 } else {
12487 bilinear_color(&patch.colors, u, v)
12488 };
12489 grid.push((x, y, color, raw));
12490 }
12491 }
12492
12493 let cols = n + 1;
12495 for row in 0..n {
12496 for col in 0..n {
12497 let i00 = row * cols + col;
12498 let i10 = i00 + 1;
12499 let i01 = i00 + cols;
12500 let i11 = i01 + 1;
12501
12502 let (x00, y00, c00, r00) = &grid[i00];
12503 let (x10, y10, c10, r10) = &grid[i10];
12504 let (x01, y01, c01, r01) = &grid[i01];
12505 let (x11, y11, c11, r11) = &grid[i11];
12506
12507 use stet_graphics::device::ShadingVertex;
12508 triangles.push(stet_graphics::device::ShadingTriangle {
12509 v0: ShadingVertex {
12510 x: *x00,
12511 y: *y00,
12512 color: c00.clone(),
12513 raw_components: r00.clone(),
12514 },
12515 v1: ShadingVertex {
12516 x: *x10,
12517 y: *y10,
12518 color: c10.clone(),
12519 raw_components: r10.clone(),
12520 },
12521 v2: ShadingVertex {
12522 x: *x01,
12523 y: *y01,
12524 color: c01.clone(),
12525 raw_components: r01.clone(),
12526 },
12527 });
12528 triangles.push(stet_graphics::device::ShadingTriangle {
12529 v0: ShadingVertex {
12530 x: *x10,
12531 y: *y10,
12532 color: c10.clone(),
12533 raw_components: r10.clone(),
12534 },
12535 v1: ShadingVertex {
12536 x: *x11,
12537 y: *y11,
12538 color: c11.clone(),
12539 raw_components: r11.clone(),
12540 },
12541 v2: ShadingVertex {
12542 x: *x01,
12543 y: *y01,
12544 color: c01.clone(),
12545 raw_components: r01.clone(),
12546 },
12547 });
12548 }
12549 }
12550}
12551
12552fn eval_coons_patch(patch: &stet_graphics::device::ShadingPatch, u: f64, v: f64) -> (f64, f64) {
12555 let pts = &patch.points;
12556 if pts.len() < 12 {
12557 return (0.0, 0.0);
12558 }
12559
12560 let c0 = eval_cubic_bezier(pts[0], pts[1], pts[2], pts[3], u);
12565 let c2 = eval_cubic_bezier(pts[6], pts[7], pts[8], pts[9], 1.0 - u);
12566 let d0 = eval_cubic_bezier(pts[0], pts[11], pts[10], pts[9], v);
12567 let d1 = eval_cubic_bezier(pts[3], pts[4], pts[5], pts[6], v);
12568
12569 let p00 = pts[0];
12571 let p10 = pts[3];
12572 let p01 = pts[9];
12573 let p11 = pts[6];
12574 let bx = (1.0 - u) * (1.0 - v) * p00.0
12575 + u * (1.0 - v) * p10.0
12576 + (1.0 - u) * v * p01.0
12577 + u * v * p11.0;
12578 let by = (1.0 - u) * (1.0 - v) * p00.1
12579 + u * (1.0 - v) * p10.1
12580 + (1.0 - u) * v * p01.1
12581 + u * v * p11.1;
12582
12583 let x = (1.0 - v) * c0.0 + v * c2.0 + (1.0 - u) * d0.0 + u * d1.0 - bx;
12585 let y = (1.0 - v) * c0.1 + v * c2.1 + (1.0 - u) * d0.1 + u * d1.1 - by;
12586
12587 (x, y)
12588}
12589
12590fn eval_tensor_patch(patch: &stet_graphics::device::ShadingPatch, u: f64, v: f64) -> (f64, f64) {
12604 let pts = &patch.points;
12605
12606 let grid: [[usize; 4]; 4] = [[0, 1, 2, 3], [11, 12, 13, 4], [10, 15, 14, 5], [9, 8, 7, 6]];
12610
12611 let su = 1.0 - u;
12613 let bu = [su * su * su, 3.0 * su * su * u, 3.0 * su * u * u, u * u * u];
12614 let sv = 1.0 - v;
12615 let bv = [sv * sv * sv, 3.0 * sv * sv * v, 3.0 * sv * v * v, v * v * v];
12616
12617 let mut x = 0.0;
12618 let mut y = 0.0;
12619 for j in 0..4 {
12620 for i in 0..4 {
12621 let w = bu[i] * bv[j];
12622 let p = pts[grid[j][i]];
12623 x += w * p.0;
12624 y += w * p.1;
12625 }
12626 }
12627 (x, y)
12628}
12629
12630fn eval_cubic_bezier(
12632 p0: (f64, f64),
12633 p1: (f64, f64),
12634 p2: (f64, f64),
12635 p3: (f64, f64),
12636 t: f64,
12637) -> (f64, f64) {
12638 let s = 1.0 - t;
12639 let s2 = s * s;
12640 let t2 = t * t;
12641 let b0 = s2 * s;
12642 let b1 = 3.0 * s2 * t;
12643 let b2 = 3.0 * s * t2;
12644 let b3 = t2 * t;
12645 (
12646 b0 * p0.0 + b1 * p1.0 + b2 * p2.0 + b3 * p3.0,
12647 b0 * p0.1 + b1 * p1.1 + b2 * p2.1 + b3 * p3.1,
12648 )
12649}
12650
12651fn bilinear_color(colors: &[DeviceColor; 4], u: f64, v: f64) -> DeviceColor {
12653 let r = (1.0 - u) * (1.0 - v) * colors[0].r
12654 + u * (1.0 - v) * colors[1].r
12655 + (1.0 - u) * v * colors[3].r
12656 + u * v * colors[2].r;
12657 let g = (1.0 - u) * (1.0 - v) * colors[0].g
12658 + u * (1.0 - v) * colors[1].g
12659 + (1.0 - u) * v * colors[3].g
12660 + u * v * colors[2].g;
12661 let b = (1.0 - u) * (1.0 - v) * colors[0].b
12662 + u * (1.0 - v) * colors[1].b
12663 + (1.0 - u) * v * colors[3].b
12664 + u * v * colors[2].b;
12665 DeviceColor::from_rgb(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0))
12666}
12667
12668fn bilinear_raw(raw_colors: &[Vec<f64>; 4], u: f64, v: f64) -> Vec<f64> {
12670 let n = raw_colors[0].len();
12671 let mut result = vec![0.0; n];
12672 for i in 0..n {
12673 result[i] = (1.0 - u) * (1.0 - v) * raw_colors[0][i]
12674 + u * (1.0 - v) * raw_colors[1][i]
12675 + (1.0 - u) * v * raw_colors[3][i]
12676 + u * v * raw_colors[2][i];
12677 }
12678 result
12679}
12680
12681fn build_gradient_lut(stops: &[stet_graphics::device::ColorStop], size: usize) -> Vec<[u8; 4]> {
12687 let size = size.max(2);
12688 let mut lut = vec![[0u8; 4]; size];
12689 if stops.is_empty() {
12690 return lut;
12691 }
12692 let mut si = 0usize; let last = (size - 1) as f64;
12694 for i in 0..size {
12695 let t = i as f64 / last;
12696 while si + 1 < stops.len() && stops[si + 1].position < t {
12698 si += 1;
12699 }
12700 let (r, g, b) = if si + 1 >= stops.len() {
12701 let c = &stops[stops.len() - 1].color;
12702 (c.r, c.g, c.b)
12703 } else if t <= stops[si].position {
12704 let c = &stops[si].color;
12705 (c.r, c.g, c.b)
12706 } else {
12707 let t0 = stops[si].position;
12708 let t1 = stops[si + 1].position;
12709 let frac = if (t1 - t0).abs() < 1e-10 {
12710 0.0
12711 } else {
12712 (t - t0) / (t1 - t0)
12713 };
12714 let c0 = &stops[si].color;
12715 let c1 = &stops[si + 1].color;
12716 (
12717 c0.r + frac * (c1.r - c0.r),
12718 c0.g + frac * (c1.g - c0.g),
12719 c0.b + frac * (c1.b - c0.b),
12720 )
12721 };
12722 lut[i] = [
12723 (r * 255.0).round().clamp(0.0, 255.0) as u8,
12724 (g * 255.0).round().clamp(0.0, 255.0) as u8,
12725 (b * 255.0).round().clamp(0.0, 255.0) as u8,
12726 255,
12727 ];
12728 }
12729 lut
12730}
12731
12732fn build_gradient_stops(
12734 stops: &[stet_graphics::device::ColorStop],
12735) -> Vec<stet_tiny_skia::GradientStop> {
12736 let mut result = Vec::with_capacity(stops.len());
12737 for stop in stops {
12738 let r = (stop.color.r * 255.0).round().clamp(0.0, 255.0) as u8;
12739 let g = (stop.color.g * 255.0).round().clamp(0.0, 255.0) as u8;
12740 let b = (stop.color.b * 255.0).round().clamp(0.0, 255.0) as u8;
12741 result.push(stet_tiny_skia::GradientStop::new(
12742 stop.position as f32,
12743 Color::from_rgba8(r, g, b, 255),
12744 ));
12745 }
12746 result
12747}
12748
12749fn interpolate_color_stops(
12751 stops: &[stet_graphics::device::ColorStop],
12752 position: f64,
12753) -> DeviceColor {
12754 if stops.is_empty() {
12755 return DeviceColor::from_gray(0.0);
12756 }
12757 if stops.len() == 1 || position <= stops[0].position {
12758 return stops[0].color.clone();
12759 }
12760 if position >= stops.last().unwrap().position {
12761 return stops.last().unwrap().color.clone();
12762 }
12763
12764 for i in 1..stops.len() {
12766 if position <= stops[i].position {
12767 let t0 = stops[i - 1].position;
12768 let t1 = stops[i].position;
12769 let frac = if (t1 - t0).abs() < 1e-10 {
12770 0.0
12771 } else {
12772 (position - t0) / (t1 - t0)
12773 };
12774 let c0 = &stops[i - 1].color;
12775 let c1 = &stops[i].color;
12776 return DeviceColor::from_rgb(
12777 (c0.r + frac * (c1.r - c0.r)).clamp(0.0, 1.0),
12778 (c0.g + frac * (c1.g - c0.g)).clamp(0.0, 1.0),
12779 (c0.b + frac * (c1.b - c0.b)).clamp(0.0, 1.0),
12780 );
12781 }
12782 }
12783
12784 stops.last().unwrap().color.clone()
12785}
12786
12787fn interpolate_cmyk_from_stops(
12799 stops: &[stet_graphics::device::ColorStop],
12800 cs: &ShadingColorSpace,
12801 t: f64,
12802 color: &DeviceColor,
12803 icc: Option<&IccCache>,
12804) -> (f64, f64, f64, f64) {
12805 let rgb_to_cmyk = |c: &DeviceColor| -> (f64, f64, f64, f64) {
12806 if let Some(cmyk) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(c.r, c.g, c.b)) {
12807 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
12808 } else {
12809 (
12810 (1.0 - c.r).clamp(0.0, 1.0),
12811 (1.0 - c.g).clamp(0.0, 1.0),
12812 (1.0 - c.b).clamp(0.0, 1.0),
12813 0.0,
12814 )
12815 }
12816 };
12817
12818 match cs {
12819 ShadingColorSpace::DeviceCMYK
12823 | ShadingColorSpace::Separation { .. }
12824 | ShadingColorSpace::DeviceN { .. } => {
12825 if stops.len() == 1 {
12827 let rc = &stops[0].raw_components;
12828 if rc.len() >= 4 {
12829 return (rc[0], rc[1], rc[2], rc[3]);
12830 }
12831 }
12832 let mut lo = &stops[0];
12834 let mut hi = stops.last().unwrap();
12835 for i in 0..stops.len() - 1 {
12836 if stops[i + 1].position >= t {
12837 lo = &stops[i];
12838 hi = &stops[i + 1];
12839 break;
12840 }
12841 }
12842 let span = hi.position - lo.position;
12843 let frac = if span > 1e-10 {
12844 (t - lo.position) / span
12845 } else {
12846 0.0
12847 };
12848 let frac = frac.clamp(0.0, 1.0);
12849 if lo.raw_components.len() >= 4 && hi.raw_components.len() >= 4 {
12850 (
12851 lo.raw_components[0] + frac * (hi.raw_components[0] - lo.raw_components[0]),
12852 lo.raw_components[1] + frac * (hi.raw_components[1] - lo.raw_components[1]),
12853 lo.raw_components[2] + frac * (hi.raw_components[2] - lo.raw_components[2]),
12854 lo.raw_components[3] + frac * (hi.raw_components[3] - lo.raw_components[3]),
12855 )
12856 } else {
12857 rgb_to_cmyk(color)
12858 }
12859 }
12860 _ => rgb_to_cmyk(color),
12861 }
12862}
12863
12864#[allow(clippy::too_many_arguments)]
12870fn interpolate_cmyk_from_vertices(
12871 v0: &ShadingVertex,
12872 v1: &ShadingVertex,
12873 v2: &ShadingVertex,
12874 w0: f64,
12875 w1: f64,
12876 w2: f64,
12877 cs: &ShadingColorSpace,
12878 r: f64,
12879 g: f64,
12880 b: f64,
12881 icc: Option<&IccCache>,
12882) -> (f64, f64, f64, f64) {
12883 let rgb_to_cmyk = |r: f64, g: f64, b: f64| -> (f64, f64, f64, f64) {
12884 if let Some(cmyk) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(r, g, b)) {
12885 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
12886 } else {
12887 (
12888 (1.0 - r).clamp(0.0, 1.0),
12889 (1.0 - g).clamp(0.0, 1.0),
12890 (1.0 - b).clamp(0.0, 1.0),
12891 0.0,
12892 )
12893 }
12894 };
12895
12896 match cs {
12897 ShadingColorSpace::DeviceCMYK
12898 | ShadingColorSpace::Separation { .. }
12899 | ShadingColorSpace::DeviceN { .. } => {
12900 if v0.raw_components.len() >= 4
12901 && v1.raw_components.len() >= 4
12902 && v2.raw_components.len() >= 4
12903 {
12904 (
12905 w0 * v0.raw_components[0]
12906 + w1 * v1.raw_components[0]
12907 + w2 * v2.raw_components[0],
12908 w0 * v0.raw_components[1]
12909 + w1 * v1.raw_components[1]
12910 + w2 * v2.raw_components[1],
12911 w0 * v0.raw_components[2]
12912 + w1 * v1.raw_components[2]
12913 + w2 * v2.raw_components[2],
12914 w0 * v0.raw_components[3]
12915 + w1 * v1.raw_components[3]
12916 + w2 * v2.raw_components[3],
12917 )
12918 } else {
12919 rgb_to_cmyk(r, g, b)
12920 }
12921 }
12922 _ => rgb_to_cmyk(r, g, b),
12923 }
12924}
12925
12926#[cfg(test)]
12927mod tests {
12928 use super::*;
12929 use stet_graphics::color::DashPattern;
12930 use stet_graphics::device::{BgUcrState, HalftoneState, TransferState};
12931
12932 #[test]
12933 fn test_create_device() {
12934 let dev = SkiaDevice::new(100, 100);
12935 assert_eq!(dev.page_size(), (100, 100));
12936 }
12937
12938 #[test]
12939 fn test_fill_rect() {
12940 let mut dev = SkiaDevice::new(100, 100);
12941 let mut path = PsPath::new();
12942 path.segments.push(PathSegment::MoveTo(10.0, 10.0));
12943 path.segments.push(PathSegment::LineTo(90.0, 10.0));
12944 path.segments.push(PathSegment::LineTo(90.0, 90.0));
12945 path.segments.push(PathSegment::LineTo(10.0, 90.0));
12946 path.segments.push(PathSegment::ClosePath);
12947
12948 let params = FillParams {
12949 color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
12950 fill_rule: FillRule::NonZeroWinding,
12951 ctm: Matrix::identity(),
12952 is_text_glyph: false,
12953 overprint: false,
12954 overprint_mode: 0,
12955 opm_paired: false,
12956 painted_channels: 0,
12957 is_device_cmyk: false,
12958 spot_color: None,
12959 icc_color: None,
12960 rendering_intent: 0,
12961 transfer: TransferState::default(),
12962 halftone: HalftoneState::default(),
12963 bg_ucr: BgUcrState::default(),
12964 alpha: 1.0,
12965 blend_mode: 0,
12966 alpha_is_shape: false,
12967 };
12968 dev.fill_path(&path, ¶ms);
12969
12970 let pixel = dev.pixmap().pixel(50, 50).unwrap();
12972 assert_eq!(pixel.red(), 255);
12973 assert_eq!(pixel.green(), 0);
12974 assert_eq!(pixel.blue(), 0);
12975 }
12976
12977 #[test]
12978 fn test_stroke_line() {
12979 let mut dev = SkiaDevice::new(100, 100);
12980 let mut path = PsPath::new();
12981 path.segments.push(PathSegment::MoveTo(10.0, 50.0));
12982 path.segments.push(PathSegment::LineTo(90.0, 50.0));
12983
12984 let params = StrokeParams {
12985 color: DeviceColor::from_rgb(0.0, 0.0, 1.0),
12986 line_width: 4.0,
12987 line_cap: LineCap::Butt,
12988 line_join: LineJoin::Miter,
12989 miter_limit: 10.0,
12990 dash_pattern: DashPattern::solid(),
12991 ctm: Matrix::identity(),
12992 stroke_adjust: false,
12993 is_text_glyph: false,
12994 overprint: false,
12995 overprint_mode: 0,
12996 opm_paired: false,
12997 painted_channels: 0,
12998 is_device_cmyk: false,
12999 spot_color: None,
13000 icc_color: None,
13001 rendering_intent: 0,
13002 transfer: TransferState::default(),
13003 halftone: HalftoneState::default(),
13004 bg_ucr: BgUcrState::default(),
13005 alpha: 1.0,
13006 blend_mode: 0,
13007 alpha_is_shape: false,
13008 };
13009 dev.stroke_path(&path, ¶ms);
13010
13011 let pixel = dev.pixmap().pixel(50, 50).unwrap();
13013 assert_eq!(pixel.blue(), 255);
13014 }
13015
13016 #[test]
13017 fn test_clip() {
13018 let mut dev = SkiaDevice::new(100, 100);
13019
13020 let mut clip_path = PsPath::new();
13022 clip_path.segments.push(PathSegment::MoveTo(0.0, 0.0));
13023 clip_path.segments.push(PathSegment::LineTo(50.0, 0.0));
13024 clip_path.segments.push(PathSegment::LineTo(50.0, 100.0));
13025 clip_path.segments.push(PathSegment::LineTo(0.0, 100.0));
13026 clip_path.segments.push(PathSegment::ClosePath);
13027
13028 let clip_params = ClipParams {
13029 fill_rule: FillRule::NonZeroWinding,
13030 ctm: Matrix::identity(),
13031 stroke_params: None,
13032 };
13033 dev.clip_path(&clip_path, &clip_params);
13034
13035 let mut fill_path = PsPath::new();
13037 fill_path.segments.push(PathSegment::MoveTo(0.0, 0.0));
13038 fill_path.segments.push(PathSegment::LineTo(100.0, 0.0));
13039 fill_path.segments.push(PathSegment::LineTo(100.0, 100.0));
13040 fill_path.segments.push(PathSegment::LineTo(0.0, 100.0));
13041 fill_path.segments.push(PathSegment::ClosePath);
13042
13043 let fill_params = FillParams {
13044 color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
13045 fill_rule: FillRule::NonZeroWinding,
13046 ctm: Matrix::identity(),
13047 is_text_glyph: false,
13048 overprint: false,
13049 overprint_mode: 0,
13050 opm_paired: false,
13051 painted_channels: 0,
13052 is_device_cmyk: false,
13053 spot_color: None,
13054 icc_color: None,
13055 rendering_intent: 0,
13056 transfer: TransferState::default(),
13057 halftone: HalftoneState::default(),
13058 bg_ucr: BgUcrState::default(),
13059 alpha: 1.0,
13060 blend_mode: 0,
13061 alpha_is_shape: false,
13062 };
13063 dev.fill_path(&fill_path, &fill_params);
13064
13065 let left_pixel = dev.pixmap().pixel(25, 50).unwrap();
13067 assert_eq!(left_pixel.red(), 255);
13068
13069 let right_pixel = dev.pixmap().pixel(75, 50).unwrap();
13071 assert_eq!(right_pixel.red(), 255);
13072 assert_eq!(right_pixel.green(), 255); }
13074
13075 #[test]
13076 fn test_erase_page() {
13077 let mut dev = SkiaDevice::new(100, 100);
13078 let mut path = PsPath::new();
13080 path.segments.push(PathSegment::MoveTo(0.0, 0.0));
13081 path.segments.push(PathSegment::LineTo(100.0, 0.0));
13082 path.segments.push(PathSegment::LineTo(100.0, 100.0));
13083 path.segments.push(PathSegment::LineTo(0.0, 100.0));
13084 path.segments.push(PathSegment::ClosePath);
13085 let params = FillParams {
13086 color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
13087 fill_rule: FillRule::NonZeroWinding,
13088 ctm: Matrix::identity(),
13089 is_text_glyph: false,
13090 overprint: false,
13091 overprint_mode: 0,
13092 opm_paired: false,
13093 painted_channels: 0,
13094 is_device_cmyk: false,
13095 spot_color: None,
13096 icc_color: None,
13097 rendering_intent: 0,
13098 transfer: TransferState::default(),
13099 halftone: HalftoneState::default(),
13100 bg_ucr: BgUcrState::default(),
13101 alpha: 1.0,
13102 blend_mode: 0,
13103 alpha_is_shape: false,
13104 };
13105 dev.fill_path(&path, ¶ms);
13106
13107 dev.erase_page();
13108
13109 let pixel = dev.pixmap().pixel(50, 50).unwrap();
13111 assert_eq!(pixel.red(), 255);
13112 assert_eq!(pixel.green(), 255);
13113 assert_eq!(pixel.blue(), 255);
13114 }
13115
13116 #[test]
13117 fn test_show_page() {
13118 let mut dev = SkiaDevice::new(10, 10);
13119 let path = std::env::temp_dir().join("stet_test_output.png");
13120 let path_str = path.to_string_lossy();
13121 let result = dev.show_page(&path_str);
13122 assert!(result.is_ok());
13123 assert!(path.exists());
13124 std::fs::remove_file(&path).ok();
13125 }
13126
13127 #[test]
13128 fn test_transform() {
13129 let mut dev = SkiaDevice::new(200, 200);
13130 let mut path = PsPath::new();
13132 path.segments.push(PathSegment::MoveTo(0.0, 0.0));
13133 path.segments.push(PathSegment::LineTo(10.0, 0.0));
13134 path.segments.push(PathSegment::LineTo(10.0, 10.0));
13135 path.segments.push(PathSegment::LineTo(0.0, 10.0));
13136 path.segments.push(PathSegment::ClosePath);
13137
13138 let params = FillParams {
13139 color: DeviceColor::from_rgb(0.0, 1.0, 0.0),
13140 fill_rule: FillRule::NonZeroWinding,
13141 ctm: Matrix::translate(100.0, 100.0),
13142 is_text_glyph: false,
13143 overprint: false,
13144 overprint_mode: 0,
13145 opm_paired: false,
13146 painted_channels: 0,
13147 is_device_cmyk: false,
13148 spot_color: None,
13149 icc_color: None,
13150 rendering_intent: 0,
13151 transfer: TransferState::default(),
13152 halftone: HalftoneState::default(),
13153 bg_ucr: BgUcrState::default(),
13154 alpha: 1.0,
13155 blend_mode: 0,
13156 alpha_is_shape: false,
13157 };
13158 dev.fill_path(&path, ¶ms);
13159
13160 let pixel = dev.pixmap().pixel(105, 105).unwrap();
13162 assert_eq!(pixel.green(), 255);
13163 assert_eq!(pixel.red(), 0);
13164 }
13165
13166 fn make_test_fill_at(x: f64, y: f64, w: f64, h: f64) -> DisplayElement {
13167 let mut path = PsPath::new();
13168 path.segments.push(PathSegment::MoveTo(x, y));
13169 path.segments.push(PathSegment::LineTo(x + w, y));
13170 path.segments.push(PathSegment::LineTo(x + w, y + h));
13171 path.segments.push(PathSegment::LineTo(x, y + h));
13172 path.segments.push(PathSegment::ClosePath);
13173 DisplayElement::Fill {
13174 path,
13175 params: FillParams {
13176 color: DeviceColor::from_rgb(0.0, 0.0, 0.0),
13177 fill_rule: FillRule::NonZeroWinding,
13178 ctm: Matrix::identity(),
13179 is_text_glyph: false,
13180 overprint: false,
13181 overprint_mode: 0,
13182 opm_paired: false,
13183 painted_channels: 0,
13184 is_device_cmyk: false,
13185 spot_color: None,
13186 icc_color: None,
13187 rendering_intent: 0,
13188 transfer: TransferState::default(),
13189 halftone: HalftoneState::default(),
13190 bg_ucr: BgUcrState::default(),
13191 alpha: 1.0,
13192 blend_mode: 0,
13193 alpha_is_shape: false,
13194 },
13195 }
13196 }
13197
13198 #[test]
13199 fn test_compute_paint_bounds_two_fills() {
13200 let mut list = DisplayList::new();
13201 list.push(make_test_fill_at(10.0, 20.0, 30.0, 40.0)); list.push(make_test_fill_at(100.0, 50.0, 50.0, 25.0)); let bounds = compute_paint_bounds(&list, 72.0).expect("expected union bounds");
13205 assert!(
13206 (bounds.x_min - 10.0).abs() < 1e-9,
13207 "x_min was {}",
13208 bounds.x_min
13209 );
13210 assert!(
13211 (bounds.y_min - 20.0).abs() < 1e-9,
13212 "y_min was {}",
13213 bounds.y_min
13214 );
13215 assert!(
13216 (bounds.x_max - 150.0).abs() < 1e-9,
13217 "x_max was {}",
13218 bounds.x_max
13219 );
13220 assert!(
13221 (bounds.y_max - 75.0).abs() < 1e-9,
13222 "y_max was {}",
13223 bounds.y_max
13224 );
13225 }
13226
13227 #[test]
13228 fn test_compute_paint_bounds_empty_list() {
13229 let list = DisplayList::new();
13230 assert!(compute_paint_bounds(&list, 72.0).is_none());
13231 }
13232
13233 #[test]
13234 fn test_compute_paint_bounds_only_clip_returns_none() {
13235 let mut list = DisplayList::new();
13236 list.push(DisplayElement::InitClip);
13237 assert!(compute_paint_bounds(&list, 72.0).is_none());
13240 }
13241
13242 #[test]
13243 fn test_rasterize_mask_anchors_to_paint_bounds() {
13244 use stet_graphics::display_list::{SoftMaskParams, SoftMaskSubtype};
13245
13246 let mut mask = DisplayList::new();
13249 let mut path = PsPath::new();
13250 path.segments.push(PathSegment::MoveTo(200.0, 300.0));
13251 path.segments.push(PathSegment::LineTo(250.0, 300.0));
13252 path.segments.push(PathSegment::LineTo(250.0, 340.0));
13253 path.segments.push(PathSegment::LineTo(200.0, 340.0));
13254 path.segments.push(PathSegment::ClosePath);
13255 mask.push(DisplayElement::Fill {
13256 path,
13257 params: FillParams {
13258 color: DeviceColor::from_rgb(1.0, 1.0, 1.0),
13259 fill_rule: FillRule::NonZeroWinding,
13260 ctm: Matrix::identity(),
13261 is_text_glyph: false,
13262 overprint: false,
13263 overprint_mode: 0,
13264 opm_paired: false,
13265 painted_channels: 0,
13266 is_device_cmyk: false,
13267 spot_color: None,
13268 icc_color: None,
13269 rendering_intent: 0,
13270 transfer: TransferState::default(),
13271 halftone: HalftoneState::default(),
13272 bg_ucr: BgUcrState::default(),
13273 alpha: 1.0,
13274 blend_mode: 0,
13275 alpha_is_shape: false,
13276 },
13277 });
13278
13279 let params = SoftMaskParams {
13280 subtype: SoftMaskSubtype::Luminosity,
13281 bbox: [0.0, 0.0, 100.0, 100.0],
13284 backdrop_color: None, transfer_invert: false,
13286 has_nested_mask_scope: false,
13287 parent_clip_bbox: None,
13288 };
13289
13290 let raster = rasterize_mask(
13291 &mask,
13292 ¶ms,
13293 None,
13294 false,
13295 72.0,
13296 1.0,
13297 1.0,
13298 &LayerSet::new(),
13299 )
13300 .expect("expected raster");
13301
13302 assert_eq!(raster.origin_x, 199);
13305 assert_eq!(raster.origin_y, 299);
13306 assert_eq!(raster.width, 52);
13308 assert_eq!(raster.height, 42);
13309 assert_eq!(raster.scale_x, 1.0);
13310 assert_eq!(raster.scale_y, 1.0);
13311
13312 let mx = 225 - raster.origin_x;
13316 let my = 320 - raster.origin_y;
13317 assert!(mx >= 0 && (mx as u32) < raster.width);
13318 assert!(my >= 0 && (my as u32) < raster.height);
13319 let center_value = raster.data[(my as usize) * raster.width as usize + mx as usize];
13320 assert_eq!(
13321 center_value, 255,
13322 "center of painted mask should be opaque white (lum=255)"
13323 );
13324
13325 let mx_out = 300 - raster.origin_x;
13329 let in_bounds = mx_out >= 0 && (mx_out as u32) < raster.width;
13330 assert!(!in_bounds, "page x=300 should be outside the mask raster");
13331 assert_eq!(
13332 out_of_bounds_mask_value(¶ms),
13333 0,
13334 "black backdrop → out-of-bounds = 0"
13335 );
13336 }
13337
13338 #[test]
13339 fn test_band_local_to_mask_formula() {
13340 let raster_origin_x = 200i32;
13351 let raster_origin_y = 300i32;
13352
13353 let sample = |vp_x_dev: f32,
13355 vp_y_dev: f32,
13356 scale: f32,
13357 crop_x: i32,
13358 crop_y: i32,
13359 x: i32,
13360 y: i32|
13361 -> (i32, i32) {
13362 let vp_x_pixels = (vp_x_dev * scale).round() as i32;
13363 let vp_y_pixels = (vp_y_dev * scale).round() as i32;
13364 let page_x = vp_x_pixels + crop_x + x;
13365 let page_y = vp_y_pixels + crop_y + y;
13366 let mx = page_x - raster_origin_x;
13367 let my = page_y - raster_origin_y;
13368 (mx, my)
13369 };
13370
13371 let (mx, my) = sample(0.0, 0.0, 1.0, 220, 310, 0, 0);
13376 assert_eq!((mx, my), (20, 10), "top band: smask top-left");
13377
13378 let (mx, my) = sample(0.0, 0.0, 1.0, 220, 310, 5, 5);
13380 assert_eq!((mx, my), (25, 15), "top band: 5px into smask");
13381
13382 let (mx, my) = sample(0.0, 305.0, 1.0, 220, 5, 0, 0);
13390 assert_eq!((mx, my), (20, 10), "mid band: smask top-left");
13391
13392 let vp_x_pixels = (100.0_f32 * 2.0).round() as i32;
13404 let crop_x = ((220.0_f32 - 100.0) * 2.0).floor() as i32;
13405 let page_x_for_x_zero = vp_x_pixels + crop_x;
13406 assert_eq!(page_x_for_x_zero, 440, "viewport scale-2: page-x at x=0");
13407 }
13408
13409 fn x_path() -> PsPath {
13412 let mut p = PsPath::new();
13413 p.segments.push(PathSegment::MoveTo(10.0, 10.0));
13414 p.segments.push(PathSegment::LineTo(20.0, 20.0));
13415 p.segments.push(PathSegment::LineTo(30.0, 10.0));
13416 p.segments.push(PathSegment::LineTo(20.0, 0.0));
13417 p.segments.push(PathSegment::ClosePath);
13418 p
13419 }
13420
13421 fn x_path_perturbed() -> PsPath {
13422 let mut p = PsPath::new();
13425 p.segments.push(PathSegment::MoveTo(10.001, 10.0));
13426 p.segments.push(PathSegment::LineTo(20.0, 19.999));
13427 p.segments.push(PathSegment::LineTo(30.002, 10.001));
13428 p.segments.push(PathSegment::LineTo(19.999, 0.0));
13429 p.segments.push(PathSegment::ClosePath);
13430 p
13431 }
13432
13433 fn fill(path: PsPath, alpha: f64, blend: u8) -> DisplayElement {
13434 DisplayElement::Fill {
13435 path,
13436 params: FillParams {
13437 color: DeviceColor::from_rgb(0.0, 0.0, 0.0),
13438 fill_rule: FillRule::NonZeroWinding,
13439 ctm: Matrix::identity(),
13440 is_text_glyph: false,
13441 overprint: false,
13442 overprint_mode: 0,
13443 opm_paired: false,
13444 painted_channels: 0,
13445 is_device_cmyk: false,
13446 spot_color: None,
13447 icc_color: None,
13448 rendering_intent: 0,
13449 transfer: TransferState::default(),
13450 halftone: HalftoneState::default(),
13451 bg_ucr: BgUcrState::default(),
13452 alpha,
13453 blend_mode: blend,
13454 alpha_is_shape: false,
13455 },
13456 }
13457 }
13458
13459 fn rect_path(x0: f64, y0: f64, x1: f64, y1: f64) -> PsPath {
13460 let mut p = PsPath::new();
13461 p.segments.push(PathSegment::MoveTo(x0, y0));
13462 p.segments.push(PathSegment::LineTo(x1, y0));
13463 p.segments.push(PathSegment::LineTo(x1, y1));
13464 p.segments.push(PathSegment::LineTo(x0, y1));
13465 p.segments.push(PathSegment::ClosePath);
13466 p
13467 }
13468
13469 fn clip_elem(path: PsPath) -> DisplayElement {
13470 DisplayElement::Clip {
13471 path,
13472 params: ClipParams {
13473 fill_rule: FillRule::NonZeroWinding,
13474 ctm: Matrix::identity(),
13475 stroke_params: None,
13476 },
13477 }
13478 }
13479
13480 fn group_elem(
13481 inner: Vec<DisplayElement>,
13482 bbox: [f64; 4],
13483 isolated: bool,
13484 alpha: f64,
13485 blend: u8,
13486 ) -> DisplayElement {
13487 let mut dl = DisplayList::new();
13488 for e in inner {
13489 dl.push(e);
13490 }
13491 DisplayElement::Group {
13492 elements: dl,
13493 params: stet_graphics::display_list::GroupParams {
13494 bbox,
13495 isolated,
13496 knockout: false,
13497 blend_mode: blend,
13498 alpha,
13499 color_space: stet_graphics::display_list::GroupColorSpace::Inherited,
13500 },
13501 }
13502 }
13503
13504 fn dl(elements: Vec<DisplayElement>) -> DisplayList {
13505 let mut d = DisplayList::new();
13506 for e in elements {
13507 d.push(e);
13508 }
13509 d
13510 }
13511
13512 #[test]
13513 fn obscured_skip_fires_on_matching_fill_plus_iso_group() {
13514 let parent = fill(x_path(), 1.0, 0);
13517 let inner = vec![fill(x_path_perturbed(), 1.0, 0)];
13518 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13519 let d = dl(vec![
13520 parent,
13521 clip_elem(rect_path(0.0, -5.0, 40.0, 30.0)),
13522 grp,
13523 ]);
13524 assert_eq!(compute_obscured_fill_skips(&d), vec![0]);
13525 }
13526
13527 #[test]
13528 fn obscured_skip_does_not_fire_on_non_isolated_group() {
13529 let parent = fill(x_path(), 1.0, 0);
13530 let inner = vec![fill(x_path(), 1.0, 0)];
13531 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], false, 1.0, 0);
13532 let d = dl(vec![parent, grp]);
13533 assert!(compute_obscured_fill_skips(&d).is_empty());
13534 }
13535
13536 #[test]
13537 fn obscured_skip_does_not_fire_on_partial_alpha_group() {
13538 let parent = fill(x_path(), 1.0, 0);
13539 let inner = vec![fill(x_path(), 1.0, 0)];
13540 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 0.5, 0);
13541 let d = dl(vec![parent, grp]);
13542 assert!(compute_obscured_fill_skips(&d).is_empty());
13543 }
13544
13545 #[test]
13546 fn obscured_skip_does_not_fire_on_non_normal_blend() {
13547 let parent = fill(x_path(), 1.0, 0);
13548 let inner = vec![fill(x_path(), 1.0, 0)];
13549 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 10);
13552 let d = dl(vec![parent, grp]);
13553 assert!(compute_obscured_fill_skips(&d).is_empty());
13554 }
13555
13556 #[test]
13557 fn obscured_skip_does_not_fire_when_paths_differ() {
13558 let parent = fill(rect_path(0.0, 0.0, 5.0, 5.0), 1.0, 0);
13559 let inner = vec![fill(x_path(), 1.0, 0)];
13560 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13561 let d = dl(vec![parent, grp]);
13562 assert!(compute_obscured_fill_skips(&d).is_empty());
13563 }
13564
13565 #[test]
13566 fn obscured_skip_does_not_fire_when_group_bbox_too_small() {
13567 let big = rect_path(0.0, 0.0, 100.0, 100.0);
13571 let parent = fill(big.clone(), 1.0, 0);
13572 let inner = vec![fill(big, 1.0, 0)];
13573 let grp = group_elem(inner, [0.0, 0.0, 10.0, 10.0], true, 1.0, 0);
13575 let d = dl(vec![parent, grp]);
13576 assert!(compute_obscured_fill_skips(&d).is_empty());
13577 }
13578
13579 #[test]
13580 fn obscured_skip_does_not_fire_when_intervening_clip_too_small() {
13581 let parent = fill(x_path(), 1.0, 0);
13585 let narrow_clip = clip_elem(rect_path(12.0, 5.0, 18.0, 15.0));
13586 let inner = vec![fill(x_path(), 1.0, 0)];
13587 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13588 let d = dl(vec![parent, narrow_clip, grp]);
13589 assert!(compute_obscured_fill_skips(&d).is_empty());
13590 }
13591
13592 #[test]
13593 fn obscured_skip_does_not_fire_when_inner_clip_too_small() {
13594 let parent = fill(x_path(), 1.0, 0);
13596 let inner = vec![
13597 clip_elem(rect_path(12.0, 5.0, 18.0, 15.0)),
13598 fill(x_path(), 1.0, 0),
13599 ];
13600 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13601 let d = dl(vec![parent, grp]);
13602 assert!(compute_obscured_fill_skips(&d).is_empty());
13603 }
13604
13605 #[test]
13606 fn obscured_skip_fires_when_inner_clip_is_wider_than_parent_path() {
13607 let parent = fill(x_path(), 1.0, 0);
13610 let inner = vec![
13611 clip_elem(rect_path(-10.0, -10.0, 40.0, 30.0)),
13612 fill(x_path_perturbed(), 1.0, 0),
13613 ];
13614 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13615 let d = dl(vec![parent, grp]);
13616 assert_eq!(compute_obscured_fill_skips(&d), vec![0]);
13617 }
13618
13619 #[test]
13620 fn obscured_skip_does_not_fire_on_partial_alpha_parent() {
13621 let parent = fill(x_path(), 0.5, 0);
13624 let inner = vec![fill(x_path(), 1.0, 0)];
13625 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13626 let d = dl(vec![parent, grp]);
13627 assert!(compute_obscured_fill_skips(&d).is_empty());
13628 }
13629}