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 width = width.max(1);
152 let height = height.max(1);
153 let dpi = height as f64 * 72.0 / 792.0;
156
157 let pixmap = Pixmap::new(1, 1).expect("Failed to create placeholder pixmap");
161 Self {
162 pixmap,
163 page_w: width,
164 page_h: height,
165 dpi,
166 clip_region: None,
167 clip_mask_cache: HashMap::new(),
168 clip_mask_seen: HashSet::new(),
169 spare_mask: None,
170 pending_render: None,
171 sink_factory,
172 system_cmyk_bytes: None,
173 render_icc_cache: None,
174 no_aa: false,
175 use_viewport_path: false,
176 layer_set: LayerSet::new(),
177 }
178 }
179
180 pub fn set_use_viewport_path(&mut self, on: bool) {
183 self.use_viewport_path = on;
184 }
185
186 pub fn set_layer_set(&mut self, layer_set: LayerSet) {
193 self.layer_set = layer_set;
194 }
195
196 pub fn layer_set(&self) -> &LayerSet {
198 &self.layer_set
199 }
200
201 fn ensure_full_pixmap(&mut self) {
204 if self.pixmap.width() != self.page_w || self.pixmap.height() != self.page_h {
205 let Some(pixmap) = Pixmap::new(self.page_w, self.page_h) else {
211 eprintln!(
212 "Warning: could not allocate a {}x{} page pixmap; \
213 rendering into the existing {}x{} buffer instead",
214 self.page_w,
215 self.page_h,
216 self.pixmap.width(),
217 self.pixmap.height()
218 );
219 return;
220 };
221 self.pixmap = pixmap;
222 self.pixmap.fill(Color::WHITE);
223 }
224 }
225
226 pub fn pixmap(&self) -> &Pixmap {
228 &self.pixmap
229 }
230
231 pub fn set_system_cmyk_bytes(&mut self, bytes: std::sync::Arc<Vec<u8>>) {
233 self.system_cmyk_bytes = Some(bytes);
234 }
235
236 pub fn set_no_aa(&mut self, no_aa: bool) {
238 self.no_aa = no_aa;
239 }
240}
241
242fn to_transform(m: &Matrix) -> Transform {
244 Transform::from_row(
245 m.a as f32,
246 m.b as f32,
247 m.c as f32,
248 m.d as f32,
249 m.tx as f32,
250 m.ty as f32,
251 )
252}
253
254fn to_paint(color: &DeviceColor) -> Paint<'static> {
256 to_paint_alpha(color, 1.0, 0, false)
257}
258
259fn to_paint_alpha(color: &DeviceColor, alpha: f64, blend_mode: u8, no_aa: bool) -> Paint<'static> {
261 let mut paint = Paint::default();
262 let a = (alpha * 255.0).round().clamp(0.0, 255.0) as u8;
263 paint.set_color_rgba8(
264 (color.r * 255.0).round().clamp(0.0, 255.0) as u8,
265 (color.g * 255.0).round().clamp(0.0, 255.0) as u8,
266 (color.b * 255.0).round().clamp(0.0, 255.0) as u8,
267 a,
268 );
269 paint.anti_alias = !no_aa;
270 paint.blend_mode = u8_to_blend_mode(blend_mode);
271 paint
272}
273
274fn u8_to_blend_mode(mode: u8) -> BlendMode {
276 match mode {
277 1 => BlendMode::Multiply,
278 2 => BlendMode::Screen,
279 3 => BlendMode::Overlay,
280 4 => BlendMode::Darken,
281 5 => BlendMode::Lighten,
282 6 => BlendMode::ColorDodge,
283 7 => BlendMode::ColorBurn,
284 8 => BlendMode::HardLight,
285 9 => BlendMode::SoftLight,
286 10 => BlendMode::Difference,
287 11 => BlendMode::Exclusion,
288 12 => BlendMode::Hue,
289 13 => BlendMode::Saturation,
290 14 => BlendMode::Color,
291 15 => BlendMode::Luminosity,
292 _ => BlendMode::SourceOver,
293 }
294}
295
296const MAX_PATH_COORD: f32 = 1e6;
302
303fn build_skia_path(path: &PsPath) -> Option<stet_tiny_skia::Path> {
304 let mut pb = PathBuilder::new();
305
306 for seg in &path.segments {
307 match seg {
308 PathSegment::MoveTo(x, y) => {
309 pb.move_to(*x as f32, *y as f32);
310 }
311 PathSegment::LineTo(x, y) => {
312 pb.line_to(*x as f32, *y as f32);
313 }
314 PathSegment::CurveTo {
315 x1,
316 y1,
317 x2,
318 y2,
319 x3,
320 y3,
321 } => {
322 pb.cubic_to(
323 *x1 as f32, *y1 as f32, *x2 as f32, *y2 as f32, *x3 as f32, *y3 as f32,
324 );
325 }
326 PathSegment::ClosePath => {
327 pb.close();
328 }
329 }
330 }
331
332 let result = pb.finish()?;
333
334 let b = result.bounds();
338 if b.left().abs() > MAX_PATH_COORD
339 || b.top().abs() > MAX_PATH_COORD
340 || b.right().abs() > MAX_PATH_COORD
341 || b.bottom().abs() > MAX_PATH_COORD
342 {
343 return None;
344 }
345
346 Some(result)
347}
348
349fn is_degenerate_fill(path: &PsPath) -> bool {
361 let mut x_min = f64::INFINITY;
362 let mut x_max = f64::NEG_INFINITY;
363 let mut y_min = f64::INFINITY;
364 let mut y_max = f64::NEG_INFINITY;
365
366 for seg in &path.segments {
367 let (x, y) = match seg {
368 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => (*x, *y),
369 PathSegment::CurveTo { .. } => return false,
371 PathSegment::ClosePath => continue,
372 };
373 x_min = x_min.min(x);
374 x_max = x_max.max(x);
375 y_min = y_min.min(y);
376 y_max = y_max.max(y);
377 }
378
379 if x_min > x_max {
380 return false; }
382
383 let w = x_max - x_min;
384 let h = y_max - y_min;
385
386 let eps = 1e-6;
390 (w < eps && h > eps) || (h < eps && w > eps)
391}
392
393fn to_fill_rule(rule: &FillRule) -> SkiaFillRule {
398 match rule {
399 FillRule::NonZeroWinding => SkiaFillRule::Winding,
400 FillRule::EvenOdd => SkiaFillRule::EvenOdd,
401 _ => SkiaFillRule::Winding,
402 }
403}
404
405fn to_line_cap(cap: LineCap) -> SkiaLineCap {
407 match cap {
408 LineCap::Butt => SkiaLineCap::Butt,
409 LineCap::Round => SkiaLineCap::Round,
410 LineCap::Square => SkiaLineCap::Square,
411 _ => SkiaLineCap::Butt,
412 }
413}
414
415fn to_line_join(join: LineJoin) -> SkiaLineJoin {
417 match join {
418 LineJoin::Miter => SkiaLineJoin::Miter,
419 LineJoin::Round => SkiaLineJoin::Round,
420 LineJoin::Bevel => SkiaLineJoin::Bevel,
421 _ => SkiaLineJoin::Miter,
422 }
423}
424
425fn detect_rect(path: &PsPath, page_w: u32, page_h: u32) -> Option<ClipRect> {
428 let segs = &path.segments;
429 let (move_to, lines, _has_close) = match segs.len() {
433 5 => {
434 if !matches!(segs[4], PathSegment::ClosePath) {
436 return None;
437 }
438 (&segs[0], &segs[1..4], true)
439 }
440 6 => {
441 if !matches!(segs[5], PathSegment::ClosePath) {
443 return None;
444 }
445 (&segs[0], &segs[1..5], true)
446 }
447 4 => {
448 (&segs[0], &segs[1..4], false)
450 }
451 _ => return None,
452 };
453
454 let PathSegment::MoveTo(mx, my) = move_to else {
455 return None;
456 };
457
458 let mut pts = vec![(*mx, *my)];
460 for seg in lines {
461 match seg {
462 PathSegment::LineTo(x, y) => pts.push((*x, *y)),
463 _ => return None,
464 }
465 }
466
467 if pts.len() == 5 {
469 let (fx, fy) = pts[0];
470 let (lx, ly) = pts[4];
471 if (fx - lx).abs() > 0.01 || (fy - ly).abs() > 0.01 {
472 return None;
473 }
474 pts.truncate(4);
475 }
476
477 for i in 0..4 {
479 let (x1, y1) = pts[i];
480 let (x2, y2) = pts[(i + 1) % 4];
481 let dx = (x2 - x1).abs();
482 let dy = (y2 - y1).abs();
483 if dx > 0.01 && dy > 0.01 {
484 return None; }
486 }
487
488 let min_x = pts.iter().map(|p| p.0).fold(f64::INFINITY, f64::min);
490 let min_y = pts.iter().map(|p| p.1).fold(f64::INFINITY, f64::min);
491 let max_x = pts.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max);
492 let max_y = pts.iter().map(|p| p.1).fold(f64::NEG_INFINITY, f64::max);
493
494 let x0 = (min_x.floor().max(0.0) as u32).min(page_w);
496 let y0 = (min_y.floor().max(0.0) as u32).min(page_h);
497 let x1 = (max_x.ceil().max(0.0) as u32).min(page_w);
498 let y1 = (max_y.ceil().max(0.0) as u32).min(page_h);
499
500 Some(ClipRect { x0, y0, x1, y1 })
501}
502
503fn intersect_mask_with_rect(mask: &mut Mask, rect: &ClipRect, w: u32, h: u32) {
505 let data = mask.data_mut();
506 let stride = w as usize;
507
508 if rect.y0 > 0 {
510 let end = (rect.y0 as usize * stride).min(data.len());
511 data[..end].fill(0);
512 }
513
514 if rect.y1 < h {
516 let start = (rect.y1 as usize * stride).min(data.len());
517 data[start..].fill(0);
518 }
519
520 for y in rect.y0..rect.y1.min(h) {
522 let row_start = y as usize * stride;
523 if rect.x0 > 0 {
525 let end = row_start + rect.x0 as usize;
526 data[row_start..end].fill(0);
527 }
528 if rect.x1 < w {
530 let start = row_start + rect.x1 as usize;
531 let end = row_start + stride;
532 data[start..end].fill(0);
533 }
534 }
535}
536
537fn resolve_clip_mask<'a>(
542 clip_region: &'a Option<ClipRegion>,
543 temp_mask: &'a mut Option<Mask>,
544 w: u32,
545 h: u32,
546) -> Option<Option<&'a Mask>> {
547 match clip_region {
548 None => Some(None),
549 Some(ClipRegion::Mask(m)) => Some(Some(m)),
550 Some(ClipRegion::Rect(rect)) => {
551 if rect.is_empty() {
552 return None; }
554 if rect.is_full_page(w, h) {
555 return Some(None); }
557 *temp_mask = rect.make_mask(w, h);
558 Some(temp_mask.as_ref())
559 }
560 }
561}
562
563fn hash_clip_path(path: &PsPath, fill_rule: &FillRule) -> u64 {
566 let mut hasher = std::collections::hash_map::DefaultHasher::new();
567 std::mem::discriminant(fill_rule).hash(&mut hasher);
568 for seg in &path.segments {
569 match seg {
570 PathSegment::MoveTo(x, y) => {
571 0u8.hash(&mut hasher);
572 x.to_bits().hash(&mut hasher);
573 y.to_bits().hash(&mut hasher);
574 }
575 PathSegment::LineTo(x, y) => {
576 1u8.hash(&mut hasher);
577 x.to_bits().hash(&mut hasher);
578 y.to_bits().hash(&mut hasher);
579 }
580 PathSegment::CurveTo {
581 x1,
582 y1,
583 x2,
584 y2,
585 x3,
586 y3,
587 } => {
588 2u8.hash(&mut hasher);
589 x1.to_bits().hash(&mut hasher);
590 y1.to_bits().hash(&mut hasher);
591 x2.to_bits().hash(&mut hasher);
592 y2.to_bits().hash(&mut hasher);
593 x3.to_bits().hash(&mut hasher);
594 y3.to_bits().hash(&mut hasher);
595 }
596 PathSegment::ClosePath => {
597 3u8.hash(&mut hasher);
598 }
599 }
600 }
601 hasher.finish()
602}
603
604fn intersect_masks(dst: &mut Mask, src: &Mask) {
606 let dst_data = dst.data_mut();
607 let src_data = src.data();
608 for (d, s) in dst_data.iter_mut().zip(src_data.iter()) {
609 *d = ((*d as u16 * *s as u16 + 127) / 255) as u8;
610 }
611}
612
613use stet_graphics::display_list::{DisplayElement, DisplayList};
616
617struct BandState {
619 clip_region: Option<ClipRegion>,
620 spare_mask: Option<Mask>,
621 clip_mask_cache: HashMap<u64, Mask>,
623 clip_mask_seen: HashSet<u64>,
625 mask_pool: Vec<Mask>,
627 cmyk_buffer: Option<Vec<f32>>,
631 op_bg_snapshot: Option<Vec<u8>>,
638 op_touched: Option<Vec<u8>>,
643 spot_mask: Option<Vec<u8>>,
651}
652
653const MAX_POOL_MASKS: usize = 8;
656
657impl BandState {
658 #[allow(dead_code)]
660 fn recycle_cache(&mut self) {
661 for (_, mask) in self.clip_mask_cache.drain() {
662 if self.mask_pool.len() < MAX_POOL_MASKS {
663 self.mask_pool.push(mask);
664 }
665 }
667 }
668
669 fn recycle_mask(&mut self, mask: Mask) {
671 if self.mask_pool.len() < MAX_POOL_MASKS {
672 self.mask_pool.push(mask);
673 }
674 }
675
676 fn take_mask(&mut self, w: u32, h: u32) -> Mask {
678 self.spare_mask
679 .take()
680 .or_else(|| self.mask_pool.pop())
681 .unwrap_or_else(|| Mask::new(w, h).expect("Failed to create mask"))
682 }
683
684 fn take_op_buffers(&mut self, w: u32, h: u32) -> (Vec<u8>, Vec<u8>) {
689 let n = w as usize * h as usize;
690 let bg = self
691 .op_bg_snapshot
692 .take()
693 .unwrap_or_else(|| vec![0u8; n * 4]);
694 let touched = self.op_touched.take().unwrap_or_else(|| vec![0u8; n]);
695 (bg, touched)
696 }
697
698 fn restore_op_buffers(&mut self, bg: Vec<u8>, touched: Vec<u8>) {
700 self.op_bg_snapshot = Some(bg);
701 self.op_touched = Some(touched);
702 }
703
704 fn take_spot_mask(&mut self, w: u32, h: u32) -> Vec<u8> {
706 let n = w as usize * h as usize;
707 self.spot_mask.take().unwrap_or_else(|| vec![0u8; n])
708 }
709
710 fn restore_spot_mask(&mut self, mask: Vec<u8>) {
712 self.spot_mask = Some(mask);
713 }
714
715 #[allow(dead_code)]
719 fn invalidate_op_snapshot(
720 &mut self,
721 bbox_x0: usize,
722 bbox_y0: usize,
723 bbox_x1: usize,
724 bbox_y1: usize,
725 stride: usize,
726 ) {
727 if let Some(touched) = self.op_touched.as_mut() {
728 for y in bbox_y0..bbox_y1 {
729 let row = y * stride;
730 for x in bbox_x0..bbox_x1 {
731 touched[row + x] = 0;
732 }
733 }
734 }
735 }
736}
737
738struct RenderContext<'a> {
743 vp_x: f32,
745 vp_y: f32,
747 scale_x: f32,
749 scale_y: f32,
751 out_w: u32,
753 out_h: u32,
755 effective_dpi: f64,
757 icc: Option<&'a IccCache>,
759 image_cache: Option<&'a ImageCache>,
761 preprocessed: Option<&'a [Option<PreprocessedImage>]>,
763 elem_idx: usize,
765 no_aa: bool,
767 opm_zero_transparent: bool,
769 knockout_painter_pass: KnockoutPainterPass,
785 parent_group_isolated: bool,
793 alpha_extraction_pass: bool,
798 layer_set: &'a LayerSet,
801}
802
803#[derive(Clone, Copy, PartialEq, Eq)]
806enum KnockoutPainterPass {
807 None,
809 ColorPass,
811 CoveragePass,
814}
815
816impl RenderContext<'_> {
817 fn transform(&self, m: &Matrix) -> Transform {
819 viewport_transform(
820 to_transform(m),
821 self.vp_x,
822 self.vp_y,
823 self.scale_x,
824 self.scale_y,
825 )
826 }
827}
828
829struct YBBox {
831 y_min: f64,
832 y_max: f64,
833}
834
835struct ClipEpoch {
840 start_idx: usize,
842 end_idx: usize,
844 paint_bbox: Option<YBBox>,
847 has_erase_page: bool,
849}
850
851fn select_band_height(w: u32, h: u32) -> u32 {
854 if w == 0 || h == 0 {
855 return h;
856 }
857 let per_row = w as u64 * 6;
859 let budget = 2 * 1024 * 1024u64; let max_rows = budget / per_row;
861
862 let band = if max_rows >= h as u64 {
864 h
865 } else {
866 let mut p = 1u32;
867 while (p as u64) * 2 <= max_rows {
868 p *= 2;
869 }
870 p.clamp(128, h)
876 };
877
878 if h.div_ceil(band) <= 2 {
880 return h;
881 }
882 band
883}
884
885fn contains_clip_op(list: &DisplayList) -> bool {
892 list.elements().iter().any(|e| match e {
893 DisplayElement::Clip { .. } | DisplayElement::InitClip => true,
894 DisplayElement::OcgGroup { elements, .. } => contains_clip_op(elements),
895 DisplayElement::Group { elements, .. } => contains_clip_op(elements),
896 DisplayElement::SoftMasked { content, .. } => contains_clip_op(content),
897 _ => false,
898 })
899}
900
901fn precompute_bboxes(list: &DisplayList, dpi: f64) -> Vec<Option<YBBox>> {
907 list.elements()
908 .iter()
909 .map(|elem| match elem {
910 DisplayElement::Fill { path, params } => fill_device_y_bbox(path, ¶ms.ctm),
911 DisplayElement::Stroke { path, params } => stroke_device_y_bbox(path, params, dpi),
912 DisplayElement::Image { params, .. } => image_y_bbox(params),
913 DisplayElement::AxialShading { params } => {
914 shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
915 }
916 DisplayElement::RadialShading { params } => {
917 shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
918 }
919 DisplayElement::MeshShading { params } => {
920 shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
921 }
922 DisplayElement::PatchShading { params } => {
923 shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
924 }
925 DisplayElement::PatternFill { params } => pattern_fill_y_bbox(params),
926 DisplayElement::Group { params, .. } => Some(YBBox {
927 y_min: params.bbox[1],
928 y_max: params.bbox[3],
929 }),
930 DisplayElement::SoftMasked { params, .. } => Some(YBBox {
931 y_min: params.bbox[1],
932 y_max: params.bbox[3],
933 }),
934 DisplayElement::OcgGroup {
935 elements,
936 visibility,
937 } => {
938 if !visibility.default_visible() && !contains_clip_op(elements) {
944 return None;
945 }
946 let child_bboxes = precompute_bboxes(elements, dpi);
947 let mut y_min = f64::INFINITY;
948 let mut y_max = f64::NEG_INFINITY;
949 for cb in child_bboxes.into_iter().flatten() {
950 y_min = y_min.min(cb.y_min);
951 y_max = y_max.max(cb.y_max);
952 }
953 if y_min <= y_max {
954 Some(YBBox { y_min, y_max })
955 } else {
956 None
957 }
958 }
959 _ => None, })
961 .collect()
962}
963
964fn shading_y_bbox_from_bbox(bbox: &Option<[f64; 4]>, ctm: &Matrix) -> Option<YBBox> {
968 if let Some(bbox) = bbox {
969 let corners = [
970 (bbox[0], bbox[1]),
971 (bbox[2], bbox[1]),
972 (bbox[0], bbox[3]),
973 (bbox[2], bbox[3]),
974 ];
975 let mut y_min = f64::INFINITY;
976 let mut y_max = f64::NEG_INFINITY;
977 for (x, y) in &corners {
978 let (_, dy) = ctm.transform_point(*x, *y);
979 y_min = y_min.min(dy);
980 y_max = y_max.max(dy);
981 }
982 Some(YBBox { y_min, y_max })
983 } else {
984 Some(YBBox {
987 y_min: 0.0,
988 y_max: 1e9,
989 })
990 }
991}
992
993fn stroke_device_y_bbox(path: &PsPath, params: &StrokeParams, dpi: f64) -> Option<YBBox> {
1000 let m = ¶ms.ctm;
1001 let is_identity =
1002 m.a == 1.0 && m.b == 0.0 && m.c == 0.0 && m.d == 1.0 && m.tx == 0.0 && m.ty == 0.0;
1003
1004 let effective_lw = params.line_width.max(hairline_min_width(¶ms.ctm, dpi));
1006
1007 if is_identity {
1008 return path_y_bbox(path).map(|mut bbox| {
1010 let expand = effective_lw * params.miter_limit * 0.5;
1011 bbox.y_min -= expand;
1012 bbox.y_max += expand;
1013 bbox
1014 });
1015 }
1016
1017 let (mut x_min, mut x_max) = (f64::INFINITY, f64::NEG_INFINITY);
1020 let (mut y_min, mut y_max) = (f64::INFINITY, f64::NEG_INFINITY);
1021 for seg in &path.segments {
1022 match seg {
1023 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => {
1024 x_min = x_min.min(*x);
1025 x_max = x_max.max(*x);
1026 y_min = y_min.min(*y);
1027 y_max = y_max.max(*y);
1028 }
1029 PathSegment::CurveTo {
1030 x1,
1031 y1,
1032 x2,
1033 y2,
1034 x3,
1035 y3,
1036 } => {
1037 x_min = x_min.min(*x1).min(*x2).min(*x3);
1038 x_max = x_max.max(*x1).max(*x2).max(*x3);
1039 y_min = y_min.min(*y1).min(*y2).min(*y3);
1040 y_max = y_max.max(*y1).max(*y2).max(*y3);
1041 }
1042 PathSegment::ClosePath => {}
1043 }
1044 }
1045 if x_min > x_max {
1046 return None;
1047 }
1048
1049 let corners = [
1051 (x_min, y_min),
1052 (x_max, y_min),
1053 (x_min, y_max),
1054 (x_max, y_max),
1055 ];
1056 let mut dev_y_min = f64::INFINITY;
1057 let mut dev_y_max = f64::NEG_INFINITY;
1058 for (x, y) in &corners {
1059 let dy = m.b * x + m.d * y + m.ty;
1060 dev_y_min = dev_y_min.min(dy);
1061 dev_y_max = dev_y_max.max(dy);
1062 }
1063
1064 let col_y_len = (m.c * m.c + m.d * m.d).sqrt().max(1.0);
1067 let expand = effective_lw * col_y_len * params.miter_limit * 0.5;
1068 dev_y_min -= expand;
1069 dev_y_max += expand;
1070
1071 Some(YBBox {
1072 y_min: dev_y_min,
1073 y_max: dev_y_max,
1074 })
1075}
1076
1077fn fill_device_y_bbox(path: &PsPath, ctm: &Matrix) -> Option<YBBox> {
1082 let is_identity = ctm.a == 1.0
1083 && ctm.b == 0.0
1084 && ctm.c == 0.0
1085 && ctm.d == 1.0
1086 && ctm.tx == 0.0
1087 && ctm.ty == 0.0;
1088 if is_identity {
1089 return path_y_bbox(path);
1090 }
1091 let bbox = path_full_bbox(path)?;
1092 let corners = [
1093 (bbox.x_min, bbox.y_min),
1094 (bbox.x_max, bbox.y_min),
1095 (bbox.x_min, bbox.y_max),
1096 (bbox.x_max, bbox.y_max),
1097 ];
1098 let mut dev_y_min = f64::INFINITY;
1099 let mut dev_y_max = f64::NEG_INFINITY;
1100 for (x, y) in &corners {
1101 let dy = ctm.b * x + ctm.d * y + ctm.ty;
1102 dev_y_min = dev_y_min.min(dy);
1103 dev_y_max = dev_y_max.max(dy);
1104 }
1105 Some(YBBox {
1106 y_min: dev_y_min,
1107 y_max: dev_y_max,
1108 })
1109}
1110
1111fn path_y_bbox(path: &PsPath) -> Option<YBBox> {
1113 let mut y_min = f64::INFINITY;
1114 let mut y_max = f64::NEG_INFINITY;
1115 for seg in &path.segments {
1116 match seg {
1117 PathSegment::MoveTo(_, y) | PathSegment::LineTo(_, y) => {
1118 y_min = y_min.min(*y);
1119 y_max = y_max.max(*y);
1120 }
1121 PathSegment::CurveTo { y1, y2, y3, .. } => {
1122 y_min = y_min.min(*y1).min(*y2).min(*y3);
1123 y_max = y_max.max(*y1).max(*y2).max(*y3);
1124 }
1125 PathSegment::ClosePath => {}
1126 }
1127 }
1128 if y_min <= y_max {
1129 Some(YBBox { y_min, y_max })
1130 } else {
1131 None
1132 }
1133}
1134
1135fn image_y_bbox(params: &ImageParams) -> Option<YBBox> {
1137 let image_inv = params.image_matrix.invert()?;
1138 let combined = params.ctm.concat(&image_inv);
1139 let corners = [
1140 (0.0, 0.0),
1141 (params.width as f64, 0.0),
1142 (params.width as f64, params.height as f64),
1143 (0.0, params.height as f64),
1144 ];
1145 let mut y_min = f64::INFINITY;
1146 let mut y_max = f64::NEG_INFINITY;
1147 for (x, y) in &corners {
1148 let (_, dy) = combined.transform_point(*x, *y);
1149 y_min = y_min.min(dy);
1150 y_max = y_max.max(dy);
1151 }
1152 Some(YBBox { y_min, y_max })
1153}
1154
1155fn precompute_clip_seen(list: &DisplayList) -> HashSet<u64> {
1158 let mut counts: HashMap<u64, u32> = HashMap::new();
1159 for elem in list.elements() {
1160 if let DisplayElement::Clip { path, params } = elem {
1161 let hash = hash_clip_path(path, ¶ms.fill_rule);
1162 *counts.entry(hash).or_insert(0) += 1;
1163 }
1164 }
1165 counts
1166 .into_iter()
1167 .filter(|(_, c)| *c > 1)
1168 .map(|(h, _)| h)
1169 .collect()
1170}
1171
1172fn build_clip_epochs(list: &DisplayList, bboxes: &[Option<YBBox>]) -> Vec<ClipEpoch> {
1175 let elements = list.elements();
1176 let mut epochs = Vec::new();
1177 let mut epoch_start = 0;
1178 let mut y_min = f64::INFINITY;
1179 let mut y_max = f64::NEG_INFINITY;
1180 let mut has_erase = false;
1181
1182 for (i, element) in elements.iter().enumerate() {
1183 if matches!(element, DisplayElement::InitClip) && i > epoch_start {
1185 epochs.push(ClipEpoch {
1186 start_idx: epoch_start,
1187 end_idx: i,
1188 paint_bbox: if y_min <= y_max {
1189 Some(YBBox { y_min, y_max })
1190 } else {
1191 None
1192 },
1193 has_erase_page: has_erase,
1194 });
1195 epoch_start = i;
1196 y_min = f64::INFINITY;
1197 y_max = f64::NEG_INFINITY;
1198 has_erase = false;
1199 }
1200 if matches!(element, DisplayElement::ErasePage) {
1201 has_erase = true;
1202 }
1203 if let Some(ref bbox) = bboxes[i] {
1204 y_min = y_min.min(bbox.y_min);
1205 y_max = y_max.max(bbox.y_max);
1206 }
1207 }
1208 if epoch_start < elements.len() {
1210 epochs.push(ClipEpoch {
1211 start_idx: epoch_start,
1212 end_idx: elements.len(),
1213 paint_bbox: if y_min <= y_max {
1214 Some(YBBox { y_min, y_max })
1215 } else {
1216 None
1217 },
1218 has_erase_page: has_erase,
1219 });
1220 }
1221 epochs
1222}
1223
1224fn composite_onto_white(data: &mut [u8]) {
1231 for pixel in data.chunks_exact_mut(4) {
1232 let a = pixel[3] as u16;
1233 if a == 255 {
1234 continue; }
1236 let inv_a = 255 - a;
1237 pixel[0] = (pixel[0] as u16 + inv_a).min(255) as u8;
1238 pixel[1] = (pixel[1] as u16 + inv_a).min(255) as u8;
1239 pixel[2] = (pixel[2] as u16 + inv_a).min(255) as u8;
1240 pixel[3] = 255;
1241 }
1242}
1243
1244fn composite_non_isolated_group_cropped(
1253 target: &mut Pixmap,
1254 source: &Pixmap,
1255 backdrop: &[u8],
1256 params: &stet_graphics::display_list::GroupParams,
1257 clip_mask: Option<&stet_tiny_skia::Mask>,
1258 crop_x: i32,
1259 crop_y: i32,
1260) {
1261 let cw = source.width();
1262 let ch = source.height();
1263
1264 let Some(mut contribution) = Pixmap::new(cw, ch) else {
1266 return;
1267 };
1268 let src_data = source.data();
1269 let contrib_data = contribution.data_mut();
1270
1271 for (i, chunk) in contrib_data.chunks_exact_mut(4).enumerate() {
1272 let off = i * 4;
1273 if src_data[off] != backdrop[off]
1274 || src_data[off + 1] != backdrop[off + 1]
1275 || src_data[off + 2] != backdrop[off + 2]
1276 || src_data[off + 3] != backdrop[off + 3]
1277 {
1278 chunk.copy_from_slice(&src_data[off..off + 4]);
1279 }
1280 }
1281
1282 let paint = stet_tiny_skia::PixmapPaint {
1283 opacity: params.alpha as f32,
1284 blend_mode: u8_to_blend_mode(params.blend_mode),
1285 quality: stet_tiny_skia::FilterQuality::Nearest,
1286 };
1287 target.draw_pixmap(
1288 crop_x,
1289 crop_y,
1290 contribution.as_ref(),
1291 &paint,
1292 Transform::identity(),
1293 clip_mask,
1294 );
1295}
1296
1297fn composite_non_isolated_extracted(
1310 target: &mut Pixmap,
1311 source: &Pixmap,
1312 isolated: &Pixmap,
1313 backdrop: &[u8],
1314 params: &stet_graphics::display_list::GroupParams,
1315 clip_mask: Option<&stet_tiny_skia::Mask>,
1316 crop_x: i32,
1317 crop_y: i32,
1318) {
1319 let cw = source.width();
1320 let ch = source.height();
1321
1322 let Some(mut contribution) = Pixmap::new(cw, ch) else {
1323 return;
1324 };
1325 let src_data = source.data();
1326 let iso_data = isolated.data();
1327 let contrib_data = contribution.data_mut();
1328
1329 for i in 0..(cw as usize * ch as usize) {
1330 let off = i * 4;
1331 let alpha_g = iso_data[off + 3];
1332 if alpha_g == 0 {
1333 continue; }
1335
1336 let inv_alpha = 255 - alpha_g as i32;
1338 for c in 0..3 {
1339 let r = src_data[off + c] as i32;
1340 let b = backdrop[off + c] as i32;
1341 let raw = r - (b * inv_alpha + 127) / 255;
1342 contrib_data[off + c] = raw.clamp(0, 255) as u8;
1343 }
1344 contrib_data[off + 3] = alpha_g;
1345 }
1346
1347 let paint = stet_tiny_skia::PixmapPaint {
1348 opacity: params.alpha as f32,
1349 blend_mode: u8_to_blend_mode(params.blend_mode),
1350 quality: stet_tiny_skia::FilterQuality::Nearest,
1351 };
1352 target.draw_pixmap(
1353 crop_x,
1354 crop_y,
1355 contribution.as_ref(),
1356 &paint,
1357 Transform::identity(),
1358 clip_mask,
1359 );
1360}
1361
1362fn viewport_transform(t: Transform, vp_x: f32, vp_y: f32, scale_x: f32, scale_y: f32) -> Transform {
1367 Transform::from_row(
1369 t.sx * scale_x,
1370 t.ky * scale_y,
1371 t.kx * scale_x,
1372 t.sy * scale_y,
1373 (t.tx - vp_x) * scale_x,
1374 (t.ty - vp_y) * scale_y,
1375 )
1376}
1377
1378fn box_resample(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
1385 if dw == 0 || dh == 0 {
1386 return Vec::new();
1387 }
1388 let (sw, sh, dw, dh) = (sw as usize, sh as usize, dw as usize, dh as usize);
1389
1390 let ratio_x = sw as f32 / dw as f32;
1394 let mut tmp = vec![0.0f32; dw * sh * 4];
1395 let tmp_stride = dw * 4;
1396
1397 for y in 0..sh {
1398 let row_off = y * sw * 4;
1399 let dst_row = y * tmp_stride;
1400 for dx in 0..dw {
1401 let left_f = dx as f32 * ratio_x;
1402 let right_f = (dx + 1) as f32 * ratio_x;
1403 let left = (left_f as usize).min(sw - 1);
1404 let right = (right_f.ceil() as usize).min(sw);
1405 let inv_area = 1.0 / (right_f - left_f);
1406 let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0, 0.0, 0.0);
1407 for sx in left..right {
1408 let pixel_left = sx as f32;
1410 let pixel_right = (sx + 1) as f32;
1411 let w = pixel_right.min(right_f) - pixel_left.max(left_f);
1412 let i = row_off + sx * 4;
1413 r += src[i] as f32 * w;
1414 g += src[i + 1] as f32 * w;
1415 b += src[i + 2] as f32 * w;
1416 a += src[i + 3] as f32 * w;
1417 }
1418 let di = dst_row + dx * 4;
1419 tmp[di] = r * inv_area;
1420 tmp[di + 1] = g * inv_area;
1421 tmp[di + 2] = b * inv_area;
1422 tmp[di + 3] = a * inv_area;
1423 }
1424 }
1425
1426 let ratio_y = sh as f32 / dh as f32;
1428 let mut out = vec![0u8; dw * dh * 4];
1429 let out_stride = dw * 4;
1430
1431 for dy in 0..dh {
1432 let top_f = dy as f32 * ratio_y;
1433 let bottom_f = (dy + 1) as f32 * ratio_y;
1434 let top = (top_f as usize).min(sh - 1);
1435 let bottom = (bottom_f.ceil() as usize).min(sh);
1436 let inv_area = 1.0 / (bottom_f - top_f);
1437
1438 let n_rows = bottom - top;
1440 let mut row_weights_buf: [(usize, f32); 8] = [(0, 0.0); 8];
1441 let row_weights_vec: Vec<(usize, f32)>;
1442 let row_weights: &[(usize, f32)] = if n_rows <= 8 {
1443 for (i, sy) in (top..bottom).enumerate() {
1444 let pixel_top = sy as f32;
1445 let pixel_bottom = (sy + 1) as f32;
1446 let w = pixel_bottom.min(bottom_f) - pixel_top.max(top_f);
1447 row_weights_buf[i] = (sy, w);
1448 }
1449 &row_weights_buf[..n_rows]
1450 } else {
1451 row_weights_vec = (top..bottom)
1452 .map(|sy| {
1453 let pixel_top = sy as f32;
1454 let pixel_bottom = (sy + 1) as f32;
1455 let w = pixel_bottom.min(bottom_f) - pixel_top.max(top_f);
1456 (sy, w)
1457 })
1458 .collect();
1459 &row_weights_vec
1460 };
1461
1462 let dst_row = dy * out_stride;
1463 for dx in 0..dw {
1464 let col = dx * 4;
1465 let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0, 0.0, 0.0);
1466 for &(sy, w) in row_weights {
1467 let i = sy * tmp_stride + col;
1468 r += tmp[i] * w;
1469 g += tmp[i + 1] * w;
1470 b += tmp[i + 2] * w;
1471 a += tmp[i + 3] * w;
1472 }
1473 let di = dst_row + col;
1474 out[di] = (r * inv_area + 0.5).clamp(0.0, 255.0) as u8;
1475 out[di + 1] = (g * inv_area + 0.5).clamp(0.0, 255.0) as u8;
1476 out[di + 2] = (b * inv_area + 0.5).clamp(0.0, 255.0) as u8;
1477 out[di + 3] = (a * inv_area + 0.5).clamp(0.0, 255.0) as u8;
1478 }
1479 }
1480
1481 out
1482}
1483
1484fn bicubic_resample(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
1491 if dw == 0 || dh == 0 {
1492 return Vec::new();
1493 }
1494
1495 let (sw, sh, dw, dh) = (sw as usize, sh as usize, dw as usize, dh as usize);
1496 let ratio_x = sw as f32 / dw as f32;
1497 let ratio_y = sh as f32 / dh as f32;
1498
1499 let mut tmp = vec![0.0f32; dw * sh * 4];
1501 for y in 0..sh {
1502 let src_row = y * sw * 4;
1503 let dst_row = y * dw * 4;
1504 for dx in 0..dw {
1505 let sx = (dx as f32 + 0.5) * ratio_x - 0.5;
1506 let sx_floor = sx.floor() as i32;
1507 let fx = sx - sx_floor as f32;
1508 let w0 = catmull_rom(fx + 1.0);
1509 let w1 = catmull_rom(fx);
1510 let w2 = catmull_rom(1.0 - fx);
1511 let w3 = catmull_rom(2.0 - fx);
1512 let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0, 0.0, 0.0);
1513 for (k, w) in [
1514 (sx_floor - 1, w0),
1515 (sx_floor, w1),
1516 (sx_floor + 1, w2),
1517 (sx_floor + 2, w3),
1518 ] {
1519 let px = k.clamp(0, sw as i32 - 1) as usize;
1520 let i = src_row + px * 4;
1521 r += src[i] as f32 * w;
1522 g += src[i + 1] as f32 * w;
1523 b += src[i + 2] as f32 * w;
1524 a += src[i + 3] as f32 * w;
1525 }
1526 let di = dst_row + dx * 4;
1527 tmp[di] = r;
1528 tmp[di + 1] = g;
1529 tmp[di + 2] = b;
1530 tmp[di + 3] = a;
1531 }
1532 }
1533
1534 let mut out = vec![0u8; dw * dh * 4];
1537 let tmp_stride = dw * 4;
1538 let out_stride = dw * 4;
1539 for dy in 0..dh {
1540 let sy = (dy as f32 + 0.5) * ratio_y - 0.5;
1541 let sy_floor = sy.floor() as i32;
1542 let fy = sy - sy_floor as f32;
1543 let w0 = catmull_rom(fy + 1.0);
1544 let w1 = catmull_rom(fy);
1545 let w2 = catmull_rom(1.0 - fy);
1546 let w3 = catmull_rom(2.0 - fy);
1547 let py0 = (sy_floor - 1).clamp(0, sh as i32 - 1) as usize * tmp_stride;
1548 let py1 = sy_floor.clamp(0, sh as i32 - 1) as usize * tmp_stride;
1549 let py2 = (sy_floor + 1).clamp(0, sh as i32 - 1) as usize * tmp_stride;
1550 let py3 = (sy_floor + 2).clamp(0, sh as i32 - 1) as usize * tmp_stride;
1551 let dst_row = dy * out_stride;
1552 for dx in 0..dw {
1553 let col = dx * 4;
1554 let r = tmp[py0 + col] * w0
1555 + tmp[py1 + col] * w1
1556 + tmp[py2 + col] * w2
1557 + tmp[py3 + col] * w3;
1558 let g = tmp[py0 + col + 1] * w0
1559 + tmp[py1 + col + 1] * w1
1560 + tmp[py2 + col + 1] * w2
1561 + tmp[py3 + col + 1] * w3;
1562 let b = tmp[py0 + col + 2] * w0
1563 + tmp[py1 + col + 2] * w1
1564 + tmp[py2 + col + 2] * w2
1565 + tmp[py3 + col + 2] * w3;
1566 let a = tmp[py0 + col + 3] * w0
1567 + tmp[py1 + col + 3] * w1
1568 + tmp[py2 + col + 3] * w2
1569 + tmp[py3 + col + 3] * w3;
1570 let di = dst_row + col;
1571 out[di] = r.round().clamp(0.0, 255.0) as u8;
1572 out[di + 1] = g.round().clamp(0.0, 255.0) as u8;
1573 out[di + 2] = b.round().clamp(0.0, 255.0) as u8;
1574 out[di + 3] = a.round().clamp(0.0, 255.0) as u8;
1575 }
1576 }
1577
1578 out
1579}
1580
1581#[inline]
1583fn catmull_rom(t: f32) -> f32 {
1584 let t = t.abs();
1585 if t < 1.0 {
1586 (1.5 * t - 2.5) * t * t + 1.0
1587 } else if t < 2.0 {
1588 ((-0.5 * t + 2.5) * t - 4.0) * t + 2.0
1589 } else {
1590 0.0
1591 }
1592}
1593
1594pub fn build_icc_cache_for_list(
1612 list: &DisplayList,
1613 system_cmyk_bytes: Option<&std::sync::Arc<Vec<u8>>>,
1614 proofing_enabled: bool,
1615) -> IccCache {
1616 let mut cache = IccCache::new();
1617 let mut seen = HashSet::new();
1618
1619 if let Some(cmyk_bytes) = system_cmyk_bytes
1622 && let Some(hash) = cache.register_profile(cmyk_bytes)
1623 {
1624 seen.insert(hash);
1625 cache.set_default_cmyk_hash(hash);
1627 cache.prepare_reverse_cmyk();
1631 cache.prepare_lab_to_oi_cmyk();
1635 }
1636
1637 cache.set_proofing_enabled(proofing_enabled);
1642
1643 fn scan_elements(
1645 elements: &[DisplayElement],
1646 seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
1647 cache: &mut IccCache,
1648 ) {
1649 for element in elements {
1650 if let DisplayElement::Group { elements: sub, .. } = element {
1652 scan_elements(sub.elements(), seen, cache);
1653 }
1654 if let DisplayElement::SoftMasked { content, mask, .. } = element {
1655 scan_elements(content.elements(), seen, cache);
1656 scan_elements(mask.elements(), seen, cache);
1657 }
1658 if let DisplayElement::OcgGroup { elements: sub, .. } = element {
1659 scan_elements(sub.elements(), seen, cache);
1660 }
1661 let shading_cs = match element {
1663 DisplayElement::AxialShading { params } => Some(¶ms.color_space),
1664 DisplayElement::RadialShading { params } => Some(¶ms.color_space),
1665 DisplayElement::MeshShading { params } => Some(¶ms.color_space),
1666 DisplayElement::PatchShading { params } => Some(¶ms.color_space),
1667 _ => None,
1668 };
1669 if let Some(stet_graphics::device::ShadingColorSpace::ICCBased {
1670 n,
1671 profile_hash,
1672 profile_data,
1673 }) = shading_cs
1674 {
1675 if seen.insert(*profile_hash) {
1676 cache.register_profile_with_n(profile_data, Some(*n));
1677 }
1678 }
1679 if let DisplayElement::Image { params, .. } = element {
1681 match ¶ms.color_space {
1682 ImageColorSpace::ICCBased {
1683 n,
1684 profile_hash,
1685 profile_data,
1686 } if seen.insert(*profile_hash) => {
1687 cache.register_profile_with_n(profile_data, Some(*n));
1688 }
1689 ImageColorSpace::Indexed { base, .. }
1690 if matches!(base.as_ref(), ImageColorSpace::ICCBased { .. }) =>
1691 {
1692 if let ImageColorSpace::ICCBased {
1693 n,
1694 profile_hash,
1695 profile_data,
1696 } = base.as_ref()
1697 {
1698 if seen.insert(*profile_hash) {
1699 cache.register_profile_with_n(profile_data, Some(*n));
1700 }
1701 }
1702 }
1703 _ => {}
1704 }
1705 }
1706 }
1707 }
1708 scan_elements(list.elements(), &mut seen, &mut cache);
1709
1710 cache
1711}
1712
1713fn register_shading_icc_profiles(list: &DisplayList, cache: &mut IccCache) {
1718 fn register_image_iccs(
1719 cs: &ImageColorSpace,
1720 seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
1721 cache: &mut IccCache,
1722 ) {
1723 match cs {
1724 ImageColorSpace::ICCBased {
1725 n,
1726 profile_hash,
1727 profile_data,
1728 } => {
1729 if seen.insert(*profile_hash) {
1730 cache.register_profile_with_n(profile_data, Some(*n));
1731 }
1732 }
1733 ImageColorSpace::Indexed { base, .. } => register_image_iccs(base, seen, cache),
1734 ImageColorSpace::Separation { alt_space, .. }
1735 | ImageColorSpace::DeviceN { alt_space, .. } => {
1736 register_image_iccs(alt_space, seen, cache)
1737 }
1738 _ => {}
1739 }
1740 }
1741 fn scan(
1742 elements: &[DisplayElement],
1743 seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
1744 cache: &mut IccCache,
1745 ) {
1746 for element in elements {
1747 if let DisplayElement::Group { elements: sub, .. } = element {
1748 scan(sub.elements(), seen, cache);
1749 }
1750 if let DisplayElement::SoftMasked { content, mask, .. } = element {
1751 scan(content.elements(), seen, cache);
1752 scan(mask.elements(), seen, cache);
1753 }
1754 if let DisplayElement::OcgGroup { elements: sub, .. } = element {
1755 scan(sub.elements(), seen, cache);
1756 }
1757 let shading_cs = match element {
1758 DisplayElement::AxialShading { params } => Some(¶ms.color_space),
1759 DisplayElement::RadialShading { params } => Some(¶ms.color_space),
1760 DisplayElement::MeshShading { params } => Some(¶ms.color_space),
1761 DisplayElement::PatchShading { params } => Some(¶ms.color_space),
1762 _ => None,
1763 };
1764 if let Some(stet_graphics::device::ShadingColorSpace::ICCBased {
1765 n,
1766 profile_hash,
1767 profile_data,
1768 }) = shading_cs
1769 && seen.insert(*profile_hash)
1770 {
1771 cache.register_profile_with_n(profile_data, Some(*n));
1772 }
1773 if let DisplayElement::Image { params, .. } = element {
1774 register_image_iccs(¶ms.color_space, seen, cache);
1775 }
1776 }
1777 }
1778 let mut seen = HashSet::new();
1779 scan(list.elements(), &mut seen, cache);
1780}
1781
1782fn samples_to_rgba(
1786 data: &[u8],
1787 params: &ImageParams,
1788 icc: Option<&IccCache>,
1789 opm_zero_transparent: bool,
1790) -> Vec<u8> {
1791 let w = params.width as usize;
1792 let h = params.height as usize;
1793 let npixels = w * h;
1794 let bpc = params.bits_per_component;
1795 match ¶ms.color_space {
1796 ImageColorSpace::PreconvertedRGBA => {
1797 data.to_vec()
1799 }
1800 ImageColorSpace::DeviceGray => {
1801 let mut rgba = vec![255u8; npixels * 4];
1802 if bpc == 16 {
1803 for i in 0..npixels {
1804 let g = data.get(i * 2).copied().unwrap_or(0);
1805 let pi = i * 4;
1806 rgba[pi] = g;
1807 rgba[pi + 1] = g;
1808 rgba[pi + 2] = g;
1809 }
1810 } else {
1811 for i in 0..npixels {
1812 let g = data.get(i).copied().unwrap_or(0);
1813 let pi = i * 4;
1814 rgba[pi] = g;
1815 rgba[pi + 1] = g;
1816 rgba[pi + 2] = g;
1817 }
1818 }
1819 rgba
1820 }
1821 ImageColorSpace::DeviceRGB => {
1822 let mut rgba = vec![255u8; npixels * 4];
1823 if bpc == 16 {
1824 for i in 0..npixels {
1827 let si = i * 6;
1828 let pi = i * 4;
1829 rgba[pi] = data.get(si).copied().unwrap_or(0);
1830 rgba[pi + 1] = data.get(si + 2).copied().unwrap_or(0);
1831 rgba[pi + 2] = data.get(si + 4).copied().unwrap_or(0);
1832 }
1833 } else {
1834 for i in 0..npixels {
1835 let si = i * 3;
1836 let pi = i * 4;
1837 rgba[pi] = data.get(si).copied().unwrap_or(0);
1838 rgba[pi + 1] = data.get(si + 1).copied().unwrap_or(0);
1839 rgba[pi + 2] = data.get(si + 2).copied().unwrap_or(0);
1840 }
1841 }
1842 rgba
1843 }
1844 ImageColorSpace::DeviceCMYK => {
1845 if let Some(cache) = icc
1849 && let Some(cmyk_hash) = cache.default_cmyk_hash()
1850 {
1851 let avail_pixels = data.len() / 4;
1852 let icc_pixels = avail_pixels.min(npixels);
1853 if icc_pixels > 0
1854 && let Some(rgb) = cache.convert_image_8bit(cmyk_hash, data, icc_pixels)
1855 {
1856 let mut rgba = vec![255u8; npixels * 4];
1857 for i in 0..icc_pixels {
1858 rgba[i * 4] = rgb[i * 3];
1859 rgba[i * 4 + 1] = rgb[i * 3 + 1];
1860 rgba[i * 4 + 2] = rgb[i * 3 + 2];
1861 if opm_zero_transparent {
1863 let si = i * 4;
1864 if data[si] == 0
1865 && data[si + 1] == 0
1866 && data[si + 2] == 0
1867 && data[si + 3] == 0
1868 {
1869 rgba[i * 4 + 3] = 0;
1870 }
1871 }
1872 }
1873 return rgba;
1875 }
1876 }
1877 let mut rgba = vec![255u8; npixels * 4];
1879 for i in 0..npixels {
1880 let si = i * 4;
1881 let c = data.get(si).copied().unwrap_or(0) as f64 / 255.0;
1882 let m = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0;
1883 let y = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0;
1884 let k = data.get(si + 3).copied().unwrap_or(0) as f64 / 255.0;
1885 let r = (1.0 - c.min(1.0)) * (1.0 - k.min(1.0));
1886 let g = (1.0 - m.min(1.0)) * (1.0 - k.min(1.0));
1887 let b = (1.0 - y.min(1.0)) * (1.0 - k.min(1.0));
1888 let pi = i * 4;
1889 rgba[pi] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
1890 rgba[pi + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
1891 rgba[pi + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
1892 if opm_zero_transparent
1894 && data.get(si).copied().unwrap_or(0) == 0
1895 && data.get(si + 1).copied().unwrap_or(0) == 0
1896 && data.get(si + 2).copied().unwrap_or(0) == 0
1897 && data.get(si + 3).copied().unwrap_or(0) == 0
1898 {
1899 rgba[pi + 3] = 0;
1900 }
1901 }
1902 rgba
1903 }
1904 ImageColorSpace::ICCBased {
1905 n,
1906 profile_hash,
1907 profile_data,
1908 } => {
1909 let intent = stet_graphics::icc::intent_from_pdf_byte(params.rendering_intent);
1920 if let Some(cache) = icc
1921 && cache.has_profile(profile_hash)
1922 && let Some(rgb) =
1923 cache.convert_image_8bit_with_intent(profile_hash, data, npixels, intent)
1924 {
1925 let mut rgba = vec![255u8; npixels * 4];
1926 for i in 0..npixels {
1927 rgba[i * 4] = rgb[i * 3];
1928 rgba[i * 4 + 1] = rgb[i * 3 + 1];
1929 rgba[i * 4 + 2] = rgb[i * 3 + 2];
1930 if opm_zero_transparent && *n == 4 {
1932 let si = i * *n as usize;
1933 if si + 3 < data.len()
1934 && data[si] == 0
1935 && data[si + 1] == 0
1936 && data[si + 2] == 0
1937 && data[si + 3] == 0
1938 {
1939 rgba[i * 4 + 3] = 0;
1940 }
1941 }
1942 }
1943 return rgba;
1944 }
1945 let _ = (profile_hash, profile_data);
1947 let fallback = match n {
1948 1 => ImageColorSpace::DeviceGray,
1949 4 => ImageColorSpace::DeviceCMYK,
1950 _ => ImageColorSpace::DeviceRGB,
1951 };
1952 let p = ImageParams {
1953 color_space: fallback,
1954 bits_per_component: 8,
1955 ..params.clone()
1956 };
1957 samples_to_rgba(data, &p, icc, opm_zero_transparent)
1958 }
1959 ImageColorSpace::Indexed {
1960 base,
1961 hival,
1962 lookup,
1963 } => {
1964 let base_ncomp = base.num_components() as usize;
1965 let mut expanded = Vec::with_capacity(npixels * base_ncomp);
1967 for i in 0..npixels {
1968 let idx = data.get(i).copied().unwrap_or(0) as usize;
1969 let idx = idx.min(*hival as usize);
1970 let offset = idx * base_ncomp;
1971 for c in 0..base_ncomp {
1972 expanded.push(lookup.get(offset + c).copied().unwrap_or(0));
1973 }
1974 }
1975 let p = ImageParams {
1976 color_space: *base.clone(),
1977 bits_per_component: 8,
1978 ..params.clone()
1979 };
1980 samples_to_rgba(&expanded, &p, icc, opm_zero_transparent)
1981 }
1982 ImageColorSpace::CIEBasedABC { params: cie_params } => {
1983 let mut rgba = vec![255u8; npixels * 4];
1984 for i in 0..npixels {
1985 let si = i * 3;
1986 let a = data.get(si).copied().unwrap_or(0) as f64 / 255.0;
1987 let b = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0;
1988 let c = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0;
1989 let color = DeviceColor::from_cie_abc(a, b, c, cie_params);
1990 let pi = i * 4;
1991 rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
1992 rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
1993 rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
1994 }
1995 rgba
1996 }
1997 ImageColorSpace::CIEBasedA { params: cie_params } => {
1998 let mut rgba = vec![255u8; npixels * 4];
1999 for i in 0..npixels {
2000 let val = data.get(i).copied().unwrap_or(0) as f64 / 255.0;
2001 let color = DeviceColor::from_cie_a(val, cie_params);
2002 let pi = i * 4;
2003 rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
2004 rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
2005 rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
2006 }
2007 rgba
2008 }
2009 ImageColorSpace::Lab { range, .. } => {
2010 let mut rgba = vec![255u8; npixels * 4];
2011 let a_span = range[1] - range[0];
2012 let b_span = range[3] - range[2];
2013 for i in 0..npixels {
2014 let si = i * 3;
2015 let l = data.get(si).copied().unwrap_or(0) as f64 / 255.0 * 100.0;
2016 let a = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0 * a_span + range[0];
2017 let b = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0 * b_span + range[2];
2018 let color = DeviceColor::from_lab(l, a, b, range);
2019 let pi = i * 4;
2020 rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
2021 rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
2022 rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
2023 }
2024 rgba
2025 }
2026 ImageColorSpace::Separation {
2027 alt_space,
2028 tint_table,
2029 ..
2030 } => {
2031 if matches!(alt_space.as_ref(), ImageColorSpace::DeviceCMYK)
2034 && let Some(rgba) = tint_separation_via_icc(data, npixels, tint_table, icc)
2035 {
2036 return rgba;
2037 }
2038 let mut rgba = vec![255u8; npixels * 4];
2039 let no = tint_table.num_outputs as usize;
2040 let mut alt_comps = vec![0.0f32; no];
2041 for i in 0..npixels {
2042 let tint = data.get(i).copied().unwrap_or(0) as f32 / 255.0;
2043 tint_table.lookup_1d(tint, &mut alt_comps);
2044 let (r, g, b) = alt_comps_to_rgb(&alt_comps, alt_space);
2045 let pi = i * 4;
2046 rgba[pi] = r;
2047 rgba[pi + 1] = g;
2048 rgba[pi + 2] = b;
2049 }
2050 rgba
2051 }
2052 ImageColorSpace::DeviceN {
2053 alt_space,
2054 tint_table,
2055 ..
2056 } => {
2057 let ni = tint_table.num_inputs as usize;
2058 let no = tint_table.num_outputs as usize;
2059 if matches!(alt_space.as_ref(), ImageColorSpace::DeviceCMYK)
2061 && let Some(rgba) = tint_devicen_via_icc(data, npixels, ni, tint_table, icc)
2062 {
2063 return rgba;
2064 }
2065 let mut rgba = vec![255u8; npixels * 4];
2066 let mut inputs = vec![0.0f32; ni];
2067 let mut alt_comps = vec![0.0f32; no];
2068 for i in 0..npixels {
2069 let si = i * ni;
2070 for (c, inp) in inputs.iter_mut().enumerate() {
2071 *inp = data.get(si + c).copied().unwrap_or(0) as f32 / 255.0;
2072 }
2073 tint_table.lookup_nd(&inputs, &mut alt_comps);
2074 let (r, g, b) = alt_comps_to_rgb(&alt_comps, alt_space);
2075 let pi = i * 4;
2076 rgba[pi] = r;
2077 rgba[pi + 1] = g;
2078 rgba[pi + 2] = b;
2079 }
2080 rgba
2081 }
2082 ImageColorSpace::Mask {
2083 color, polarity, ..
2084 } => {
2085 let mut rgba = vec![0u8; npixels * 4];
2086 let r = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
2087 let g = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
2088 let b = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
2089 let bytes_per_row = (w).div_ceil(8);
2090 for row in 0..h {
2091 for col in 0..w {
2092 let byte_idx = row * bytes_per_row + col / 8;
2093 let bit_offset = 7 - (col % 8);
2094 let bit = if byte_idx < data.len() {
2095 (data[byte_idx] >> bit_offset) & 1
2096 } else {
2097 0
2098 };
2099 let paint = if *polarity { bit == 1 } else { bit == 0 };
2100 if paint {
2101 let pi = (row * w + col) * 4;
2102 rgba[pi] = r;
2103 rgba[pi + 1] = g;
2104 rgba[pi + 2] = b;
2105 rgba[pi + 3] = 255;
2106 }
2107 }
2108 }
2109 rgba
2110 }
2111 _ => vec![0u8; npixels * 4],
2112 }
2113}
2114
2115fn tint_separation_via_icc(
2118 data: &[u8],
2119 npixels: 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];
2127 let mut alt_comps = [0.0f32; 4];
2128 for i in 0..npixels {
2129 let tint = data.get(i).copied().unwrap_or(0) as f32 / 255.0;
2130 tint_table.lookup_1d(tint, &mut alt_comps);
2131 let si = i * 4;
2132 cmyk_data[si] = (alt_comps[0].clamp(0.0, 1.0) * 255.0).round() as u8;
2133 cmyk_data[si + 1] = (alt_comps[1].clamp(0.0, 1.0) * 255.0).round() as u8;
2134 cmyk_data[si + 2] = (alt_comps[2].clamp(0.0, 1.0) * 255.0).round() as u8;
2135 cmyk_data[si + 3] = (alt_comps[3].clamp(0.0, 1.0) * 255.0).round() as u8;
2136 }
2137 let rgb = cache.convert_image_8bit(cmyk_hash, &cmyk_data, npixels)?;
2138 let mut rgba = vec![255u8; npixels * 4];
2139 for i in 0..npixels {
2140 rgba[i * 4] = rgb[i * 3];
2141 rgba[i * 4 + 1] = rgb[i * 3 + 1];
2142 rgba[i * 4 + 2] = rgb[i * 3 + 2];
2143 }
2144 Some(rgba)
2145}
2146
2147fn tint_devicen_via_icc(
2149 data: &[u8],
2150 npixels: usize,
2151 ni: usize,
2152 tint_table: &TintLookupTable,
2153 icc: Option<&IccCache>,
2154) -> Option<Vec<u8>> {
2155 let cache = icc?;
2156 let cmyk_hash = cache.default_cmyk_hash()?;
2157 let mut cmyk_data = vec![0u8; npixels * 4];
2158 let mut inputs = vec![0.0f32; ni];
2159 let mut alt_comps = [0.0f32; 4];
2160 for i in 0..npixels {
2161 let si = i * ni;
2162 for (c, inp) in inputs.iter_mut().enumerate() {
2163 *inp = data.get(si + c).copied().unwrap_or(0) as f32 / 255.0;
2164 }
2165 tint_table.lookup_nd(&inputs, &mut alt_comps);
2166 let di = i * 4;
2167 cmyk_data[di] = (alt_comps[0].clamp(0.0, 1.0) * 255.0).round() as u8;
2168 cmyk_data[di + 1] = (alt_comps[1].clamp(0.0, 1.0) * 255.0).round() as u8;
2169 cmyk_data[di + 2] = (alt_comps[2].clamp(0.0, 1.0) * 255.0).round() as u8;
2170 cmyk_data[di + 3] = (alt_comps[3].clamp(0.0, 1.0) * 255.0).round() as u8;
2171 }
2172 let rgb = cache.convert_image_8bit(cmyk_hash, &cmyk_data, npixels)?;
2173 let mut rgba = vec![255u8; npixels * 4];
2174 for i in 0..npixels {
2175 rgba[i * 4] = rgb[i * 3];
2176 rgba[i * 4 + 1] = rgb[i * 3 + 1];
2177 rgba[i * 4 + 2] = rgb[i * 3 + 2];
2178 }
2179 Some(rgba)
2180}
2181
2182fn alt_comps_to_rgb(comps: &[f32], alt_space: &ImageColorSpace) -> (u8, u8, u8) {
2184 match alt_space {
2185 ImageColorSpace::DeviceGray => {
2186 let g = (comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2187 (g, g, g)
2188 }
2189 ImageColorSpace::DeviceRGB => {
2190 let r = (comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2191 let g = (comps.get(1).copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2192 let b = (comps.get(2).copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2193 (r, g, b)
2194 }
2195 ImageColorSpace::DeviceCMYK => {
2196 let c = comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0);
2197 let m = comps.get(1).copied().unwrap_or(0.0).clamp(0.0, 1.0);
2198 let y = comps.get(2).copied().unwrap_or(0.0).clamp(0.0, 1.0);
2199 let k = comps.get(3).copied().unwrap_or(0.0).clamp(0.0, 1.0);
2200 let r = ((1.0 - (c + k).min(1.0)) * 255.0).round() as u8;
2201 let g = ((1.0 - (m + k).min(1.0)) * 255.0).round() as u8;
2202 let b = ((1.0 - (y + k).min(1.0)) * 255.0).round() as u8;
2203 (r, g, b)
2204 }
2205 _ => (0, 0, 0),
2206 }
2207}
2208
2209fn apply_mask_color_rgba(rgba: &mut [u8], sample_data: &[u8], params: &ImageParams) {
2211 let mask_color = match ¶ms.mask_color {
2212 Some(mc) => mc,
2213 None => return,
2214 };
2215 let ncomp = params.color_space.num_components() as usize;
2216 let npixels = params.width as usize * params.height as usize;
2217 let is_range = mask_color.len() == 2 * ncomp;
2218
2219 for i in 0..npixels {
2220 let si = i * ncomp;
2221 let matched = if is_range {
2222 (0..ncomp).all(|c| {
2223 let sample = sample_data.get(si + c).copied().unwrap_or(0);
2224 let min_val = mask_color.get(c * 2).copied().unwrap_or(0);
2225 let max_val = mask_color.get(c * 2 + 1).copied().unwrap_or(0);
2226 sample >= min_val && sample <= max_val
2227 })
2228 } else {
2229 (0..ncomp).all(|c| {
2230 let sample = sample_data.get(si + c).copied().unwrap_or(0);
2231 let target = mask_color.get(c).copied().unwrap_or(0);
2232 sample == target
2233 })
2234 };
2235 if matched {
2236 let pi = i * 4;
2237 if pi + 3 < rgba.len() {
2238 rgba[pi] = 0;
2239 rgba[pi + 1] = 0;
2240 rgba[pi + 2] = 0;
2241 rgba[pi + 3] = 0;
2242 }
2243 }
2244 }
2245}
2246
2247fn image_filter_quality(transform: Transform, interpolate: bool) -> stet_tiny_skia::FilterQuality {
2253 let eff_sx = (transform.sx * transform.sx + transform.ky * transform.ky).sqrt();
2254 let eff_sy = (transform.kx * transform.kx + transform.sy * transform.sy).sqrt();
2255 let min_scale = eff_sx.min(eff_sy);
2256 if (eff_sx - 1.0).abs() < 0.01 && (eff_sy - 1.0).abs() < 0.01 {
2258 stet_tiny_skia::FilterQuality::Nearest
2259 } else if !interpolate && min_scale >= 0.95 {
2260 stet_tiny_skia::FilterQuality::Nearest
2262 } else {
2263 stet_tiny_skia::FilterQuality::Bilinear
2264 }
2265}
2266
2267fn prescale_image(
2272 rgba_data: &[u8],
2273 w: u32,
2274 h: u32,
2275 transform: Transform,
2276 interpolate: bool,
2277) -> Option<(Vec<u8>, u32, u32, Transform)> {
2278 let scale_x = (transform.sx * transform.sx + transform.ky * transform.ky).sqrt();
2280 let scale_y = (transform.kx * transform.kx + transform.sy * transform.sy).sqrt();
2281 let min_scale = scale_x.min(scale_y);
2282
2283 if min_scale > 1.05 {
2287 if interpolate {
2288 let is_axis_aligned = transform.kx.abs() < 1e-4 && transform.ky.abs() < 1e-4;
2289 if is_axis_aligned && w >= 2 && h >= 2 {
2290 let dw = (w as f32 * transform.sx.abs()).round().max(1.0) as u32;
2291 let dh = (h as f32 * transform.sy.abs()).round().max(1.0) as u32;
2292 if dw > w || dh > h {
2293 let resampled = bicubic_resample(rgba_data, w, h, dw, dh);
2294 let new_sx = transform.sx * w as f32 / dw as f32;
2295 let new_sy = transform.sy * h as f32 / dh as f32;
2296 let adjusted = Transform::from_row(
2297 new_sx,
2298 transform.ky,
2299 transform.kx,
2300 new_sy,
2301 transform.tx,
2302 transform.ty,
2303 );
2304 return Some((resampled, dw, dh, adjusted));
2305 }
2306 }
2307 }
2308 return None;
2309 }
2310
2311 if min_scale >= 0.95 {
2313 return None;
2314 }
2315
2316 let is_axis_aligned = transform.kx.abs() < 1e-4 && transform.ky.abs() < 1e-4;
2319 if is_axis_aligned && w >= 2 && h >= 2 {
2320 let dw = (w as f32 * transform.sx.abs()).ceil().max(1.0) as u32;
2321 let dh = (h as f32 * transform.sy.abs()).ceil().max(1.0) as u32;
2322 if dw < w || dh < h {
2323 let resampled = box_resample(rgba_data, w, h, dw, dh);
2324 let new_sx = transform.sx * w as f32 / dw as f32;
2326 let new_sy = transform.sy * h as f32 / dh as f32;
2327 let adjusted = Transform::from_row(
2328 new_sx,
2329 transform.ky,
2330 transform.kx,
2331 new_sy,
2332 transform.tx,
2333 transform.ty,
2334 );
2335 return Some((resampled, dw, dh, adjusted));
2336 }
2337 }
2338
2339 let factor = (1.0 / min_scale) as u32;
2341 if factor < 2 || w < factor || h < factor {
2342 return None;
2343 }
2344 let nw = w / factor;
2345 let nh = h / factor;
2346 if nw == 0 || nh == 0 {
2347 return None;
2348 }
2349 let area = factor * factor;
2350 let half = area / 2;
2351 let stride = w as usize * 4;
2352 let mut out = vec![0u8; (nw * nh * 4) as usize];
2353 for dy in 0..nh {
2354 for dx in 0..nw {
2355 let (mut r, mut g, mut b, mut a) = (0u32, 0u32, 0u32, 0u32);
2356 let sy0 = (dy * factor) as usize;
2357 let sx0 = (dx * factor) as usize;
2358 for iy in 0..factor as usize {
2359 let row = (sy0 + iy) * stride + sx0 * 4;
2360 for ix in 0..factor as usize {
2361 let i = row + ix * 4;
2362 r += rgba_data[i] as u32;
2363 g += rgba_data[i + 1] as u32;
2364 b += rgba_data[i + 2] as u32;
2365 a += rgba_data[i + 3] as u32;
2366 }
2367 }
2368 let di = (dy * nw + dx) as usize * 4;
2369 out[di] = ((r + half) / area) as u8;
2370 out[di + 1] = ((g + half) / area) as u8;
2371 out[di + 2] = ((b + half) / area) as u8;
2372 out[di + 3] = ((a + half) / area) as u8;
2373 }
2374 }
2375 let f = factor as f32;
2376 let adjusted = Transform::from_row(
2377 transform.sx * f,
2378 transform.ky * f,
2379 transform.kx * f,
2380 transform.sy * f,
2381 transform.tx,
2382 transform.ty,
2383 );
2384 Some((out, nw, nh, adjusted))
2385}
2386
2387fn translate_clip_rect(rect: &ClipRect, y_start: u32, band_h: u32) -> ClipRect {
2389 ClipRect {
2390 x0: rect.x0,
2391 y0: rect.y0.saturating_sub(y_start).min(band_h),
2392 x1: rect.x1,
2393 y1: rect.y1.saturating_sub(y_start).min(band_h),
2394 }
2395}
2396
2397fn enforce_min_image_size(transform: Transform, img_w: u32, img_h: u32) -> Transform {
2404 let eff_w =
2406 ((transform.sx * img_w as f32).powi(2) + (transform.ky * img_w as f32).powi(2)).sqrt();
2407 let eff_h =
2408 ((transform.kx * img_h as f32).powi(2) + (transform.sy * img_h as f32).powi(2)).sqrt();
2409
2410 if eff_w >= 1.0 && eff_h >= 1.0 {
2411 return transform;
2412 }
2413
2414 let ratio = eff_w.max(eff_h) / eff_w.min(eff_h).max(0.001);
2418 if ratio < 3.0 {
2419 return transform;
2420 }
2421
2422 let mut t = transform;
2423 if eff_w < 1.0 && eff_w > 0.001 {
2424 let boost = 1.0 / eff_w;
2425 t.sx *= boost;
2426 t.ky *= boost;
2427 }
2428 if eff_h < 1.0 && eff_h > 0.001 {
2429 let boost = 1.0 / eff_h;
2430 t.kx *= boost;
2431 t.sy *= boost;
2432 }
2433 t
2434}
2435
2436fn hairline_min_width(ctm: &Matrix, dpi: f64) -> f64 {
2440 let (a, b, c, d) = (ctm.a, ctm.b, ctm.c, ctm.d);
2441 let sum_sq = a * a + b * b + c * c + d * d;
2442 let diff = ((a * a + b * b - c * c - d * d).powi(2) + 4.0 * (a * c + b * d).powi(2)).sqrt();
2443 let s_max = (0.5 * (sum_sq + diff)).max(0.0).sqrt();
2444 let min_px = if dpi <= 150.0 { 0.5 } else { 1.0 };
2445 if s_max > 1e-10 {
2446 min_px / s_max
2447 } else {
2448 min_px
2449 }
2450}
2451
2452fn is_k_only_src(color: &DeviceColor) -> bool {
2458 if let Some((c, m, y, _k)) = color.native_cmyk {
2459 c == 0.0 && m == 0.0 && y == 0.0
2460 } else {
2461 false
2462 }
2463}
2464
2465fn needs_gray_promotion(
2474 overprint: bool,
2475 painted_channels: u8,
2476 is_device_cmyk: bool,
2477 color: &DeviceColor,
2478) -> Option<f64> {
2479 if !overprint
2480 || painted_channels != 0
2481 || is_device_cmyk
2482 || color.native_cmyk.is_some()
2483 || color.process_cmyk.is_some()
2484 {
2485 return None;
2486 }
2487 let r = color.r;
2488 if (r - color.g).abs() > f64::EPSILON || (r - color.b).abs() > f64::EPSILON {
2489 return None;
2490 }
2491 Some(r.clamp(0.0, 1.0))
2492}
2493
2494fn maybe_promote_gray_fill<'a>(
2497 params: &'a FillParams,
2498 buf: &'a mut Option<FillParams>,
2499) -> &'a FillParams {
2500 if let Some(gray) = needs_gray_promotion(
2501 params.overprint,
2502 params.painted_channels,
2503 params.is_device_cmyk,
2504 ¶ms.color,
2505 ) {
2506 let mut promoted = params.clone();
2507 promoted.is_device_cmyk = true;
2508 promoted.painted_channels = stet_graphics::device::CMYK_K;
2509 promoted.color.native_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2510 promoted.color.process_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2511 *buf = Some(promoted);
2512 return buf.as_ref().unwrap();
2513 }
2514 params
2515}
2516
2517fn maybe_promote_gray_stroke<'a>(
2519 params: &'a StrokeParams,
2520 buf: &'a mut Option<StrokeParams>,
2521) -> &'a StrokeParams {
2522 if let Some(gray) = needs_gray_promotion(
2523 params.overprint,
2524 params.painted_channels,
2525 params.is_device_cmyk,
2526 ¶ms.color,
2527 ) {
2528 let mut promoted = params.clone();
2529 promoted.is_device_cmyk = true;
2530 promoted.painted_channels = stet_graphics::device::CMYK_K;
2531 promoted.color.native_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2532 promoted.color.process_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2533 *buf = Some(promoted);
2534 return buf.as_ref().unwrap();
2535 }
2536 params
2537}
2538
2539fn build_stroke(params: &StrokeParams, dpi: f64) -> Stroke {
2543 let min_lw = hairline_min_width(¶ms.ctm, dpi);
2544 let mut stroke = Stroke {
2545 width: (params.line_width as f32).max(min_lw as f32),
2546 line_cap: to_line_cap(params.line_cap),
2547 line_join: to_line_join(params.line_join),
2548 miter_limit: params.miter_limit as f32,
2549 ..Stroke::default()
2550 };
2551 if !params.dash_pattern.array.is_empty() {
2552 let mut dash_array: Vec<f32> = params
2553 .dash_pattern
2554 .array
2555 .iter()
2556 .map(|&v| v as f32)
2557 .collect();
2558 if dash_array.len() % 2 == 1 {
2561 let clone = dash_array.clone();
2562 dash_array.extend_from_slice(&clone);
2563 }
2564 if let Some(dash) = StrokeDash::new(dash_array, params.dash_pattern.offset as f32) {
2565 stroke.dash = Some(dash);
2566 }
2567 }
2568 stroke
2569}
2570
2571fn ctm_is_device_space(ctm: &Matrix) -> bool {
2588 (ctm.a.abs() - 1.0).abs() < 0.01
2589 && ctm.b.abs() < 0.01
2590 && ctm.c.abs() < 0.01
2591 && (ctm.d.abs() - 1.0).abs() < 0.01
2592}
2593
2594fn stroke_adjust_path_viewport(
2600 path: &PsPath,
2601 device_width: f64,
2602 scale_x: f64,
2603 scale_y: f64,
2604 vp_x: f64,
2605 vp_y: f64,
2606) -> PsPath {
2607 let use_half_pixel = device_width < 1.5 || (device_width.round() as i32) % 2 == 1;
2608
2609 let snap_x = |v: f64| -> f64 {
2611 let out = (v - vp_x) * scale_x;
2612 let snapped = if use_half_pixel {
2613 out.floor() + 0.5
2614 } else {
2615 out.round()
2616 };
2617 snapped / scale_x + vp_x
2618 };
2619 let snap_y = |v: f64| -> f64 {
2620 let out = (v - vp_y) * scale_y;
2621 let snapped = if use_half_pixel {
2622 out.floor() + 0.5
2623 } else {
2624 out.round()
2625 };
2626 snapped / scale_y + vp_y
2627 };
2628
2629 let mut result = PsPath::new();
2630 let mut prev_x = 0.0_f64;
2631 let mut prev_y = 0.0_f64;
2632
2633 for seg in &path.segments {
2634 match *seg {
2635 PathSegment::MoveTo(x, y) => {
2636 prev_x = x;
2637 prev_y = y;
2638 result.segments.push(PathSegment::MoveTo(x, y));
2639 }
2640 PathSegment::LineTo(x, y) => {
2641 let is_horizontal = (y - prev_y).abs() < 1e-6;
2642 let is_vertical = (x - prev_x).abs() < 1e-6;
2643
2644 if is_horizontal {
2645 let snapped_y = snap_y(y);
2646 if let Some(PathSegment::MoveTo(_, ly) | PathSegment::LineTo(_, ly)) =
2647 result.segments.last_mut()
2648 {
2649 *ly = snapped_y;
2650 }
2651 result.segments.push(PathSegment::LineTo(x, snapped_y));
2652 prev_x = x;
2653 prev_y = snapped_y;
2654 } else if is_vertical {
2655 let snapped_x = snap_x(x);
2656 if let Some(PathSegment::MoveTo(lx, _) | PathSegment::LineTo(lx, _)) =
2657 result.segments.last_mut()
2658 {
2659 *lx = snapped_x;
2660 }
2661 result.segments.push(PathSegment::LineTo(snapped_x, y));
2662 prev_x = snapped_x;
2663 prev_y = y;
2664 } else {
2665 result.segments.push(PathSegment::LineTo(x, y));
2666 prev_x = x;
2667 prev_y = y;
2668 }
2669 }
2670 PathSegment::CurveTo {
2671 x1,
2672 y1,
2673 x2,
2674 y2,
2675 x3,
2676 y3,
2677 } => {
2678 result.segments.push(PathSegment::CurveTo {
2679 x1,
2680 y1,
2681 x2,
2682 y2,
2683 x3,
2684 y3,
2685 });
2686 prev_x = x3;
2687 prev_y = y3;
2688 }
2689 PathSegment::ClosePath => {
2690 result.segments.push(PathSegment::ClosePath);
2691 }
2692 }
2693 }
2694 result
2695}
2696
2697fn render_element(
2703 pixmap: &mut Pixmap,
2704 band_state: &mut BandState,
2705 element: &DisplayElement,
2706 ctx: &RenderContext<'_>,
2707) {
2708 match element {
2709 DisplayElement::Fill { path, params } => {
2710 let mut promoted_fill: Option<FillParams> = None;
2716 let params = maybe_promote_gray_fill(params, &mut promoted_fill);
2717 let painted = params.painted_channels;
2738 let subset_channels = painted != 0 && painted != stet_graphics::device::CMYK_ALL;
2739 let opm1_cmyk = params.is_device_cmyk && params.overprint_mode == 1;
2740 let custom_spot = painted == 0
2749 && !params.is_device_cmyk
2750 && params.color.native_cmyk.is_some()
2751 && params.color.process_cmyk.is_some();
2752 let is_k_only_cmyk =
2758 params.is_device_cmyk && params.overprint_mode == 0 && is_k_only_src(¶ms.color);
2759 let needs_overprint = params.overprint
2760 && band_state.cmyk_buffer.is_some()
2761 && params.blend_mode == 0
2762 && (subset_channels || opm1_cmyk || custom_spot || is_k_only_cmyk);
2763
2764 if needs_overprint {
2765 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2766 let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
2767 let spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2768 render_overprint_fill(
2769 pixmap,
2770 &mut cmyk_buf,
2771 &mut op_bg,
2772 &mut op_touched,
2773 &spot_mask,
2774 band_state,
2775 path,
2776 params,
2777 ctx.vp_x,
2778 ctx.vp_y,
2779 ctx.scale_x,
2780 ctx.scale_y,
2781 ctx.out_w,
2782 ctx.out_h,
2783 ctx.icc,
2784 ctx.no_aa,
2785 );
2786 band_state.cmyk_buffer = Some(cmyk_buf);
2787 band_state.restore_op_buffers(op_bg, op_touched);
2788 band_state.restore_spot_mask(spot_mask);
2789 } else {
2790 let Some(skia_path) = build_skia_path(path) else {
2791 return;
2792 };
2793 let mut temp_mask = None;
2794 let Some(mask_ref) = resolve_clip_mask(
2795 &band_state.clip_region,
2796 &mut temp_mask,
2797 ctx.out_w,
2798 ctx.out_h,
2799 ) else {
2800 return;
2801 };
2802 let paint =
2803 to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, ctx.no_aa);
2804 let transform = ctx.transform(¶ms.ctm);
2805
2806 if is_degenerate_fill(path) {
2812 let stroke = Stroke {
2813 width: 1.0,
2814 ..Stroke::default()
2815 };
2816 pixmap.stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
2817 } else {
2818 let fill_rule = to_fill_rule(¶ms.fill_rule);
2819 pixmap.fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
2820 }
2821
2822 if band_state.cmyk_buffer.is_some() {
2824 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2825 let mut spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2826 update_cmyk_buffer_for_fill(
2827 &mut cmyk_buf,
2828 &mut spot_mask,
2829 path,
2830 params,
2831 ctx.vp_x,
2832 ctx.vp_y,
2833 ctx.scale_x,
2834 ctx.scale_y,
2835 ctx.out_w,
2836 ctx.out_h,
2837 &band_state.clip_region,
2838 ctx.no_aa,
2839 ctx.icc,
2840 );
2841 band_state.cmyk_buffer = Some(cmyk_buf);
2842 band_state.restore_spot_mask(spot_mask);
2843 }
2844 }
2845 }
2846 DisplayElement::Stroke { path, params } => {
2847 let mut promoted_stroke: Option<StrokeParams> = None;
2848 let params = maybe_promote_gray_stroke(params, &mut promoted_stroke);
2849 let transform = ctx.transform(¶ms.ctm);
2850 let vp_ctm = Matrix {
2853 a: transform.sx as f64,
2854 b: transform.ky as f64,
2855 c: transform.kx as f64,
2856 d: transform.sy as f64,
2857 tx: 0.0,
2858 ty: 0.0,
2859 };
2860 let vp_params = StrokeParams {
2861 ctm: vp_ctm,
2862 ..params.clone()
2863 };
2864 let stroke = build_stroke(&vp_params, ctx.effective_dpi);
2865
2866 let adjusted;
2868 let draw_path = if params.stroke_adjust
2869 && stroke.width <= 2.0
2870 && ctm_is_device_space(¶ms.ctm)
2871 {
2872 adjusted = stroke_adjust_path_viewport(
2873 path,
2874 stroke.width as f64,
2875 ctx.scale_x as f64,
2876 ctx.scale_y as f64,
2877 ctx.vp_x as f64,
2878 ctx.vp_y as f64,
2879 );
2880 &adjusted
2881 } else {
2882 path
2883 };
2884
2885 let painted = params.painted_channels;
2893 let subset_channels = painted != 0 && painted != stet_graphics::device::CMYK_ALL;
2894 let opm1_cmyk = params.is_device_cmyk && params.overprint_mode == 1;
2895 let custom_spot = painted == 0
2900 && !params.is_device_cmyk
2901 && params.color.native_cmyk.is_some()
2902 && params.color.process_cmyk.is_some();
2903 let is_k_only_cmyk =
2904 params.is_device_cmyk && params.overprint_mode == 0 && is_k_only_src(¶ms.color);
2905 let needs_overprint = params.overprint
2906 && band_state.cmyk_buffer.is_some()
2907 && params.blend_mode == 0
2908 && (subset_channels || opm1_cmyk || custom_spot || is_k_only_cmyk);
2909
2910 let Some(skia_path) = build_skia_path(draw_path) else {
2911 return;
2912 };
2913 let mut temp_mask = None;
2914 let Some(mask_ref) = resolve_clip_mask(
2915 &band_state.clip_region,
2916 &mut temp_mask,
2917 ctx.out_w,
2918 ctx.out_h,
2919 ) else {
2920 return;
2921 };
2922
2923 if needs_overprint {
2924 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2929 let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
2930 let spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2931 render_overprint_stroke(
2932 pixmap,
2933 &mut cmyk_buf,
2934 &mut op_bg,
2935 &mut op_touched,
2936 &spot_mask,
2937 band_state,
2938 &skia_path,
2939 &stroke,
2940 transform,
2941 params,
2942 ctx.out_w,
2943 ctx.out_h,
2944 ctx.icc,
2945 ctx.no_aa,
2946 );
2947 band_state.cmyk_buffer = Some(cmyk_buf);
2948 band_state.restore_op_buffers(op_bg, op_touched);
2949 band_state.restore_spot_mask(spot_mask);
2950 } else {
2951 let paint =
2952 to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, ctx.no_aa);
2953 pixmap.stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
2954
2955 if band_state.cmyk_buffer.is_some() {
2956 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2957 let mut spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2958 update_cmyk_buffer_for_stroke(
2959 &mut cmyk_buf,
2960 &mut spot_mask,
2961 draw_path,
2962 params,
2963 &stroke,
2964 transform,
2965 ctx.out_w,
2966 ctx.out_h,
2967 &band_state.clip_region,
2968 ctx.no_aa,
2969 ctx.icc,
2970 );
2971 band_state.cmyk_buffer = Some(cmyk_buf);
2972 band_state.restore_spot_mask(spot_mask);
2973 }
2974 }
2975 }
2976 DisplayElement::Clip { path, params } => {
2977 clip_path_unified(band_state, path, params, ctx);
2978 }
2979 DisplayElement::InitClip => {
2980 if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
2981 band_state.recycle_mask(mask);
2982 }
2983 band_state.clip_region = None;
2984 }
2985 DisplayElement::ErasePage => {
2986 pixmap.fill(Color::TRANSPARENT);
2987 if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
2988 band_state.recycle_mask(mask);
2989 }
2990 band_state.clip_region = None;
2991 }
2992 DisplayElement::Image {
2993 sample_data,
2994 params,
2995 } => {
2996 let iw = params.width;
2997 let ih = params.height;
2998 if iw == 0 || ih == 0 {
2999 return;
3000 }
3001
3002 let needs_overprint = params.overprint
3003 && band_state.cmyk_buffer.is_some()
3004 && image_supports_overprint(¶ms.color_space);
3005
3006 if needs_overprint {
3007 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
3008 let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
3009 render_overprint_image(
3010 pixmap,
3011 &mut cmyk_buf,
3012 &mut op_bg,
3013 &mut op_touched,
3014 band_state,
3015 sample_data,
3016 params,
3017 ctx.vp_x,
3018 ctx.vp_y,
3019 ctx.scale_x,
3020 ctx.scale_y,
3021 ctx.out_w,
3022 ctx.out_h,
3023 ctx.icc,
3024 );
3025 band_state.cmyk_buffer = Some(cmyk_buf);
3026 band_state.restore_op_buffers(op_bg, op_touched);
3027 } else if let Some(pp) = ctx
3028 .preprocessed
3029 .and_then(|pp| pp.get(ctx.elem_idx))
3030 .and_then(|e| e.as_ref())
3031 {
3032 let Some(image_inv) = params.image_matrix.invert() else {
3035 return;
3036 };
3037 let combined = params.ctm.concat(&image_inv);
3038 let raw_transform = ctx.transform(&combined);
3039 let transform = Transform::from_row(
3040 pp.adj_sx,
3041 pp.adj_ky,
3042 pp.adj_kx,
3043 pp.adj_sy,
3044 raw_transform.tx,
3045 raw_transform.ty,
3046 );
3047
3048 let Some(img_pixmap) =
3049 stet_tiny_skia::PixmapRef::from_bytes(&pp.data, pp.width, pp.height)
3050 else {
3051 return;
3052 };
3053 #[allow(unused_assignments)]
3054 let mut temp_mask = None;
3055 let mask_ref = match &band_state.clip_region {
3056 None => None,
3057 Some(ClipRegion::Mask(m)) => Some(m as &Mask),
3058 Some(ClipRegion::Rect(rect)) => {
3059 if rect.is_empty() {
3060 return;
3061 } else if rect.is_full_page(ctx.out_w, ctx.out_h) {
3062 None
3063 } else {
3064 temp_mask = rect.make_mask(ctx.out_w, ctx.out_h);
3065 temp_mask.as_ref()
3066 }
3067 }
3068 };
3069 let img_paint = stet_tiny_skia::PixmapPaint {
3070 quality: pp.quality,
3071 opacity: params.alpha as f32,
3072 blend_mode: u8_to_blend_mode(params.blend_mode),
3073 };
3074 pixmap.draw_pixmap(0, 0, img_pixmap, &img_paint, transform, mask_ref);
3075
3076 if let Some(ref mut cmyk_buf) = band_state.cmyk_buffer {
3082 update_cmyk_buffer_for_image(
3083 cmyk_buf,
3084 sample_data,
3085 pixmap.data(),
3086 params,
3087 ctx.vp_x,
3088 ctx.vp_y,
3089 ctx.scale_x,
3090 ctx.scale_y,
3091 ctx.out_w,
3092 ctx.out_h,
3093 &band_state.clip_region,
3094 ctx.icc,
3095 );
3096 }
3097 } else {
3098 let owned_rgba;
3100 let rgba_data: &[u8] = if let Some(cached) =
3101 ctx.image_cache.and_then(|c| c.get(ctx.elem_idx))
3102 {
3103 cached
3104 } else {
3105 owned_rgba = {
3106 let mut rgba =
3107 samples_to_rgba(sample_data, params, ctx.icc, ctx.opm_zero_transparent);
3108 if params.mask_color.is_some() {
3109 apply_mask_color_rgba(&mut rgba, sample_data, params);
3110 }
3111 rgba
3112 };
3113 &owned_rgba
3114 };
3115 let expected = (iw * ih * 4) as usize;
3116 if rgba_data.len() < expected {
3117 return;
3118 }
3119 let Some(image_inv) = params.image_matrix.invert() else {
3120 return;
3121 };
3122 let combined = params.ctm.concat(&image_inv);
3123 let raw_transform = enforce_min_image_size(ctx.transform(&combined), iw, ih);
3124
3125 let prescaled =
3130 prescale_image(rgba_data, iw, ih, raw_transform, params.interpolate);
3131 let (img_data, img_w, img_h, transform) = match &prescaled {
3132 Some((data, w, h, t)) => (data.as_slice(), *w, *h, *t),
3133 None => (rgba_data, iw, ih, raw_transform),
3134 };
3135
3136 let Some(img_pixmap) =
3137 stet_tiny_skia::PixmapRef::from_bytes(img_data, img_w, img_h)
3138 else {
3139 return;
3140 };
3141 #[allow(unused_assignments)]
3142 let mut temp_mask = None;
3143 let mask_ref = match &band_state.clip_region {
3144 None => None,
3145 Some(ClipRegion::Mask(m)) => Some(m as &Mask),
3146 Some(ClipRegion::Rect(rect)) => {
3147 if rect.is_empty() {
3148 return;
3149 } else if rect.is_full_page(ctx.out_w, ctx.out_h) {
3150 None
3151 } else {
3152 temp_mask = rect.make_mask(ctx.out_w, ctx.out_h);
3153 temp_mask.as_ref()
3154 }
3155 }
3156 };
3157 let img_paint = stet_tiny_skia::PixmapPaint {
3158 quality: image_filter_quality(transform, params.interpolate),
3159 opacity: params.alpha as f32,
3160 blend_mode: u8_to_blend_mode(params.blend_mode),
3161 };
3162 pixmap.draw_pixmap(0, 0, img_pixmap, &img_paint, transform, mask_ref);
3163
3164 if let Some(ref mut cmyk_buf) = band_state.cmyk_buffer {
3168 update_cmyk_buffer_for_image(
3169 cmyk_buf,
3170 sample_data,
3171 pixmap.data(),
3172 params,
3173 ctx.vp_x,
3174 ctx.vp_y,
3175 ctx.scale_x,
3176 ctx.scale_y,
3177 ctx.out_w,
3178 ctx.out_h,
3179 &band_state.clip_region,
3180 ctx.icc,
3181 );
3182 }
3183 }
3184 }
3185 DisplayElement::AxialShading { params } => {
3186 let mut temp_mask = None;
3187 let Some(mask_ref) = resolve_clip_mask(
3188 &band_state.clip_region,
3189 &mut temp_mask,
3190 ctx.out_w,
3191 ctx.out_h,
3192 ) else {
3193 return;
3194 };
3195 render_axial_shading(
3196 pixmap,
3197 params,
3198 ctx.vp_x,
3199 ctx.vp_y,
3200 ctx.scale_x,
3201 ctx.scale_y,
3202 mask_ref,
3203 ctx.no_aa,
3204 band_state.cmyk_buffer.as_deref_mut(),
3205 ctx.icc,
3206 );
3207 }
3208 DisplayElement::RadialShading { params } => {
3209 let mut temp_mask = None;
3210 let Some(mask_ref) = resolve_clip_mask(
3211 &band_state.clip_region,
3212 &mut temp_mask,
3213 ctx.out_w,
3214 ctx.out_h,
3215 ) else {
3216 return;
3217 };
3218 render_radial_shading(
3219 pixmap,
3220 params,
3221 ctx.vp_x,
3222 ctx.vp_y,
3223 ctx.scale_x,
3224 ctx.scale_y,
3225 mask_ref,
3226 ctx.no_aa,
3227 band_state.cmyk_buffer.as_deref_mut(),
3228 ctx.icc,
3229 );
3230 }
3231 DisplayElement::MeshShading { params } => {
3232 let mut temp_mask = None;
3233 let Some(mask_ref) = resolve_clip_mask(
3234 &band_state.clip_region,
3235 &mut temp_mask,
3236 ctx.out_w,
3237 ctx.out_h,
3238 ) else {
3239 return;
3240 };
3241 render_mesh_shading(
3242 pixmap,
3243 params,
3244 ctx.vp_x,
3245 ctx.vp_y,
3246 ctx.scale_x,
3247 ctx.scale_y,
3248 mask_ref,
3249 band_state.cmyk_buffer.as_deref_mut(),
3250 ctx.icc,
3251 );
3252 }
3253 DisplayElement::PatchShading { params } => {
3254 let mut temp_mask = None;
3255 let Some(mask_ref) = resolve_clip_mask(
3256 &band_state.clip_region,
3257 &mut temp_mask,
3258 ctx.out_w,
3259 ctx.out_h,
3260 ) else {
3261 return;
3262 };
3263 render_patch_shading(
3264 pixmap,
3265 params,
3266 ctx.vp_x,
3267 ctx.vp_y,
3268 ctx.scale_x,
3269 ctx.scale_y,
3270 mask_ref,
3271 band_state.cmyk_buffer.as_deref_mut(),
3272 ctx.icc,
3273 );
3274 }
3275 DisplayElement::PatternFill { params } => {
3276 render_pattern_fill(pixmap, band_state, params, ctx);
3277 }
3278 DisplayElement::Group { elements, params } => {
3279 render_group(pixmap, band_state, elements, params, ctx);
3280 }
3281 DisplayElement::SoftMasked {
3282 mask,
3283 content,
3284 params,
3285 mask_cache,
3286 } => {
3287 render_soft_masked(pixmap, band_state, mask, content, params, mask_cache, ctx);
3288 }
3289 DisplayElement::Text { .. } => {} DisplayElement::OcgGroup {
3291 elements,
3292 visibility,
3293 } => {
3294 let visible = ctx.layer_set.evaluate(visibility);
3300 for (idx, elem) in elements.elements().iter().enumerate() {
3301 if !visible
3302 && !matches!(elem, DisplayElement::Clip { .. } | DisplayElement::InitClip)
3303 {
3304 continue;
3305 }
3306 let elem_ctx = RenderContext {
3307 elem_idx: idx,
3308 ..*ctx
3309 };
3310 render_element(pixmap, band_state, elem, &elem_ctx);
3311 }
3312 }
3313 _ => {}
3314 }
3315}
3316
3317fn compute_group_crop(bbox: &[f64; 4], ctx: &RenderContext<'_>) -> Option<(i32, i32, u32, u32)> {
3322 let px_min = ((bbox[0] as f32 - ctx.vp_x) * ctx.scale_x).floor() as i32;
3324 let py_min = ((bbox[1] as f32 - ctx.vp_y) * ctx.scale_y).floor() as i32;
3325 let px_max = ((bbox[2] as f32 - ctx.vp_x) * ctx.scale_x).ceil() as i32;
3326 let py_max = ((bbox[3] as f32 - ctx.vp_y) * ctx.scale_y).ceil() as i32;
3327
3328 let x0 = px_min.max(0);
3330 let y0 = py_min.max(0);
3331 let x1 = px_max.min(ctx.out_w as i32);
3332 let y1 = py_max.min(ctx.out_h as i32);
3333
3334 if x0 >= x1 || y0 >= y1 {
3335 return None;
3336 }
3337
3338 let crop_w = (x1 - x0) as u32;
3339 let crop_h = (y1 - y0) as u32;
3340
3341 let crop_pixels = crop_w as u64 * crop_h as u64;
3343 let full_pixels = ctx.out_w as u64 * ctx.out_h as u64;
3344 if crop_pixels * 4 >= full_pixels * 3 {
3345 return None;
3346 }
3347
3348 Some((x0, y0, crop_w, crop_h))
3349}
3350
3351fn blend_cmyk_separable_channel(cb: f64, cs: f64, mode: u8) -> f64 {
3355 let cbi = 1.0 - cb;
3356 let csi = 1.0 - cs;
3357 let result_inv = match mode {
3358 1 => cbi * csi, 2 => cbi + csi - cbi * csi, 3 => {
3361 if cbi <= 0.5 {
3363 2.0 * cbi * csi
3364 } else {
3365 1.0 - 2.0 * (1.0 - cbi) * (1.0 - csi)
3366 }
3367 }
3368 4 => cbi.min(csi), 5 => cbi.max(csi), 6 => {
3371 if csi >= 1.0 {
3373 1.0
3374 } else {
3375 (cbi / (1.0 - csi)).min(1.0)
3376 }
3377 }
3378 7 => {
3379 if csi <= 0.0 {
3381 0.0
3382 } else {
3383 1.0 - ((1.0 - cbi) / csi).min(1.0)
3384 }
3385 }
3386 8 => {
3387 if csi <= 0.5 {
3389 2.0 * cbi * csi
3390 } else {
3391 1.0 - 2.0 * (1.0 - cbi) * (1.0 - csi)
3392 }
3393 }
3394 9 => {
3395 let d = if cbi <= 0.25 {
3397 ((16.0 * cbi - 12.0) * cbi + 4.0) * cbi
3398 } else {
3399 cbi.sqrt()
3400 };
3401 if csi <= 0.5 {
3402 cbi - (1.0 - 2.0 * csi) * cbi * (1.0 - cbi)
3403 } else {
3404 cbi + (2.0 * csi - 1.0) * (d - cbi)
3405 }
3406 }
3407 10 => (cbi - csi).abs(), 11 => cbi + csi - 2.0 * cbi * csi, _ => csi, };
3411 1.0 - result_inv.clamp(0.0, 1.0)
3412}
3413
3414fn blend_cmyk_nonseparable(cb: [f64; 4], cs: [f64; 4], mode: u8) -> [f64; 4] {
3420 fn lum(c: [f64; 3]) -> f64 {
3421 0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
3422 }
3423 fn clip_color(mut c: [f64; 3]) -> [f64; 3] {
3424 let l = lum(c);
3425 let n = c[0].min(c[1]).min(c[2]);
3426 let x = c[0].max(c[1]).max(c[2]);
3427 if n < 0.0 {
3428 for ci in c.iter_mut() {
3429 *ci = l + (*ci - l) * l / (l - n);
3430 }
3431 }
3432 if x > 1.0 {
3433 for ci in c.iter_mut() {
3434 *ci = l + (*ci - l) * (1.0 - l) / (x - l);
3435 }
3436 }
3437 c
3438 }
3439 fn set_lum(c: [f64; 3], l: f64) -> [f64; 3] {
3440 let d = l - lum(c);
3441 clip_color([c[0] + d, c[1] + d, c[2] + d])
3442 }
3443 fn sat(c: [f64; 3]) -> f64 {
3444 c[0].max(c[1]).max(c[2]) - c[0].min(c[1]).min(c[2])
3445 }
3446 fn set_sat(c: [f64; 3], s: f64) -> [f64; 3] {
3447 let mut idx = [0usize, 1, 2];
3449 idx.sort_by(|a, b| {
3450 c[*a]
3451 .partial_cmp(&c[*b])
3452 .unwrap_or(std::cmp::Ordering::Equal)
3453 });
3454 let (i_min, i_mid, i_max) = (idx[0], idx[1], idx[2]);
3455 let mut out = c;
3456 if c[i_max] > c[i_min] {
3457 out[i_mid] = (c[i_mid] - c[i_min]) * s / (c[i_max] - c[i_min]);
3458 out[i_max] = s;
3459 } else {
3460 out[i_mid] = 0.0;
3461 out[i_max] = 0.0;
3462 }
3463 out[i_min] = 0.0;
3464 out
3465 }
3466
3467 let cb_rgb = [1.0 - cb[0], 1.0 - cb[1], 1.0 - cb[2]];
3468 let cs_rgb = [1.0 - cs[0], 1.0 - cs[1], 1.0 - cs[2]];
3469 let result_rgb = match mode {
3470 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,
3475 };
3476 let result_k = if mode == 15 { cs[3] } else { cb[3] };
3480 [
3481 (1.0 - result_rgb[0]).clamp(0.0, 1.0),
3482 (1.0 - result_rgb[1]).clamp(0.0, 1.0),
3483 (1.0 - result_rgb[2]).clamp(0.0, 1.0),
3484 result_k,
3485 ]
3486}
3487
3488fn ps_path_bbox(path: &PsPath) -> Option<(f64, f64, f64, f64)> {
3493 let mut it = path.segments.iter().filter_map(|seg| match *seg {
3494 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => Some(vec![(x, y)]),
3495 PathSegment::CurveTo {
3496 x1,
3497 y1,
3498 x2,
3499 y2,
3500 x3,
3501 y3,
3502 } => Some(vec![(x1, y1), (x2, y2), (x3, y3)]),
3503 PathSegment::ClosePath => None,
3504 });
3505 let first = it.next()?.into_iter().next()?;
3506 let (mut x0, mut y0) = first;
3507 let (mut x1, mut y1) = first;
3508 for seg_points in std::iter::once(vec![first]).chain(it) {
3509 for (x, y) in seg_points {
3510 x0 = x0.min(x);
3511 y0 = y0.min(y);
3512 x1 = x1.max(x);
3513 y1 = y1.max(y);
3514 }
3515 }
3516 Some((x0, y0, x1, y1))
3517}
3518
3519fn bbox_contains(outer: (f64, f64, f64, f64), inner: (f64, f64, f64, f64), tolerance: f64) -> bool {
3522 inner.0 >= outer.0 - tolerance
3523 && inner.1 >= outer.1 - tolerance
3524 && inner.2 <= outer.2 + tolerance
3525 && inner.3 <= outer.3 + tolerance
3526}
3527
3528fn compute_obscured_fill_skips(elements: &DisplayList) -> Vec<usize> {
3554 let mut skips = Vec::new();
3555 let els = elements.elements();
3556 for i in 0..els.len() {
3557 let DisplayElement::Fill {
3558 path: parent_path,
3559 params: parent_params,
3560 } = &els[i]
3561 else {
3562 continue;
3563 };
3564 if (parent_params.alpha - 1.0).abs() > 1e-6 || parent_params.blend_mode != 0 {
3565 continue;
3566 }
3567 let Some(parent_bbox) = ps_path_bbox(parent_path) else {
3568 continue;
3569 };
3570 let mut j = i + 1;
3574 let mut clips_ok = true;
3575 while j < els.len() {
3576 match &els[j] {
3577 DisplayElement::InitClip => {}
3578 DisplayElement::Clip {
3579 path: clip_path, ..
3580 } => match ps_path_bbox(clip_path) {
3581 Some(cb) if bbox_contains(cb, parent_bbox, 0.5) => {}
3582 _ => {
3583 clips_ok = false;
3584 break;
3585 }
3586 },
3587 _ => break,
3588 }
3589 j += 1;
3590 }
3591 if !clips_ok {
3592 continue;
3593 }
3594 let Some(DisplayElement::Group {
3595 elements: group_elements,
3596 params: group_params,
3597 }) = els.get(j)
3598 else {
3599 continue;
3600 };
3601 if !group_params.isolated
3602 || (group_params.alpha - 1.0).abs() > 1e-6
3603 || group_params.blend_mode != 0
3604 {
3605 continue;
3606 }
3607 let group_bbox = (
3611 group_params.bbox[0],
3612 group_params.bbox[1],
3613 group_params.bbox[2],
3614 group_params.bbox[3],
3615 );
3616 if !bbox_contains(group_bbox, parent_bbox, 0.5) {
3617 continue;
3618 }
3619 let inner_els = group_elements.elements();
3622 let mut k = 0;
3623 let mut inner_clips_ok = true;
3624 while k < inner_els.len() {
3625 match &inner_els[k] {
3626 DisplayElement::InitClip => {}
3627 DisplayElement::Clip {
3628 path: clip_path, ..
3629 } => match ps_path_bbox(clip_path) {
3630 Some(cb) if bbox_contains(cb, parent_bbox, 0.5) => {}
3631 _ => {
3632 inner_clips_ok = false;
3633 break;
3634 }
3635 },
3636 _ => break,
3637 }
3638 k += 1;
3639 }
3640 if !inner_clips_ok {
3641 continue;
3642 }
3643 let Some(DisplayElement::Fill {
3644 path: group_path,
3645 params: group_fill_params,
3646 }) = inner_els.get(k)
3647 else {
3648 continue;
3649 };
3650 if (group_fill_params.alpha - 1.0).abs() > 1e-6 || group_fill_params.blend_mode != 0 {
3651 continue;
3652 }
3653 if paths_approximately_equal(parent_path, group_path, 0.5) {
3654 skips.push(i);
3655 }
3656 }
3657 skips
3658}
3659
3660fn paths_approximately_equal(a: &PsPath, b: &PsPath, tolerance: f64) -> bool {
3666 if a.segments.len() != b.segments.len() {
3667 return false;
3668 }
3669 for (sa, sb) in a.segments.iter().zip(b.segments.iter()) {
3670 let close_pair = |(x1, y1): (f64, f64), (x2, y2): (f64, f64)| -> bool {
3671 (x1 - x2).abs() <= tolerance && (y1 - y2).abs() <= tolerance
3672 };
3673 match (sa, sb) {
3674 (PathSegment::MoveTo(x1, y1), PathSegment::MoveTo(x2, y2)) => {
3675 if !close_pair((*x1, *y1), (*x2, *y2)) {
3676 return false;
3677 }
3678 }
3679 (PathSegment::LineTo(x1, y1), PathSegment::LineTo(x2, y2)) => {
3680 if !close_pair((*x1, *y1), (*x2, *y2)) {
3681 return false;
3682 }
3683 }
3684 (
3685 PathSegment::CurveTo {
3686 x1: ax1,
3687 y1: ay1,
3688 x2: ax2,
3689 y2: ay2,
3690 x3: ax3,
3691 y3: ay3,
3692 },
3693 PathSegment::CurveTo {
3694 x1: bx1,
3695 y1: by1,
3696 x2: bx2,
3697 y2: by2,
3698 x3: bx3,
3699 y3: by3,
3700 },
3701 ) => {
3702 if !close_pair((*ax1, *ay1), (*bx1, *by1))
3703 || !close_pair((*ax2, *ay2), (*bx2, *by2))
3704 || !close_pair((*ax3, *ay3), (*bx3, *by3))
3705 {
3706 return false;
3707 }
3708 }
3709 (PathSegment::ClosePath, PathSegment::ClosePath) => {}
3710 _ => return false,
3711 }
3712 }
3713 true
3714}
3715
3716fn render_group(
3720 pixmap: &mut Pixmap,
3721 band_state: &mut BandState,
3722 elements: &DisplayList,
3723 params: &stet_graphics::display_list::GroupParams,
3724 ctx: &RenderContext<'_>,
3725) {
3726 if params.knockout {
3727 render_knockout_group(pixmap, band_state, elements, params, ctx);
3728 return;
3729 }
3730
3731 let crop = compute_group_crop(¶ms.bbox, ctx);
3732
3733 let (eff_w, eff_h, crop_x, crop_y, eff_vp_x, eff_vp_y) = match crop {
3734 Some((cx, cy, cw, ch)) => (
3735 cw,
3736 ch,
3737 cx,
3738 cy,
3739 ctx.vp_x + cx as f32 / ctx.scale_x,
3740 ctx.vp_y + cy as f32 / ctx.scale_y,
3741 ),
3742 None => (ctx.out_w, ctx.out_h, 0, 0, ctx.vp_x, ctx.vp_y),
3743 };
3744
3745 let Some(mut offscreen) = Pixmap::new(eff_w, eff_h) else {
3746 return;
3747 };
3748
3749 use stet_graphics::display_list::GroupColorSpace;
3757
3758 let needs_group_cmyk = has_overprint_elements(elements)
3764 || band_state.cmyk_buffer.is_some()
3765 || params.color_space == GroupColorSpace::DeviceCMYK
3766 || has_cmyk_group(elements);
3767
3768 let force_cmyk_compose =
3775 std::env::var_os("STET_FORCE_CMYK_COMPOSITE_BACK").as_deref() == Some("1".as_ref());
3776 let plan_cmyk_compose = match ctx.knockout_painter_pass {
3787 KnockoutPainterPass::CoveragePass => false,
3788 KnockoutPainterPass::ColorPass => {
3789 !params.isolated
3790 && params.blend_mode != 0
3791 && needs_group_cmyk
3792 && band_state.cmyk_buffer.is_some()
3793 && group_content_is_native_cmyk(elements)
3794 }
3795 KnockoutPainterPass::None if force_cmyk_compose => {
3796 !params.isolated
3814 && matches!(params.blend_mode, 10..=15)
3815 && needs_group_cmyk
3816 && band_state.cmyk_buffer.is_some()
3817 && group_content_is_native_cmyk(elements)
3818 }
3819 KnockoutPainterPass::None => {
3820 let inversion_sensitive = !params.isolated
3825 && matches!(params.blend_mode, 10..=15)
3826 && group_only_native_cmyk_fills(elements);
3827 let proofing_enabled = ctx.icc.is_some_and(|c| c.proofing_enabled());
3855 let effective_cs_is_cmyk = params.color_space == GroupColorSpace::DeviceCMYK
3864 || (params.color_space == GroupColorSpace::Inherited
3865 && band_state.cmyk_buffer.is_some());
3866 let cmyk_group_blend = !params.isolated
3867 && (ctx.parent_group_isolated || proofing_enabled)
3868 && params.blend_mode != 0
3869 && effective_cs_is_cmyk
3870 && needs_group_cmyk
3871 && band_state.cmyk_buffer.is_some()
3872 && group_content_is_native_cmyk(elements);
3873 inversion_sensitive || cmyk_group_blend
3874 }
3875 };
3876 let needs_alpha_extraction = !params.isolated
3881 && params.blend_mode != 0
3882 && !plan_cmyk_compose
3883 && !ctx.alpha_extraction_pass;
3884 let needs_backdrop_preload =
3885 !params.isolated && (params.blend_mode == 0 || plan_cmyk_compose || needs_alpha_extraction);
3886 let backdrop = if needs_backdrop_preload {
3887 let data = if crop.is_some() {
3888 copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h)
3889 } else {
3890 pixmap.data().to_vec()
3891 };
3892 offscreen.data_mut().copy_from_slice(&data);
3893 Some(data)
3894 } else {
3895 None
3896 };
3897 let group_cmyk = if needs_group_cmyk {
3898 let buf_size = eff_w as usize * eff_h as usize * 4;
3899 let mut buf = vec![0.0f32; buf_size];
3900 if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
3901 let parent_stride = ctx.out_w as usize * 4;
3902 let group_stride = eff_w as usize * 4;
3903 for gy in 0..eff_h as usize {
3904 let py = crop_y as usize + gy;
3905 if py < ctx.out_h as usize {
3906 let p_start = py * parent_stride + crop_x as usize * 4;
3907 let g_start = gy * group_stride;
3908 let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
3909 buf[g_start..g_start + copy_len]
3910 .copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
3911 }
3912 }
3913 }
3914 Some(buf)
3915 } else {
3916 None
3917 };
3918
3919 let backdrop_cmyk: Option<Vec<f32>> = if !params.isolated {
3924 group_cmyk.clone()
3925 } else {
3926 None
3927 };
3928
3929 let mut group_band = BandState {
3930 clip_region: None,
3931 spare_mask: None,
3932 clip_mask_cache: HashMap::new(),
3933 clip_mask_seen: HashSet::new(),
3934 mask_pool: Vec::new(),
3935 cmyk_buffer: group_cmyk,
3936 op_bg_snapshot: None,
3937 op_touched: None,
3938 spot_mask: None,
3939 };
3940
3941 let group_ctx = RenderContext {
3942 vp_x: eff_vp_x,
3943 vp_y: eff_vp_y,
3944 scale_x: ctx.scale_x,
3945 scale_y: ctx.scale_y,
3946 out_w: eff_w,
3947 out_h: eff_h,
3948 effective_dpi: ctx.effective_dpi,
3949 icc: ctx.icc,
3950 image_cache: None, preprocessed: None,
3952 elem_idx: 0,
3953 no_aa: ctx.no_aa,
3954 opm_zero_transparent: ctx.opm_zero_transparent,
3955 knockout_painter_pass: ctx.knockout_painter_pass,
3956 parent_group_isolated: params.isolated,
3958 alpha_extraction_pass: ctx.alpha_extraction_pass,
3959 layer_set: ctx.layer_set,
3960 };
3961
3962 let skip_indices = compute_obscured_fill_skips(elements);
3963 for (idx, elem) in elements.elements().iter().enumerate() {
3964 if skip_indices.contains(&idx) {
3965 continue;
3966 }
3967 let elem_ctx = RenderContext {
3968 elem_idx: idx,
3969 ..group_ctx
3970 };
3971 render_element(&mut offscreen, &mut group_band, elem, &elem_ctx);
3972 }
3973
3974 let alpha_offscreen = if needs_alpha_extraction {
3978 let mut iso = Pixmap::new(eff_w, eff_h);
3979 if let Some(ref mut iso_pm) = iso {
3980 let mut iso_band = BandState {
3981 clip_region: None,
3982 spare_mask: None,
3983 clip_mask_cache: HashMap::new(),
3984 clip_mask_seen: HashSet::new(),
3985 mask_pool: Vec::new(),
3986 cmyk_buffer: None,
3987 op_bg_snapshot: None,
3988 op_touched: None,
3989 spot_mask: None,
3990 };
3991 let iso_ctx = RenderContext {
3992 parent_group_isolated: true,
3993 alpha_extraction_pass: true,
3994 ..group_ctx
3995 };
3996 for (idx, elem) in elements.elements().iter().enumerate() {
3997 let elem_ctx = RenderContext {
3998 elem_idx: idx,
3999 ..iso_ctx
4000 };
4001 render_element(iso_pm, &mut iso_band, elem, &elem_ctx);
4002 }
4003 }
4004 iso
4005 } else {
4006 None
4007 };
4008
4009 let mut temp_mask = None;
4010 let mask_ref = match resolve_clip_mask(
4011 &band_state.clip_region,
4012 &mut temp_mask,
4013 ctx.out_w,
4014 ctx.out_h,
4015 ) {
4016 None => return, Some(m) => m,
4018 };
4019
4020 let coverage_params;
4025 let effective_params: &stet_graphics::display_list::GroupParams =
4026 if ctx.knockout_painter_pass == KnockoutPainterPass::CoveragePass {
4027 coverage_params = stet_graphics::display_list::GroupParams {
4028 alpha: 1.0,
4029 blend_mode: 0,
4030 ..params.clone()
4031 };
4032 &coverage_params
4033 } else {
4034 params
4035 };
4036
4037 let mut cmyk_compose_done = false;
4038 if let Some(backdrop) = &backdrop {
4039 let inner_cmyk = group_band.cmyk_buffer.as_deref();
4057 let pre_cmyk = backdrop_cmyk.as_deref();
4058 if plan_cmyk_compose && let (Some(inner), Some(pre)) = (inner_cmyk, pre_cmyk) {
4059 composite_non_isolated_cmyk(
4060 pixmap,
4061 band_state.cmyk_buffer.as_deref_mut(),
4062 &offscreen,
4063 inner,
4064 pre,
4065 backdrop,
4066 effective_params,
4067 mask_ref,
4068 crop_x,
4069 crop_y,
4070 ctx.icc,
4071 );
4072 cmyk_compose_done = true;
4073 } else if let Some(ref alpha_os) = alpha_offscreen {
4074 composite_non_isolated_extracted(
4075 pixmap,
4076 &offscreen,
4077 alpha_os,
4078 backdrop,
4079 effective_params,
4080 mask_ref,
4081 crop_x,
4082 crop_y,
4083 );
4084 } else {
4085 composite_non_isolated_group_cropped(
4086 pixmap,
4087 &offscreen,
4088 backdrop,
4089 effective_params,
4090 mask_ref,
4091 crop_x,
4092 crop_y,
4093 );
4094 }
4095 } else {
4096 let paint = stet_tiny_skia::PixmapPaint {
4097 opacity: effective_params.alpha as f32,
4098 blend_mode: u8_to_blend_mode(effective_params.blend_mode),
4099 quality: stet_tiny_skia::FilterQuality::Nearest,
4100 };
4101 pixmap.draw_pixmap(
4102 crop_x,
4103 crop_y,
4104 offscreen.as_ref(),
4105 &paint,
4106 Transform::identity(),
4107 mask_ref,
4108 );
4109 }
4110
4111 if !cmyk_compose_done
4117 && let (Some(group_cmyk), Some(parent_cmyk)) =
4118 (&group_band.cmyk_buffer, &mut band_state.cmyk_buffer)
4119 {
4120 copy_cmyk_buffer_to_parent(
4121 parent_cmyk,
4122 group_cmyk,
4123 offscreen.data(),
4124 crop_x as usize,
4125 crop_y as usize,
4126 eff_w as usize,
4127 eff_h as usize,
4128 ctx.out_w as usize,
4129 ctx.out_h as usize,
4130 );
4131 }
4132}
4133
4134#[allow(clippy::too_many_arguments)]
4146fn composite_non_isolated_cmyk(
4147 target: &mut Pixmap,
4148 parent_cmyk: Option<&mut [f32]>,
4149 source: &Pixmap,
4150 source_cmyk: &[f32],
4151 backdrop_cmyk: &[f32],
4152 backdrop_pixels: &[u8],
4153 params: &stet_graphics::display_list::GroupParams,
4154 clip_mask: Option<&stet_tiny_skia::Mask>,
4155 crop_x: i32,
4156 crop_y: i32,
4157 icc: Option<&IccCache>,
4158) {
4159 let cw = source.width() as usize;
4160 let ch = source.height() as usize;
4161 let target_w = target.width() as usize;
4162 let target_h = target.height() as usize;
4163
4164 let opacity = params.alpha.clamp(0.0, 1.0);
4165 let blend_mode = params.blend_mode;
4166 let is_nonseparable = matches!(blend_mode, 12..=15);
4167
4168 let target_data = target.data_mut();
4169 let target_stride = target_w * 4;
4170 let group_stride = cw * 4;
4171
4172 let clip_data = clip_mask.map(|m| m.data());
4173
4174 for gy in 0..ch {
4175 let ty = crop_y + gy as i32;
4176 if ty < 0 || ty as usize >= target_h {
4177 continue;
4178 }
4179 let ty = ty as usize;
4180 let group_row = gy * group_stride;
4181 let target_row = ty * target_stride;
4182
4183 for gx in 0..cw {
4184 let tx = crop_x + gx as i32;
4185 if tx < 0 || tx as usize >= target_w {
4186 continue;
4187 }
4188 let tx = tx as usize;
4189 let gi = group_row + gx * 4;
4190 let ti = target_row + tx * 4;
4191
4192 let bc = backdrop_cmyk[gi] as f64;
4194 let bm = backdrop_cmyk[gi + 1] as f64;
4195 let by_ = backdrop_cmyk[gi + 2] as f64;
4196 let bk = backdrop_cmyk[gi + 3] as f64;
4197 let sc = source_cmyk[gi] as f64;
4198 let sm = source_cmyk[gi + 1] as f64;
4199 let sy_ = source_cmyk[gi + 2] as f64;
4200 let sk = source_cmyk[gi + 3] as f64;
4201 if (sc - bc).abs() < 1.0 / 255.0
4202 && (sm - bm).abs() < 1.0 / 255.0
4203 && (sy_ - by_).abs() < 1.0 / 255.0
4204 && (sk - bk).abs() < 1.0 / 255.0
4205 {
4206 continue;
4207 }
4208
4209 let cov = if let Some(cd) = clip_data {
4211 cd[ty * target_w + tx] as f64 / 255.0
4212 } else {
4213 1.0
4214 };
4215 if cov <= 0.0 {
4216 continue;
4217 }
4218
4219 let backdrop_alpha = backdrop_pixels[gi + 3];
4238 let backdrop_transparent = backdrop_alpha == 0;
4239
4240 let mix = cov * opacity;
4241 let dst_a = target_data[ti + 3] as f64 / 255.0;
4242
4243 if backdrop_transparent {
4244 let src_data = source.data();
4257 let src_a_pm = src_data[gi + 3] as f64 / 255.0;
4258 if src_a_pm <= 0.0 {
4259 continue;
4260 }
4261 let (full_r, full_g, full_b) = icc
4266 .and_then(|i| i.convert_cmyk_readonly(sc, sm, sy_, sk))
4267 .unwrap_or_else(|| cmyk_to_rgb_plrm(sc, sm, sy_, sk));
4268 let alpha_s = mix;
4269 let inv_sa = 1.0 - alpha_s;
4270 let dst_r_pm = target_data[ti] as f64 / 255.0;
4271 let dst_g_pm = target_data[ti + 1] as f64 / 255.0;
4272 let dst_b_pm = target_data[ti + 2] as f64 / 255.0;
4273 let out_r = full_r * alpha_s + dst_r_pm * inv_sa;
4274 let out_g = full_g * alpha_s + dst_g_pm * inv_sa;
4275 let out_b = full_b * alpha_s + dst_b_pm * inv_sa;
4276 let out_a = alpha_s + dst_a * inv_sa;
4277 target_data[ti] = (out_r * 255.0).round().clamp(0.0, 255.0) as u8;
4278 target_data[ti + 1] = (out_g * 255.0).round().clamp(0.0, 255.0) as u8;
4279 target_data[ti + 2] = (out_b * 255.0).round().clamp(0.0, 255.0) as u8;
4280 target_data[ti + 3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4281 continue;
4282 }
4283
4284 let (rc, rm, ry, rk) = if is_nonseparable {
4286 let r = blend_cmyk_nonseparable([bc, bm, by_, bk], [sc, sm, sy_, sk], blend_mode);
4287 (r[0], r[1], r[2], r[3])
4288 } else {
4289 (
4290 blend_cmyk_separable_channel(bc, sc, blend_mode),
4291 blend_cmyk_separable_channel(bm, sm, blend_mode),
4292 blend_cmyk_separable_channel(by_, sy_, blend_mode),
4293 blend_cmyk_separable_channel(bk, sk, blend_mode),
4294 )
4295 };
4296
4297 let (new_r, new_g, new_b) = icc
4298 .and_then(|i| i.convert_cmyk_readonly(rc, rm, ry, rk))
4299 .unwrap_or_else(|| cmyk_to_rgb_plrm(rc, rm, ry, rk));
4300
4301 let alpha_s = mix;
4314 let alpha_b = dst_a;
4315 let out_a = alpha_s + alpha_b * (1.0 - alpha_s);
4316 if out_a <= 0.0 {
4317 continue;
4318 }
4319 let (dst_r, dst_g, dst_b) = if alpha_b > 0.0 {
4320 let inv_a = 1.0 / alpha_b;
4321 (
4322 (target_data[ti] as f64 / 255.0) * inv_a,
4323 (target_data[ti + 1] as f64 / 255.0) * inv_a,
4324 (target_data[ti + 2] as f64 / 255.0) * inv_a,
4325 )
4326 } else {
4327 (0.0, 0.0, 0.0)
4328 };
4329 let coef_b = alpha_s * alpha_b;
4337 let coef_s = alpha_s * (1.0 - alpha_b);
4338 let coef_d = (1.0 - alpha_s) * alpha_b;
4339 let out_r = (coef_s * new_r + coef_b * new_r + coef_d * dst_r) / out_a;
4340 let out_g = (coef_s * new_g + coef_b * new_g + coef_d * dst_g) / out_a;
4341 let out_b = (coef_s * new_b + coef_b * new_b + coef_d * dst_b) / out_a;
4342
4343 target_data[ti] = (out_r * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4344 target_data[ti + 1] = (out_g * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4345 target_data[ti + 2] = (out_b * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4346 target_data[ti + 3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4347 }
4348 }
4349
4350 if let Some(parent_cmyk) = parent_cmyk {
4355 for gy in 0..ch {
4356 let ty = crop_y + gy as i32;
4357 if ty < 0 || ty as usize >= target_h {
4358 continue;
4359 }
4360 let ty = ty as usize;
4361 let group_row = gy * group_stride;
4362 let parent_row = ty * target_stride;
4363
4364 for gx in 0..cw {
4365 let tx = crop_x + gx as i32;
4366 if tx < 0 || tx as usize >= target_w {
4367 continue;
4368 }
4369 let tx = tx as usize;
4370 let gi = group_row + gx * 4;
4371 let pi = parent_row + tx * 4;
4372
4373 let bc = backdrop_cmyk[gi] as f64;
4374 let bm = backdrop_cmyk[gi + 1] as f64;
4375 let by_ = backdrop_cmyk[gi + 2] as f64;
4376 let bk = backdrop_cmyk[gi + 3] as f64;
4377 let sc = source_cmyk[gi] as f64;
4378 let sm = source_cmyk[gi + 1] as f64;
4379 let sy_ = source_cmyk[gi + 2] as f64;
4380 let sk = source_cmyk[gi + 3] as f64;
4381 if (sc - bc).abs() < 1.0 / 255.0
4382 && (sm - bm).abs() < 1.0 / 255.0
4383 && (sy_ - by_).abs() < 1.0 / 255.0
4384 && (sk - bk).abs() < 1.0 / 255.0
4385 {
4386 continue;
4387 }
4388
4389 let backdrop_transparent = backdrop_pixels[gi + 3] == 0;
4395 let (rc, rm, ry, rk) = if backdrop_transparent {
4396 (sc, sm, sy_, sk)
4397 } else if is_nonseparable {
4398 let r =
4399 blend_cmyk_nonseparable([bc, bm, by_, bk], [sc, sm, sy_, sk], blend_mode);
4400 (r[0], r[1], r[2], r[3])
4401 } else {
4402 (
4403 blend_cmyk_separable_channel(bc, sc, blend_mode),
4404 blend_cmyk_separable_channel(bm, sm, blend_mode),
4405 blend_cmyk_separable_channel(by_, sy_, blend_mode),
4406 blend_cmyk_separable_channel(bk, sk, blend_mode),
4407 )
4408 };
4409 parent_cmyk[pi] = rc as f32;
4410 parent_cmyk[pi + 1] = rm as f32;
4411 parent_cmyk[pi + 2] = ry as f32;
4412 parent_cmyk[pi + 3] = rk as f32;
4413 }
4414 }
4415 }
4416}
4417
4418fn render_knockout_group(
4423 pixmap: &mut Pixmap,
4424 band_state: &mut BandState,
4425 elements: &DisplayList,
4426 params: &stet_graphics::display_list::GroupParams,
4427 ctx: &RenderContext<'_>,
4428) {
4429 let crop = compute_group_crop(¶ms.bbox, ctx);
4430
4431 let (eff_w, eff_h, crop_x, crop_y, eff_vp_x, eff_vp_y) = match crop {
4432 Some((cx, cy, cw, ch)) => (
4433 cw,
4434 ch,
4435 cx,
4436 cy,
4437 ctx.vp_x + cx as f32 / ctx.scale_x,
4438 ctx.vp_y + cy as f32 / ctx.scale_y,
4439 ),
4440 None => (ctx.out_w, ctx.out_h, 0, 0, ctx.vp_x, ctx.vp_y),
4441 };
4442
4443 let Some(mut offscreen) = Pixmap::new(eff_w, eff_h) else {
4444 return;
4445 };
4446
4447 let initial_backdrop = if !params.isolated {
4448 if crop.is_some() {
4449 copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h)
4450 } else {
4451 pixmap.data().to_vec()
4452 }
4453 } else {
4454 vec![0u8; (eff_w * eff_h * 4) as usize]
4455 };
4456
4457 let Some(mut accumulated) = Pixmap::new(eff_w, eff_h) else {
4458 return;
4459 };
4460 accumulated.data_mut().copy_from_slice(&initial_backdrop);
4461
4462 let needs_cmyk = has_overprint_elements(elements) || band_state.cmyk_buffer.is_some();
4464 let initial_cmyk = if needs_cmyk {
4465 let buf_size = eff_w as usize * eff_h as usize * 4;
4466 let mut buf = vec![0.0f32; buf_size];
4467 if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
4468 let parent_stride = ctx.out_w as usize * 4;
4469 let group_stride = eff_w as usize * 4;
4470 for gy in 0..eff_h as usize {
4471 let py = crop_y as usize + gy;
4472 if py < ctx.out_h as usize {
4473 let p_start = py * parent_stride + crop_x as usize * 4;
4474 let g_start = gy * group_stride;
4475 let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
4476 buf[g_start..g_start + copy_len]
4477 .copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
4478 }
4479 }
4480 }
4481 Some(buf)
4482 } else {
4483 None
4484 };
4485
4486 let mut accumulated_cmyk = initial_cmyk.clone();
4487
4488 let group_ctx = RenderContext {
4493 vp_x: eff_vp_x,
4494 vp_y: eff_vp_y,
4495 scale_x: ctx.scale_x,
4496 scale_y: ctx.scale_y,
4497 out_w: eff_w,
4498 out_h: eff_h,
4499 effective_dpi: ctx.effective_dpi,
4500 icc: ctx.icc,
4501 image_cache: None,
4502 preprocessed: None,
4503 elem_idx: 0,
4504 no_aa: true,
4505 opm_zero_transparent: ctx.opm_zero_transparent,
4506 knockout_painter_pass: ctx.knockout_painter_pass,
4507 parent_group_isolated: true,
4511 alpha_extraction_pass: false,
4512 layer_set: ctx.layer_set,
4513 };
4514
4515 let mut ko_band = BandState {
4519 clip_region: None,
4520 spare_mask: None,
4521 clip_mask_cache: HashMap::new(),
4522 clip_mask_seen: HashSet::new(),
4523 mask_pool: Vec::new(),
4524 cmyk_buffer: None,
4525 op_bg_snapshot: None,
4526 op_touched: None,
4527 spot_mask: None,
4528 };
4529
4530 let mut coverage_offscreen: Option<Pixmap> = None;
4533
4534 for elem in elements.elements() {
4535 match elem {
4536 DisplayElement::Clip { .. } | DisplayElement::InitClip => {
4538 render_element(&mut offscreen, &mut ko_band, elem, &group_ctx);
4539 }
4540 DisplayElement::Group { .. } => {
4550 let pass1_ctx = RenderContext {
4557 knockout_painter_pass: KnockoutPainterPass::ColorPass,
4558 ..group_ctx
4559 };
4560 offscreen.data_mut().copy_from_slice(&initial_backdrop);
4561 ko_band.cmyk_buffer = initial_cmyk.clone();
4562 render_element(&mut offscreen, &mut ko_band, elem, &pass1_ctx);
4563 let pass1_cmyk = ko_band.cmyk_buffer.take();
4564
4565 let cov = match coverage_offscreen.as_mut() {
4570 Some(p) => {
4571 p.data_mut().fill(0);
4572 p
4573 }
4574 None => {
4575 let Some(p) = Pixmap::new(eff_w, eff_h) else {
4576 replace_changed_pixels(
4580 accumulated.data_mut(),
4581 offscreen.data(),
4582 &initial_backdrop,
4583 );
4584 if let (Some(p1), Some(acc)) = (&pass1_cmyk, &mut accumulated_cmyk) {
4585 replace_changed_cmyk(acc, p1, offscreen.data(), &initial_backdrop);
4586 }
4587 continue;
4588 };
4589 coverage_offscreen = Some(p);
4590 coverage_offscreen.as_mut().unwrap()
4591 }
4592 };
4593 ko_band.cmyk_buffer = None;
4594 let coverage_ctx = RenderContext {
4599 knockout_painter_pass: KnockoutPainterPass::CoveragePass,
4600 ..group_ctx
4601 };
4602 render_element(cov, &mut ko_band, elem, &coverage_ctx);
4603
4604 replace_with_coverage_mask(accumulated.data_mut(), offscreen.data(), cov.data());
4608
4609 if let (Some(p1_cmyk), Some(acc_cmyk)) = (&pass1_cmyk, &mut accumulated_cmyk) {
4610 replace_cmyk_with_coverage_mask(acc_cmyk, p1_cmyk, cov.data());
4611 }
4612 ko_band.cmyk_buffer = None;
4613 }
4614 _ => {
4618 offscreen.data_mut().copy_from_slice(&initial_backdrop);
4619
4620 ko_band.cmyk_buffer = initial_cmyk.clone();
4621
4622 render_element(&mut offscreen, &mut ko_band, elem, &group_ctx);
4623
4624 if let (Some(elem_cmyk), Some(acc_cmyk)) =
4625 (&ko_band.cmyk_buffer, &mut accumulated_cmyk)
4626 {
4627 replace_changed_cmyk(acc_cmyk, elem_cmyk, offscreen.data(), &initial_backdrop);
4628 }
4629 ko_band.cmyk_buffer = None;
4630
4631 replace_changed_pixels(accumulated.data_mut(), offscreen.data(), &initial_backdrop);
4632 }
4633 }
4634 }
4635
4636 let mut temp_mask = None;
4637 let mask_ref = resolve_clip_mask(
4638 &band_state.clip_region,
4639 &mut temp_mask,
4640 ctx.out_w,
4641 ctx.out_h,
4642 );
4643 let mask_ref = match mask_ref {
4644 None => return,
4645 Some(m) => m,
4646 };
4647
4648 composite_non_isolated_group_cropped(
4649 pixmap,
4650 &accumulated,
4651 &initial_backdrop,
4652 params,
4653 mask_ref,
4654 crop_x,
4655 crop_y,
4656 );
4657
4658 if let (Some(acc_cmyk), Some(parent_cmyk)) = (&accumulated_cmyk, &mut band_state.cmyk_buffer) {
4659 copy_cmyk_buffer_to_parent(
4660 parent_cmyk,
4661 acc_cmyk,
4662 accumulated.data(),
4663 crop_x as usize,
4664 crop_y as usize,
4665 eff_w as usize,
4666 eff_h as usize,
4667 ctx.out_w as usize,
4668 ctx.out_h as usize,
4669 );
4670 }
4671}
4672fn replace_with_coverage_mask(target: &mut [u8], source: &[u8], coverage: &[u8]) {
4680 for i in (0..target.len()).step_by(4) {
4681 let cov_a = coverage[i + 3];
4682 if cov_a == 0 {
4683 continue;
4684 }
4685 if cov_a == 255 {
4686 target[i..i + 4].copy_from_slice(&source[i..i + 4]);
4687 continue;
4688 }
4689 let a = cov_a as u32;
4690 let inv = 255 - a;
4691 for c in 0..4 {
4692 let s = source[i + c] as u32;
4693 let t = target[i + c] as u32;
4694 target[i + c] = ((s * a + t * inv + 127) / 255) as u8;
4695 }
4696 }
4697}
4698
4699fn replace_cmyk_with_coverage_mask(target: &mut [f32], source: &[f32], coverage: &[u8]) {
4703 let pixel_count = target.len() / 4;
4704 for i in 0..pixel_count {
4705 let pi = i * 4;
4706 let cov_a = coverage[pi + 3];
4707 if cov_a == 0 {
4708 continue;
4709 }
4710 if cov_a == 255 {
4711 target[pi..pi + 4].copy_from_slice(&source[pi..pi + 4]);
4712 continue;
4713 }
4714 let a = cov_a as f32 / 255.0;
4715 let inv = 1.0 - a;
4716 for c in 0..4 {
4717 target[pi + c] = source[pi + c] * a + target[pi + c] * inv;
4718 }
4719 }
4720}
4721
4722fn replace_changed_pixels(target: &mut [u8], source: &[u8], backdrop: &[u8]) {
4726 for i in (0..target.len()).step_by(4) {
4727 if source[i] != backdrop[i]
4728 || source[i + 1] != backdrop[i + 1]
4729 || source[i + 2] != backdrop[i + 2]
4730 || source[i + 3] != backdrop[i + 3]
4731 {
4732 target[i..i + 4].copy_from_slice(&source[i..i + 4]);
4733 }
4734 }
4735}
4736
4737#[allow(clippy::too_many_arguments)]
4741fn copy_cmyk_buffer_to_parent(
4742 parent_cmyk: &mut [f32],
4743 group_cmyk: &[f32],
4744 group_pixels: &[u8],
4745 crop_x: usize,
4746 crop_y: usize,
4747 group_w: usize,
4748 group_h: usize,
4749 parent_w: usize,
4750 parent_h: usize,
4751) {
4752 let parent_stride = parent_w * 4;
4753 let group_stride = group_w * 4;
4754 for gy in 0..group_h {
4755 let py = crop_y + gy;
4756 if py >= parent_h {
4757 break;
4758 }
4759 for gx in 0..group_w {
4760 let px = crop_x + gx;
4761 if px >= parent_w {
4762 break;
4763 }
4764 let g_pixel_idx = (gy * group_w + gx) * 4;
4769 let g_cmyk_idx = gy * group_stride + gx * 4;
4770 if group_pixels[g_pixel_idx + 3] > 0
4771 && (group_cmyk[g_cmyk_idx] != 0.0
4772 || group_cmyk[g_cmyk_idx + 1] != 0.0
4773 || group_cmyk[g_cmyk_idx + 2] != 0.0
4774 || group_cmyk[g_cmyk_idx + 3] != 0.0)
4775 {
4776 let p_cmyk_idx = py * parent_stride + px * 4;
4777 parent_cmyk[p_cmyk_idx..p_cmyk_idx + 4]
4778 .copy_from_slice(&group_cmyk[g_cmyk_idx..g_cmyk_idx + 4]);
4779 }
4780 }
4781 }
4782}
4783
4784fn replace_changed_cmyk(
4787 target_cmyk: &mut [f32],
4788 source_cmyk: &[f32],
4789 source_pixels: &[u8],
4790 backdrop_pixels: &[u8],
4791) {
4792 let pixel_count = target_cmyk.len() / 4;
4793 for i in 0..pixel_count {
4794 let pi = i * 4;
4795 if source_pixels[pi] != backdrop_pixels[pi]
4796 || source_pixels[pi + 1] != backdrop_pixels[pi + 1]
4797 || source_pixels[pi + 2] != backdrop_pixels[pi + 2]
4798 || source_pixels[pi + 3] != backdrop_pixels[pi + 3]
4799 {
4800 target_cmyk[pi..pi + 4].copy_from_slice(&source_cmyk[pi..pi + 4]);
4801 }
4802 }
4803}
4804
4805#[allow(clippy::too_many_arguments)]
4813fn render_soft_masked(
4814 pixmap: &mut Pixmap,
4815 band_state: &mut BandState,
4816 mask_list: &DisplayList,
4817 content_list: &DisplayList,
4818 params: &stet_graphics::display_list::SoftMaskParams,
4819 mask_cache: &Arc<Mutex<Option<Option<stet_graphics::display_list::MaskRaster>>>>,
4820 ctx: &RenderContext<'_>,
4821) {
4822 let use_inline_mask = ctx.vp_x != 0.0;
4839 let bbox = ¶ms.bbox;
4840 let smask_px_x0 = ((bbox[0] as f32 - ctx.vp_x) * ctx.scale_x).floor() as i32;
4841 let smask_px_y0 = ((bbox[1] as f32 - ctx.vp_y) * ctx.scale_y).floor() as i32;
4842 let smask_px_x1 = ((bbox[2] as f32 - ctx.vp_x) * ctx.scale_x).ceil() as i32;
4843 let smask_px_y1 = ((bbox[3] as f32 - ctx.vp_y) * ctx.scale_y).ceil() as i32;
4844
4845 let crop_x = smask_px_x0.max(0);
4847 let crop_y = smask_px_y0.max(0);
4848 let crop_x1 = smask_px_x1.min(ctx.out_w as i32);
4849 let crop_y1 = smask_px_y1.min(ctx.out_h as i32);
4850 if crop_x >= crop_x1 || crop_y >= crop_y1 {
4851 return;
4852 }
4853 let eff_w = (crop_x1 - crop_x) as u32;
4854 let eff_h = (crop_y1 - crop_y) as u32;
4855
4856 let eff_vp_x = ctx.vp_x + crop_x as f32 / ctx.scale_x;
4861 let eff_vp_y = ctx.vp_y + crop_y as f32 / ctx.scale_y;
4862
4863 let sub_ctx = RenderContext {
4864 vp_x: eff_vp_x,
4865 vp_y: eff_vp_y,
4866 scale_x: ctx.scale_x,
4867 scale_y: ctx.scale_y,
4868 out_w: eff_w,
4869 out_h: eff_h,
4870 effective_dpi: ctx.effective_dpi,
4871 icc: ctx.icc,
4872 image_cache: None,
4873 preprocessed: None,
4874 elem_idx: 0,
4875 no_aa: ctx.no_aa,
4876 opm_zero_transparent: ctx.opm_zero_transparent,
4877 knockout_painter_pass: ctx.knockout_painter_pass,
4878 parent_group_isolated: ctx.parent_group_isolated,
4879 alpha_extraction_pass: false,
4883 layer_set: ctx.layer_set,
4884 };
4885
4886 let mut mask_values_inline: Vec<u8> = Vec::new();
4890 if use_inline_mask {
4891 let Some(mut mask_pixmap) = Pixmap::new(eff_w, eff_h) else {
4892 return;
4893 };
4894 let mut mask_band = BandState {
4895 clip_region: None,
4896 spare_mask: None,
4897 clip_mask_cache: HashMap::new(),
4898 clip_mask_seen: HashSet::new(),
4899 mask_pool: Vec::new(),
4900 cmyk_buffer: None,
4901 op_bg_snapshot: None,
4902 op_touched: None,
4903 spot_mask: None,
4904 };
4905 for (idx, elem) in mask_list.elements().iter().enumerate() {
4906 let elem_ctx = RenderContext {
4907 elem_idx: idx,
4908 ..sub_ctx
4909 };
4910 render_element(&mut mask_pixmap, &mut mask_band, elem, &elem_ctx);
4911 }
4912 if params.has_nested_mask_scope
4913 && params.subtype == stet_graphics::display_list::SoftMaskSubtype::Luminosity
4914 {
4915 let bc = params.backdrop_color.as_ref();
4916 let bd_r = bc.map_or(0u8, |c| (c[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
4917 let bd_g = bc.map_or(0u8, |c| (c[1].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
4918 let bd_b = bc.map_or(0u8, |c| (c[2].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
4919 for chunk in mask_pixmap.data_mut().chunks_exact_mut(4) {
4920 let a = chunk[3] as u16;
4921 if a == 255 {
4922 continue;
4923 }
4924 let inv_a = 255 - a;
4925 chunk[0] = ((chunk[0] as u16 * 255 + bd_r as u16 * inv_a + 127) / 255) as u8;
4926 chunk[1] = ((chunk[1] as u16 * 255 + bd_g as u16 * inv_a + 127) / 255) as u8;
4927 chunk[2] = ((chunk[2] as u16 * 255 + bd_b as u16 * inv_a + 127) / 255) as u8;
4928 chunk[3] = 255;
4929 }
4930 }
4931 mask_values_inline = vec![0u8; (eff_w * eff_h) as usize];
4932 extract_soft_mask_values(mask_pixmap.data(), &mut mask_values_inline, params);
4933 }
4934
4935 let raster_owned: Option<stet_graphics::display_list::MaskRaster> = if use_inline_mask {
4937 None
4938 } else {
4939 let mut guard = mask_cache.lock().unwrap();
4940 let needs_build = match guard.as_ref() {
4941 None => true,
4942 Some(None) => false, Some(Some(r)) => {
4944 (r.scale_x - ctx.scale_x).abs() > 1e-4 || (r.scale_y - ctx.scale_y).abs() > 1e-4
4945 }
4946 };
4947 if needs_build {
4948 let built = rasterize_mask(
4949 mask_list,
4950 params,
4951 ctx.icc,
4952 ctx.no_aa,
4953 ctx.effective_dpi,
4954 ctx.scale_x,
4955 ctx.scale_y,
4956 ctx.layer_set,
4957 );
4958 *guard = Some(built);
4959 }
4960 guard.as_ref().and_then(|inner| inner.clone())
4961 };
4962
4963 let fallback_mask = out_of_bounds_mask_value(params) as i32;
4967
4968 let Some(mut content_pixmap) = Pixmap::new(eff_w, eff_h) else {
4972 return;
4973 };
4974 let backdrop = copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h);
4975 content_pixmap.data_mut().copy_from_slice(&backdrop);
4976
4977 let content_cmyk = if has_overprint_elements(content_list) || band_state.cmyk_buffer.is_some() {
4978 let buf_size = eff_w as usize * eff_h as usize * 4;
4979 let mut buf = vec![0.0f32; buf_size];
4980 if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
4981 let parent_stride = ctx.out_w as usize * 4;
4982 let group_stride = eff_w as usize * 4;
4983 for gy in 0..eff_h as usize {
4984 let py = crop_y as usize + gy;
4985 if py < ctx.out_h as usize {
4986 let p_start = py * parent_stride + crop_x as usize * 4;
4987 let g_start = gy * group_stride;
4988 let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
4989 buf[g_start..g_start + copy_len]
4990 .copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
4991 }
4992 }
4993 }
4994 Some(buf)
4995 } else {
4996 None
4997 };
4998 let backdrop_cmyk: Option<Vec<f32>> = content_cmyk.clone();
5005 let mut content_band = BandState {
5006 clip_region: None,
5007 spare_mask: None,
5008 clip_mask_cache: HashMap::new(),
5009 clip_mask_seen: HashSet::new(),
5010 mask_pool: Vec::new(),
5011 cmyk_buffer: content_cmyk,
5012 op_bg_snapshot: None,
5013 op_touched: None,
5014 spot_mask: None,
5015 };
5016 for (idx, elem) in content_list.elements().iter().enumerate() {
5017 let elem_ctx = RenderContext {
5018 elem_idx: idx,
5019 ..sub_ctx
5020 };
5021 render_element(&mut content_pixmap, &mut content_band, elem, &elem_ctx);
5022 }
5023
5024 let vp_x_pixels = (ctx.vp_x * ctx.scale_x).round() as i32;
5044 let vp_y_pixels = (ctx.vp_y * ctx.scale_y).round() as i32;
5045
5046 let mut temp_mask = None;
5047 let clip_ref = resolve_clip_mask(
5048 &band_state.clip_region,
5049 &mut temp_mask,
5050 ctx.out_w,
5051 ctx.out_h,
5052 );
5053 let clip_ref = match clip_ref {
5054 None => return,
5055 Some(m) => m,
5056 };
5057
5058 let use_cmyk_blend = ctx.icc.is_some()
5074 && backdrop_cmyk.is_some()
5075 && content_band.cmyk_buffer.is_some()
5076 && content_list_is_simple_native_cmyk(content_list);
5077
5078 let content_data = content_pixmap.data();
5079 let parent_data = pixmap.data_mut();
5080 let parent_stride = ctx.out_w as usize * 4;
5081 let content_stride = eff_w as usize * 4;
5082
5083 for y in 0..eff_h as usize {
5084 let py = crop_y as usize + y;
5085 if py >= ctx.out_h as usize {
5086 break;
5087 }
5088 let ci_row = y * content_stride;
5089 let pi_row = py * parent_stride;
5090 let page_y = vp_y_pixels + crop_y + y as i32;
5091
5092 for x in 0..eff_w as usize {
5093 let px = crop_x as usize + x;
5094 if px >= ctx.out_w as usize {
5095 break;
5096 }
5097
5098 if let Some(clip) = clip_ref {
5100 if clip.data()[py * ctx.out_w as usize + px] == 0 {
5101 continue;
5102 }
5103 }
5104
5105 let m = if use_inline_mask {
5108 mask_values_inline[y * eff_w as usize + x] as i32
5109 } else if let Some(ref raster) = raster_owned {
5110 let page_x = vp_x_pixels + crop_x + x as i32;
5111 let mx = page_x - raster.origin_x;
5112 let my = page_y - raster.origin_y;
5113 if mx >= 0 && (mx as u32) < raster.width && my >= 0 && (my as u32) < raster.height {
5114 raster.data[my as usize * raster.width as usize + mx as usize] as i32
5115 } else {
5116 fallback_mask
5117 }
5118 } else {
5119 fallback_mask
5120 };
5121 if m == 0 {
5122 continue;
5123 }
5124
5125 let ci = ci_row + x * 4;
5126 let pi = pi_row + px * 4;
5127
5128 let ci_cmyk = (y * eff_w as usize + x) * 4;
5138 let cmyk_path_ok = use_cmyk_blend && {
5139 let bc_cmyk = &backdrop_cmyk.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5140 let cc_cmyk = &content_band.cmyk_buffer.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5141 let icc_match = |cmyk: &[f32], rgb: &[u8]| -> bool {
5142 let (r, g, b) = ctx
5143 .icc
5144 .and_then(|i| {
5145 i.convert_cmyk_readonly(
5146 cmyk[0] as f64,
5147 cmyk[1] as f64,
5148 cmyk[2] as f64,
5149 cmyk[3] as f64,
5150 )
5151 })
5152 .unwrap_or_else(|| {
5153 cmyk_to_rgb_plrm(
5154 cmyk[0] as f64,
5155 cmyk[1] as f64,
5156 cmyk[2] as f64,
5157 cmyk[3] as f64,
5158 )
5159 });
5160 let r = (r * 255.0).round() as i32;
5161 let g = (g * 255.0).round() as i32;
5162 let b = (b * 255.0).round() as i32;
5163 (r - rgb[0] as i32).abs() <= 3
5164 && (g - rgb[1] as i32).abs() <= 3
5165 && (b - rgb[2] as i32).abs() <= 3
5166 };
5167 icc_match(bc_cmyk, &backdrop[ci..ci + 3])
5168 && icc_match(cc_cmyk, &content_data[ci..ci + 3])
5169 };
5170
5171 if cmyk_path_ok {
5172 let bc_cmyk = &backdrop_cmyk.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5174 let cc_cmyk = &content_band.cmyk_buffer.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5175 let mf = m as f64 / 255.0;
5176 let rc = bc_cmyk[0] as f64 + mf * (cc_cmyk[0] as f64 - bc_cmyk[0] as f64);
5177 let rm = bc_cmyk[1] as f64 + mf * (cc_cmyk[1] as f64 - bc_cmyk[1] as f64);
5178 let ry = bc_cmyk[2] as f64 + mf * (cc_cmyk[2] as f64 - bc_cmyk[2] as f64);
5179 let rk = bc_cmyk[3] as f64 + mf * (cc_cmyk[3] as f64 - bc_cmyk[3] as f64);
5180 let (fr, fg, fb) = ctx
5181 .icc
5182 .and_then(|i| i.convert_cmyk_readonly(rc, rm, ry, rk))
5183 .unwrap_or_else(|| cmyk_to_rgb_plrm(rc, rm, ry, rk));
5184 parent_data[pi] = (fr * 255.0).round().clamp(0.0, 255.0) as u8;
5185 parent_data[pi + 1] = (fg * 255.0).round().clamp(0.0, 255.0) as u8;
5186 parent_data[pi + 2] = (fb * 255.0).round().clamp(0.0, 255.0) as u8;
5187 let content_a = content_data[ci + 3] as i32;
5189 let backdrop_a = backdrop[ci + 3] as i32;
5190 let delta = content_a - backdrop_a;
5191 if delta != 0 {
5192 let masked_delta = if delta > 0 {
5193 (delta * m + 128) / 255
5194 } else {
5195 (delta * m - 128) / 255
5196 };
5197 let result = (parent_data[pi + 3] as i32 + masked_delta).clamp(0, 255);
5198 parent_data[pi + 3] = result as u8;
5199 }
5200 } else {
5209 for c in 0..4 {
5210 let content_val = content_data[ci + c] as i32;
5211 let backdrop_val = backdrop[ci + c] as i32;
5212 let delta = content_val - backdrop_val;
5213 if delta != 0 {
5214 let masked_delta = if delta > 0 {
5215 (delta * m + 128) / 255
5216 } else {
5217 (delta * m - 128) / 255
5218 };
5219 let result = (parent_data[pi + c] as i32 + masked_delta).clamp(0, 255);
5220 parent_data[pi + c] = result as u8;
5221 }
5222 }
5223 }
5224 }
5225 }
5226
5227 if !use_cmyk_blend {
5231 if let (Some(content_cmyk), Some(parent_cmyk)) =
5232 (&content_band.cmyk_buffer, &mut band_state.cmyk_buffer)
5233 {
5234 copy_cmyk_buffer_to_parent(
5235 parent_cmyk,
5236 content_cmyk,
5237 content_pixmap.data(),
5238 crop_x as usize,
5239 crop_y as usize,
5240 eff_w as usize,
5241 eff_h as usize,
5242 ctx.out_w as usize,
5243 ctx.out_h as usize,
5244 );
5245 }
5246 }
5247}
5248fn extract_soft_mask_values(
5250 rgba: &[u8],
5251 out: &mut [u8],
5252 params: &stet_graphics::display_list::SoftMaskParams,
5253) {
5254 use stet_graphics::display_list::SoftMaskSubtype;
5255 let pixel_count = out.len();
5256
5257 match params.subtype {
5258 SoftMaskSubtype::Alpha => {
5259 for i in 0..pixel_count {
5260 let a = rgba[i * 4 + 3]; out[i] = if params.transfer_invert { 255 - a } else { a };
5262 }
5263 }
5264 SoftMaskSubtype::Luminosity => {
5265 let backdrop_lum = if let Some(bc) = ¶ms.backdrop_color {
5267 (0.2126 * bc[0] + 0.7152 * bc[1] + 0.0722 * bc[2]).clamp(0.0, 1.0)
5268 } else {
5269 0.0 };
5271 let backdrop_byte = (backdrop_lum * 255.0 + 0.5) as u8;
5272
5273 #[allow(clippy::needless_range_loop)]
5274 for i in 0..pixel_count {
5275 let off = i * 4;
5276 let a = rgba[off + 3];
5277 let lum_byte = if a == 0 {
5278 backdrop_byte
5279 } else if a < 255 {
5280 let af = a as f64;
5283 let bd = backdrop_lum * 255.0;
5284 let r = rgba[off] as f64 + bd * (255.0 - af) / 255.0;
5285 let g = rgba[off + 1] as f64 + bd * (255.0 - af) / 255.0;
5286 let b = rgba[off + 2] as f64 + bd * (255.0 - af) / 255.0;
5287 let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
5288 (lum + 0.5).clamp(0.0, 255.0) as u8
5289 } else {
5290 let lum = 0.2126 * rgba[off] as f64
5292 + 0.7152 * rgba[off + 1] as f64
5293 + 0.0722 * rgba[off + 2] as f64;
5294 (lum + 0.5).clamp(0.0, 255.0) as u8
5295 };
5296 out[i] = if params.transfer_invert {
5298 255 - lum_byte
5299 } else {
5300 lum_byte
5301 };
5302 }
5303 }
5304 }
5305}
5306
5307fn out_of_bounds_mask_value(params: &stet_graphics::display_list::SoftMaskParams) -> u8 {
5315 use stet_graphics::display_list::SoftMaskSubtype;
5316 let raw = match params.subtype {
5317 SoftMaskSubtype::Alpha => 0u8,
5318 SoftMaskSubtype::Luminosity => {
5319 let lum = if let Some(bc) = ¶ms.backdrop_color {
5320 (0.2126 * bc[0] + 0.7152 * bc[1] + 0.0722 * bc[2]).clamp(0.0, 1.0)
5321 } else {
5322 0.0
5323 };
5324 (lum * 255.0 + 0.5) as u8
5325 }
5326 };
5327 if params.transfer_invert {
5328 255 - raw
5329 } else {
5330 raw
5331 }
5332}
5333
5334const MAX_MASK_RASTER_PIXELS: u64 = 64 * 1024 * 1024;
5341
5342fn rasterize_mask(
5354 mask_list: &DisplayList,
5355 params: &stet_graphics::display_list::SoftMaskParams,
5356 icc: Option<&IccCache>,
5357 no_aa: bool,
5358 effective_dpi: f64,
5359 scale_x: f32,
5360 scale_y: f32,
5361 layer_set: &LayerSet,
5362) -> Option<stet_graphics::display_list::MaskRaster> {
5363 let mut bounds = compute_paint_bounds(mask_list, effective_dpi)?;
5370 if let Some(cap) = params.parent_clip_bbox {
5371 let cap_bbox = BBox2D {
5372 x_min: cap[0],
5373 y_min: cap[1],
5374 x_max: cap[2],
5375 y_max: cap[3],
5376 };
5377 bounds = intersect_bbox(&bounds, &cap_bbox)?;
5378 }
5379
5380 let px_x_min = (bounds.x_min as f32 * scale_x).floor() as i32 - 1;
5383 let px_y_min = (bounds.y_min as f32 * scale_y).floor() as i32 - 1;
5384 let px_x_max = (bounds.x_max as f32 * scale_x).ceil() as i32 + 1;
5385 let px_y_max = (bounds.y_max as f32 * scale_y).ceil() as i32 + 1;
5386 if px_x_min >= px_x_max || px_y_min >= px_y_max {
5387 return None;
5388 }
5389 let raster_w = (px_x_max - px_x_min) as u32;
5390 let raster_h = (px_y_max - px_y_min) as u32;
5391 if raster_w == 0 || raster_h == 0 {
5392 return None;
5393 }
5394 if (raster_w as u64) * (raster_h as u64) > MAX_MASK_RASTER_PIXELS {
5395 return None;
5396 }
5397
5398 let mut mask_pixmap = Pixmap::new(raster_w, raster_h)?;
5400
5401 let sub_ctx = RenderContext {
5405 vp_x: px_x_min as f32 / scale_x,
5406 vp_y: px_y_min as f32 / scale_y,
5407 scale_x,
5408 scale_y,
5409 out_w: raster_w,
5410 out_h: raster_h,
5411 effective_dpi,
5412 icc,
5413 image_cache: None,
5414 preprocessed: None,
5415 elem_idx: 0,
5416 no_aa,
5417 opm_zero_transparent: false,
5418 knockout_painter_pass: KnockoutPainterPass::None,
5419 parent_group_isolated: false,
5420 alpha_extraction_pass: false,
5421 layer_set,
5422 };
5423
5424 let mut mask_band = BandState {
5426 clip_region: None,
5427 spare_mask: None,
5428 clip_mask_cache: HashMap::new(),
5429 clip_mask_seen: HashSet::new(),
5430 mask_pool: Vec::new(),
5431 cmyk_buffer: None,
5432 op_bg_snapshot: None,
5433 op_touched: None,
5434 spot_mask: None,
5435 };
5436
5437 for (idx, elem) in mask_list.elements().iter().enumerate() {
5439 let elem_ctx = RenderContext {
5440 elem_idx: idx,
5441 ..sub_ctx
5442 };
5443 render_element(&mut mask_pixmap, &mut mask_band, elem, &elem_ctx);
5444 }
5445
5446 if params.has_nested_mask_scope
5454 && params.subtype == stet_graphics::display_list::SoftMaskSubtype::Luminosity
5455 {
5456 let bc = params.backdrop_color.as_ref();
5457 let bd_r = bc.map_or(0u8, |c| (c[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
5458 let bd_g = bc.map_or(0u8, |c| (c[1].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
5459 let bd_b = bc.map_or(0u8, |c| (c[2].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
5460 for chunk in mask_pixmap.data_mut().chunks_exact_mut(4) {
5461 let a = chunk[3] as u16;
5462 if a == 255 {
5463 continue;
5464 }
5465 let inv_a = 255 - a;
5466 chunk[0] = ((chunk[0] as u16 * 255 + bd_r as u16 * inv_a + 127) / 255) as u8;
5467 chunk[1] = ((chunk[1] as u16 * 255 + bd_g as u16 * inv_a + 127) / 255) as u8;
5468 chunk[2] = ((chunk[2] as u16 * 255 + bd_b as u16 * inv_a + 127) / 255) as u8;
5469 chunk[3] = 255;
5470 }
5471 }
5472
5473 let pixel_count = (raster_w * raster_h) as usize;
5475 let mut data = vec![0u8; pixel_count];
5476 extract_soft_mask_values(mask_pixmap.data(), &mut data, params);
5477
5478 Some(stet_graphics::display_list::MaskRaster {
5479 data,
5480 width: raster_w,
5481 height: raster_h,
5482 origin_x: px_x_min,
5483 origin_y: px_y_min,
5484 scale_x,
5485 scale_y,
5486 })
5487}
5488
5489fn transform_element_ctm(elem: &DisplayElement, pm: &Matrix) -> DisplayElement {
5493 match elem {
5494 DisplayElement::Fill { path, params } => {
5495 let mut p = params.clone();
5496 p.ctm = pm.concat(&p.ctm);
5497 DisplayElement::Fill {
5498 path: path.clone(),
5499 params: p,
5500 }
5501 }
5502 DisplayElement::Stroke { path, params } => {
5503 let mut p = params.clone();
5504 p.ctm = pm.concat(&p.ctm);
5505 DisplayElement::Stroke {
5506 path: path.clone(),
5507 params: p,
5508 }
5509 }
5510 DisplayElement::Clip { path, params } => {
5511 let mut p = params.clone();
5512 p.ctm = pm.concat(&p.ctm);
5513 if let Some(ref mut sp) = p.stroke_params {
5514 sp.ctm = pm.concat(&sp.ctm);
5515 }
5516 DisplayElement::Clip {
5517 path: path.clone(),
5518 params: p,
5519 }
5520 }
5521 DisplayElement::Image {
5522 sample_data,
5523 params,
5524 } => {
5525 let mut p = params.clone();
5526 p.ctm = pm.concat(&p.ctm);
5527 DisplayElement::Image {
5528 sample_data: sample_data.clone(),
5529 params: p,
5530 }
5531 }
5532 DisplayElement::MeshShading { params } => {
5533 let mut p = params.clone();
5534 p.ctm = pm.concat(&p.ctm);
5535 DisplayElement::MeshShading { params: p }
5536 }
5537 DisplayElement::PatchShading { params } => {
5538 let mut p = params.clone();
5539 p.ctm = pm.concat(&p.ctm);
5540 DisplayElement::PatchShading { params: p }
5541 }
5542 DisplayElement::AxialShading { params } => {
5543 let mut p = params.clone();
5544 p.ctm = pm.concat(&p.ctm);
5545 DisplayElement::AxialShading { params: p }
5546 }
5547 DisplayElement::RadialShading { params } => {
5548 let mut p = params.clone();
5549 p.ctm = pm.concat(&p.ctm);
5550 DisplayElement::RadialShading { params: p }
5551 }
5552 DisplayElement::Group { elements, params } => {
5553 let mut t = DisplayList::new();
5554 for child in elements.elements() {
5555 t.push(transform_element_ctm(child, pm));
5556 }
5557 let mut p = params.clone();
5558 let corners = [
5559 pm.transform_point(p.bbox[0], p.bbox[1]),
5560 pm.transform_point(p.bbox[2], p.bbox[1]),
5561 pm.transform_point(p.bbox[0], p.bbox[3]),
5562 pm.transform_point(p.bbox[2], p.bbox[3]),
5563 ];
5564 p.bbox = [
5565 corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min),
5566 corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min),
5567 corners
5568 .iter()
5569 .map(|c| c.0)
5570 .fold(f64::NEG_INFINITY, f64::max),
5571 corners
5572 .iter()
5573 .map(|c| c.1)
5574 .fold(f64::NEG_INFINITY, f64::max),
5575 ];
5576 DisplayElement::Group {
5577 elements: t,
5578 params: p,
5579 }
5580 }
5581 DisplayElement::SoftMasked {
5582 mask,
5583 content,
5584 params,
5585 ..
5586 } => {
5587 let mut t_mask = DisplayList::new();
5588 for child in mask.elements() {
5589 t_mask.push(transform_element_ctm(child, pm));
5590 }
5591 let mut t_content = DisplayList::new();
5592 for child in content.elements() {
5593 t_content.push(transform_element_ctm(child, pm));
5594 }
5595 let mut p = params.clone();
5596 let corners = [
5597 pm.transform_point(p.bbox[0], p.bbox[1]),
5598 pm.transform_point(p.bbox[2], p.bbox[1]),
5599 pm.transform_point(p.bbox[0], p.bbox[3]),
5600 pm.transform_point(p.bbox[2], p.bbox[3]),
5601 ];
5602 p.bbox = [
5603 corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min),
5604 corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min),
5605 corners
5606 .iter()
5607 .map(|c| c.0)
5608 .fold(f64::NEG_INFINITY, f64::max),
5609 corners
5610 .iter()
5611 .map(|c| c.1)
5612 .fold(f64::NEG_INFINITY, f64::max),
5613 ];
5614 if let Some(pcb) = p.parent_clip_bbox {
5621 let pcb_corners = [
5622 pm.transform_point(pcb[0], pcb[1]),
5623 pm.transform_point(pcb[2], pcb[1]),
5624 pm.transform_point(pcb[0], pcb[3]),
5625 pm.transform_point(pcb[2], pcb[3]),
5626 ];
5627 p.parent_clip_bbox = Some([
5628 pcb_corners
5629 .iter()
5630 .map(|c| c.0)
5631 .fold(f64::INFINITY, f64::min),
5632 pcb_corners
5633 .iter()
5634 .map(|c| c.1)
5635 .fold(f64::INFINITY, f64::min),
5636 pcb_corners
5637 .iter()
5638 .map(|c| c.0)
5639 .fold(f64::NEG_INFINITY, f64::max),
5640 pcb_corners
5641 .iter()
5642 .map(|c| c.1)
5643 .fold(f64::NEG_INFINITY, f64::max),
5644 ]);
5645 }
5646 DisplayElement::SoftMasked {
5650 mask: t_mask,
5651 content: t_content,
5652 params: p,
5653 mask_cache: Arc::new(Mutex::new(None)),
5654 }
5655 }
5656 DisplayElement::PatternFill { params } => {
5657 let mut p = params.clone();
5658 p.pattern_matrix = pm.concat(&p.pattern_matrix);
5659 p.path = transform_path_by_matrix(&p.path, pm);
5661 if let Some(ref mut sp) = p.stroke_params {
5662 sp.ctm = pm.concat(&sp.ctm);
5663 }
5664 DisplayElement::PatternFill { params: p }
5665 }
5666 DisplayElement::OcgGroup {
5667 elements,
5668 visibility,
5669 } => {
5670 let mut t = DisplayList::new();
5671 for child in elements.elements() {
5672 t.push(transform_element_ctm(child, pm));
5673 }
5674 DisplayElement::OcgGroup {
5675 elements: t,
5676 visibility: visibility.clone(),
5677 }
5678 }
5679 other => other.clone(),
5680 }
5681}
5682
5683fn transform_path_by_matrix(path: &PsPath, m: &Matrix) -> PsPath {
5685 use stet_fonts::geometry::PathSegment;
5686 let mut out = PsPath::new();
5687 for seg in &path.segments {
5688 out.segments.push(match *seg {
5689 PathSegment::MoveTo(x, y) => {
5690 let (nx, ny) = m.transform_point(x, y);
5691 PathSegment::MoveTo(nx, ny)
5692 }
5693 PathSegment::LineTo(x, y) => {
5694 let (nx, ny) = m.transform_point(x, y);
5695 PathSegment::LineTo(nx, ny)
5696 }
5697 PathSegment::CurveTo {
5698 x1,
5699 y1,
5700 x2,
5701 y2,
5702 x3,
5703 y3,
5704 } => {
5705 let (nx1, ny1) = m.transform_point(x1, y1);
5706 let (nx2, ny2) = m.transform_point(x2, y2);
5707 let (nx3, ny3) = m.transform_point(x3, y3);
5708 PathSegment::CurveTo {
5709 x1: nx1,
5710 y1: ny1,
5711 x2: nx2,
5712 y2: ny2,
5713 x3: nx3,
5714 y3: ny3,
5715 }
5716 }
5717 PathSegment::ClosePath => PathSegment::ClosePath,
5718 });
5719 }
5720 out
5721}
5722
5723fn bilinear_prescale(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
5730 let mut dst = vec![0u8; (dw * dh * 4) as usize];
5731 for dy in 0..dh {
5732 let sy_f = (dy as f64 + 0.5) * sh as f64 / dh as f64 - 0.5;
5733 let sy0 = sy_f.floor().max(0.0) as u32;
5734 let sy1 = (sy0 + 1).min(sh - 1);
5735 let fy = (sy_f - sy0 as f64) as f32;
5736 let ify = 1.0 - fy;
5737 for dx in 0..dw {
5738 let sx_f = (dx as f64 + 0.5) * sw as f64 / dw as f64 - 0.5;
5739 let sx0 = sx_f.floor().max(0.0) as u32;
5740 let sx1 = (sx0 + 1).min(sw - 1);
5741 let fx = (sx_f - sx0 as f64) as f32;
5742 let ifx = 1.0 - fx;
5743
5744 let i00 = (sy0 * sw + sx0) as usize * 4;
5745 let i10 = (sy0 * sw + sx1) as usize * 4;
5746 let i01 = (sy1 * sw + sx0) as usize * 4;
5747 let i11 = (sy1 * sw + sx1) as usize * 4;
5748 let di = (dy * dw + dx) as usize * 4;
5749 for c in 0..4 {
5750 dst[di + c] = (src[i00 + c] as f32 * ifx * ify
5751 + src[i10 + c] as f32 * fx * ify
5752 + src[i01 + c] as f32 * ifx * fy
5753 + src[i11 + c] as f32 * fx * fy)
5754 .round() as u8;
5755 }
5756 }
5757 }
5758 dst
5759}
5760
5761fn render_pattern_fill(
5762 pixmap: &mut Pixmap,
5763 band_state: &mut BandState,
5764 params: &stet_graphics::device::PatternFillParams,
5765 ctx: &RenderContext<'_>,
5766) {
5767 let mut temp_mask = None;
5768 let Some(mask_ref) = resolve_clip_mask(
5769 &band_state.clip_region,
5770 &mut temp_mask,
5771 ctx.out_w,
5772 ctx.out_h,
5773 ) else {
5774 return;
5775 };
5776
5777 let pm = ¶ms.pattern_matrix;
5778
5779 let (step_ux, step_uy) = pm.transform_delta(params.xstep, 0.0);
5781 let (step_vx, step_vy) = pm.transform_delta(0.0, params.ystep);
5782
5783 let step_u_len = (step_ux * step_ux + step_uy * step_uy).sqrt();
5784 let step_v_len = (step_vx * step_vx + step_vy * step_vy).sqrt();
5785 if step_u_len < 0.01 || step_v_len < 0.01 {
5786 return;
5787 }
5788
5789 let origin_x = pm.tx;
5790 let origin_y = pm.ty;
5791
5792 let dev_vp_x = ctx.vp_x as f64;
5794 let dev_vp_y = ctx.vp_y as f64;
5795 let dev_vp_w = ctx.out_w as f64 / ctx.scale_x as f64;
5796 let dev_vp_h = ctx.out_h as f64 / ctx.scale_y as f64;
5797
5798 let (mut min_x, mut min_y, mut max_x, mut max_y) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
5799 for seg in ¶ms.path.segments {
5800 let (x, y) = match seg {
5801 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => (*x, *y),
5802 PathSegment::CurveTo { x3, y3, .. } => (*x3, *y3),
5803 PathSegment::ClosePath => continue,
5804 };
5805 min_x = min_x.min(x);
5806 min_y = min_y.min(y);
5807 max_x = max_x.max(x);
5808 max_y = max_y.max(y);
5809 }
5810
5811 if let Some(ref sp) = params.stroke_params {
5815 let ctm = &sp.ctm;
5817 let corners = [
5818 ctm.transform_point(min_x, min_y),
5819 ctm.transform_point(max_x, min_y),
5820 ctm.transform_point(min_x, max_y),
5821 ctm.transform_point(max_x, max_y),
5822 ];
5823 min_x = f64::MAX;
5824 min_y = f64::MAX;
5825 max_x = f64::MIN;
5826 max_y = f64::MIN;
5827 for (cx, cy) in &corners {
5828 min_x = min_x.min(*cx);
5829 min_y = min_y.min(*cy);
5830 max_x = max_x.max(*cx);
5831 max_y = max_y.max(*cy);
5832 }
5833 let half_w = sp.line_width
5835 * 0.5
5836 * (ctm.a * ctm.a + ctm.b * ctm.b)
5837 .sqrt()
5838 .max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt());
5839 min_x -= half_w;
5840 min_y -= half_w;
5841 max_x += half_w;
5842 max_y += half_w;
5843 }
5844
5845 min_x = min_x.max(dev_vp_x);
5847 min_y = min_y.max(dev_vp_y);
5848 max_x = max_x.min(dev_vp_x + dev_vp_w);
5849 max_y = max_y.min(dev_vp_y + dev_vp_h);
5850 if min_x >= max_x || min_y >= max_y {
5851 return;
5852 }
5853
5854 let det = step_ux * step_vy - step_uy * step_vx;
5855 if det.abs() < 1e-10 {
5856 return;
5857 }
5858 let inv_det = 1.0 / det;
5859
5860 let mut tu_min = f64::MAX;
5861 let mut tu_max = f64::MIN;
5862 let mut tv_min = f64::MAX;
5863 let mut tv_max = f64::MIN;
5864 for &(cx, cy) in &[
5865 (min_x, min_y),
5866 (max_x, min_y),
5867 (min_x, max_y),
5868 (max_x, max_y),
5869 ] {
5870 let dx = cx - origin_x;
5871 let dy = cy - origin_y;
5872 let tu = (dx * step_vy - dy * step_vx) * inv_det;
5873 let tv = (-dx * step_uy + dy * step_ux) * inv_det;
5874 tu_min = tu_min.min(tu);
5875 tu_max = tu_max.max(tu);
5876 tv_min = tv_min.min(tv);
5877 tv_max = tv_max.max(tv);
5878 }
5879
5880 let tile_x_start = tu_min.floor() as i32 - 1;
5881 let tile_x_end = tu_max.ceil() as i32 + 1;
5882 let tile_y_start = tv_min.floor() as i32 - 1;
5883 let tile_y_end = tv_max.ceil() as i32 + 1;
5884
5885 let tile_count = (tile_x_end - tile_x_start) as i64 * (tile_y_end - tile_y_start) as i64;
5886 if tile_count > 10000 {
5887 return;
5888 }
5889
5890 let Some(mut tile_buf) = Pixmap::new(ctx.out_w, ctx.out_h) else {
5891 return;
5892 };
5893
5894 let sx_f = ctx.scale_x as f64;
5895 let sy_f = ctx.scale_y as f64;
5896
5897 if params.device_space_tile {
5898 for tv in tile_y_start..tile_y_end {
5904 for tu in tile_x_start..tile_x_end {
5905 let offset_x = tu as f64 * step_ux + tv as f64 * step_vx;
5906 let offset_y = tu as f64 * step_uy + tv as f64 * step_vy;
5907
5908 let tile_ctx = RenderContext {
5909 vp_x: ctx.vp_x - offset_x as f32,
5910 vp_y: ctx.vp_y - offset_y as f32,
5911 scale_x: ctx.scale_x,
5912 scale_y: ctx.scale_y,
5913 out_w: ctx.out_w,
5914 out_h: ctx.out_h,
5915 effective_dpi: ctx.effective_dpi,
5916 icc: ctx.icc,
5917 image_cache: None,
5918 preprocessed: None,
5919 elem_idx: 0,
5920 no_aa: ctx.no_aa,
5921 opm_zero_transparent: params.overprint_mode == 1,
5922 knockout_painter_pass: ctx.knockout_painter_pass,
5923 parent_group_isolated: ctx.parent_group_isolated,
5924 alpha_extraction_pass: ctx.alpha_extraction_pass,
5925 layer_set: ctx.layer_set,
5926 };
5927
5928 let mut tile_band = BandState {
5929 clip_region: None,
5930 spare_mask: None,
5931 clip_mask_cache: HashMap::new(),
5932 clip_mask_seen: HashSet::new(),
5933 mask_pool: Vec::new(),
5934 cmyk_buffer: None,
5935 op_bg_snapshot: None,
5936 op_touched: None,
5937 spot_mask: None,
5938 };
5939
5940 for (idx, elem) in params.tile.elements().iter().enumerate() {
5941 let elem_ctx = RenderContext {
5942 elem_idx: idx,
5943 ..tile_ctx
5944 };
5945 render_element(&mut tile_buf, &mut tile_band, elem, &elem_ctx);
5946 }
5947 }
5948 }
5949 } else if params.tile.elements().iter().any(|e| {
5950 !matches!(
5951 e,
5952 DisplayElement::Fill { .. }
5953 | DisplayElement::Stroke { .. }
5954 | DisplayElement::Image { .. }
5955 | DisplayElement::Clip { .. }
5956 | DisplayElement::InitClip
5957 )
5958 }) {
5959 let bbox = ¶ms.bbox;
5963 let corners_dev = [
5964 pm.transform_point(bbox[0], bbox[1]),
5965 pm.transform_point(bbox[2], bbox[1]),
5966 pm.transform_point(bbox[0], bbox[3]),
5967 pm.transform_point(bbox[2], bbox[3]),
5968 ];
5969 let (mut td_x0, mut td_y0) = (f64::MAX, f64::MAX);
5970 let (mut td_x1, mut td_y1) = (f64::MIN, f64::MIN);
5971 for (x, y) in &corners_dev {
5972 td_x0 = td_x0.min(*x);
5973 td_y0 = td_y0.min(*y);
5974 td_x1 = td_x1.max(*x);
5975 td_y1 = td_y1.max(*y);
5976 }
5977 let tile_pw = ((td_x1 - td_x0) * sx_f).ceil().max(1.0) as u32;
5978 let tile_ph = ((td_y1 - td_y0) * sy_f).ceil().max(1.0) as u32;
5979 let tile_pw = tile_pw.min(8192);
5980 let tile_ph = tile_ph.min(8192);
5981
5982 if let Some(mut one_tile) = Pixmap::new(tile_pw, tile_ph) {
5983 let tile_render_ctx = RenderContext {
5984 vp_x: td_x0 as f32,
5985 vp_y: td_y0 as f32,
5986 scale_x: ctx.scale_x,
5987 scale_y: ctx.scale_y,
5988 out_w: tile_pw,
5989 out_h: tile_ph,
5990 effective_dpi: ctx.effective_dpi,
5991 icc: ctx.icc,
5992 image_cache: None,
5993 preprocessed: None,
5994 elem_idx: 0,
5995 no_aa: ctx.no_aa,
5996 opm_zero_transparent: params.overprint_mode == 1,
5997 knockout_painter_pass: ctx.knockout_painter_pass,
5998 parent_group_isolated: ctx.parent_group_isolated,
5999 alpha_extraction_pass: ctx.alpha_extraction_pass,
6000 layer_set: ctx.layer_set,
6001 };
6002 let mut tile_bs = BandState {
6003 clip_region: None,
6004 spare_mask: None,
6005 clip_mask_cache: HashMap::new(),
6006 clip_mask_seen: HashSet::new(),
6007 mask_pool: Vec::new(),
6008 cmyk_buffer: None,
6009 op_bg_snapshot: None,
6010 op_touched: None,
6011 spot_mask: None,
6012 };
6013 for (idx, elem) in params.tile.elements().iter().enumerate() {
6014 let transformed = transform_element_ctm(elem, pm);
6015 let elem_ctx = RenderContext {
6016 elem_idx: idx,
6017 ..tile_render_ctx
6018 };
6019 render_element(&mut one_tile, &mut tile_bs, &transformed, &elem_ctx);
6020 }
6021 for tv in tile_y_start..tile_y_end {
6023 for tu in tile_x_start..tile_x_end {
6024 let offset_x = tu as f64 * step_ux + tv as f64 * step_vx;
6025 let offset_y = tu as f64 * step_uy + tv as f64 * step_vy;
6026 let px = ((td_x0 + offset_x - dev_vp_x) * sx_f) as i32;
6027 let py = ((td_y0 + offset_y - dev_vp_y) * sy_f) as i32;
6028 let paint = stet_tiny_skia::PixmapPaint {
6029 opacity: 1.0,
6030 blend_mode: BlendMode::SourceOver,
6031 quality: stet_tiny_skia::FilterQuality::Nearest,
6032 };
6033 tile_buf.draw_pixmap(
6034 px,
6035 py,
6036 one_tile.as_ref(),
6037 &paint,
6038 Transform::identity(),
6039 None,
6040 );
6041 }
6042 }
6043 }
6044 } else {
6045 struct PreprocessedImage {
6053 rgba: Vec<u8>,
6054 width: u32,
6055 height: u32,
6056 img_transform: Transform,
6059 }
6060 let tile_elements = params.tile.elements();
6061 let mut preprocessed: Vec<Option<PreprocessedImage>> =
6062 Vec::with_capacity(tile_elements.len());
6063 let tt_sx = (pm.a * sx_f) as f32;
6065 let tt_sy = (pm.d * sy_f) as f32;
6066 let tt_kx = (pm.c * sx_f) as f32;
6067 let tt_ky = (pm.b * sy_f) as f32;
6068 for elem in tile_elements {
6069 if let DisplayElement::Image {
6070 sample_data,
6071 params: ip,
6072 } = elem
6073 {
6074 let iw = ip.width;
6075 let ih = ip.height;
6076 if iw > 0 && ih > 0 {
6077 let mut rgba =
6078 samples_to_rgba(sample_data, ip, ctx.icc, ctx.opm_zero_transparent);
6079 if ip.mask_color.is_some() {
6080 apply_mask_color_rgba(&mut rgba, sample_data, ip);
6081 }
6082 let expected = (iw * ih * 4) as usize;
6083 if rgba.len() >= expected {
6084 if let Some(inv) = ip.image_matrix.invert() {
6085 let combined_mat = ip.ctm.concat(&inv);
6086 let t = to_transform(&combined_mat);
6087 let test = t.post_concat(Transform::from_row(
6090 tt_sx, tt_ky, tt_kx, tt_sy, 0.0, 0.0,
6091 ));
6092 let eff_sx = (test.sx * test.sx + test.ky * test.ky).sqrt();
6093 let eff_sy = (test.kx * test.kx + test.sy * test.sy).sqrt();
6094 if eff_sx < 0.99 || eff_sy < 0.99 {
6095 let tw = (iw as f32 * eff_sx).floor().max(1.0) as u32;
6099 let th = (ih as f32 * eff_sy).floor().max(1.0) as u32;
6100 let scaled = bilinear_prescale(&rgba, iw, ih, tw, th);
6101 let adj = Transform::from_scale(
6104 iw as f32 / tw as f32,
6105 ih as f32 / th as f32,
6106 );
6107 preprocessed.push(Some(PreprocessedImage {
6108 rgba: scaled,
6109 width: tw,
6110 height: th,
6111 img_transform: t.pre_concat(adj),
6112 }));
6113 } else {
6114 preprocessed.push(Some(PreprocessedImage {
6115 rgba,
6116 width: iw,
6117 height: ih,
6118 img_transform: t,
6119 }));
6120 }
6121 } else {
6122 preprocessed.push(None);
6123 }
6124 } else {
6125 preprocessed.push(None);
6126 }
6127 } else {
6128 preprocessed.push(None);
6129 }
6130 }
6133 }
6134
6135 for tv in tile_y_start..tile_y_end {
6136 for tu in tile_x_start..tile_x_end {
6137 let pat_offset_x = tu as f64 * params.xstep;
6138 let pat_offset_y = tv as f64 * params.ystep;
6139
6140 let tile_transform = Transform::from_row(
6141 tt_sx,
6142 tt_ky,
6143 tt_kx,
6144 tt_sy,
6145 ((pm.a * pat_offset_x + pm.c * pat_offset_y + pm.tx - dev_vp_x) * sx_f) as f32,
6146 ((pm.b * pat_offset_x + pm.d * pat_offset_y + pm.ty - dev_vp_y) * sy_f) as f32,
6147 );
6148
6149 let bbox_clip = {
6151 let bb = ¶ms.bbox;
6152 let mut bp = stet_tiny_skia::PathBuilder::new();
6153 bp.move_to(bb[0] as f32, bb[1] as f32);
6154 bp.line_to(bb[2] as f32, bb[1] as f32);
6155 bp.line_to(bb[2] as f32, bb[3] as f32);
6156 bp.line_to(bb[0] as f32, bb[3] as f32);
6157 bp.close();
6158 bp.finish().and_then(|sp| {
6159 let mut m = Mask::new(ctx.out_w, ctx.out_h)?;
6160 m.fill_path(
6161 &sp,
6162 stet_tiny_skia::FillRule::Winding,
6163 false,
6164 tile_transform,
6165 );
6166 Some(m)
6167 })
6168 };
6169 let mut tile_clip: Option<Mask> = bbox_clip;
6170 let mut img_idx = 0usize;
6171 for elem in tile_elements {
6172 let clip_ref = tile_clip.as_ref();
6173 match elem {
6174 DisplayElement::Clip { path, params: cp } => {
6175 if let Some(sp) = build_skia_path(path) {
6176 let t = to_transform(&cp.ctm);
6177 let combined = t.post_concat(tile_transform);
6178 let mut mask = Mask::new(ctx.out_w, ctx.out_h).expect("mask");
6179 mask.fill_path(&sp, to_fill_rule(&cp.fill_rule), false, combined);
6180 if let Some(prev) = tile_clip.take() {
6181 intersect_masks(&mut mask, &prev);
6182 }
6183 tile_clip = Some(mask);
6184 }
6185 }
6186 DisplayElement::InitClip => {
6187 tile_clip = None;
6188 }
6189 DisplayElement::Fill { path, params: fp } => {
6190 if let Some(sp) = build_skia_path(path) {
6191 let mut paint = if params.paint_type == 1 {
6192 to_paint(&fp.color)
6193 } else {
6194 to_paint(
6195 params
6196 .underlying_color
6197 .as_ref()
6198 .unwrap_or(&DeviceColor::black()),
6199 )
6200 };
6201 paint.anti_alias = false;
6202 let t = to_transform(&fp.ctm);
6203 let combined = t.post_concat(tile_transform);
6204 let fr = to_fill_rule(&fp.fill_rule);
6205 tile_buf.fill_path(&sp, &paint, fr, combined, clip_ref);
6206 }
6207 }
6208 DisplayElement::Stroke { path, params: sp } => {
6209 if let Some(skp) = build_skia_path(path) {
6210 let effective_ctm = pm.concat(&sp.ctm);
6214 let mut sp_adj = sp.clone();
6215 sp_adj.ctm = effective_ctm;
6216 let stroke = build_stroke(&sp_adj, ctx.effective_dpi);
6217 let paint = if params.paint_type == 1 {
6218 to_paint(&sp.color)
6219 } else {
6220 to_paint(
6221 params
6222 .underlying_color
6223 .as_ref()
6224 .unwrap_or(&DeviceColor::black()),
6225 )
6226 };
6227 let t = to_transform(&sp.ctm);
6228 let combined = t.post_concat(tile_transform);
6229 tile_buf.stroke_path(&skp, &paint, &stroke, combined, clip_ref);
6230 }
6231 }
6232 DisplayElement::Image { .. } => {
6233 if let Some(ref pi) = preprocessed[img_idx] {
6234 let combined = pi.img_transform.post_concat(tile_transform);
6235 if let Some(img_ref) = stet_tiny_skia::PixmapRef::from_bytes(
6236 &pi.rgba, pi.width, pi.height,
6237 ) {
6238 let paint = stet_tiny_skia::PixmapPaint {
6239 opacity: 1.0,
6240 blend_mode: BlendMode::SourceOver,
6241 quality: stet_tiny_skia::FilterQuality::Nearest,
6242 };
6243 tile_buf.draw_pixmap(0, 0, img_ref, &paint, combined, clip_ref);
6244 }
6245 }
6246 img_idx += 1;
6247 }
6248 _ => {}
6249 }
6250 }
6251 }
6252 }
6253 }
6254
6255 let Some(fill_skia_path) = build_skia_path(¶ms.path) else {
6257 return;
6258 };
6259 let fill_rule = to_fill_rule(¶ms.fill_rule);
6260 let mut fill_mask = Mask::new(ctx.out_w, ctx.out_h).expect("mask");
6261 let path_transform = viewport_transform(
6262 Transform::identity(),
6263 ctx.vp_x,
6264 ctx.vp_y,
6265 ctx.scale_x,
6266 ctx.scale_y,
6267 );
6268 if let Some(ref sp) = params.stroke_params {
6269 let stroke = build_stroke(sp, ctx.effective_dpi);
6273 let ctm_transform = to_transform(&sp.ctm);
6274 let combined = ctm_transform.post_concat(path_transform);
6275 let res_scale = stet_tiny_skia::PathStroker::compute_resolution_scale(&combined);
6276 let dashed;
6277 let stroke_path = if let Some(ref dash) = stroke.dash {
6278 dashed = fill_skia_path.dash(dash, res_scale);
6279 match dashed.as_ref() {
6280 Some(p) => p,
6281 None => &fill_skia_path,
6282 }
6283 } else {
6284 &fill_skia_path
6285 };
6286 if let Some(outline) = stroke_path.stroke(&stroke, res_scale) {
6287 fill_mask.fill_path(
6288 &outline,
6289 stet_tiny_skia::FillRule::Winding,
6290 !ctx.no_aa,
6291 combined,
6292 );
6293 }
6294 } else {
6295 fill_mask.fill_path(&fill_skia_path, fill_rule, !ctx.no_aa, path_transform);
6296 }
6297
6298 if let Some(clip_mask) = mask_ref {
6299 intersect_masks(&mut fill_mask, clip_mask);
6300 }
6301
6302 let img_paint = stet_tiny_skia::PixmapPaint::default();
6303 pixmap.draw_pixmap(
6304 0,
6305 0,
6306 tile_buf.as_ref(),
6307 &img_paint,
6308 Transform::identity(),
6309 Some(&fill_mask),
6310 );
6311}
6312
6313fn clip_path_unified(
6318 band_state: &mut BandState,
6319 path: &PsPath,
6320 params: &ClipParams,
6321 ctx: &RenderContext<'_>,
6322) {
6323 let is_unit_scale = ctx.scale_x == 1.0 && ctx.scale_y == 1.0;
6324
6325 if is_unit_scale {
6327 let y_start = ctx.vp_y as u32;
6328 let x_start = ctx.vp_x as u32;
6329
6330 if x_start == 0
6335 && params.stroke_params.is_none()
6336 && params.ctm.a == 1.0
6337 && params.ctm.d == 1.0
6338 && params.ctm.tx == 0.0
6339 && params.ctm.ty == 0.0
6340 && let Some(bbox) = path_y_bbox(path)
6341 && (bbox.y_max <= y_start as f64 || bbox.y_min >= (y_start + ctx.out_h) as f64)
6342 {
6343 if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
6344 band_state.recycle_mask(mask);
6345 }
6346 band_state.clip_region = Some(ClipRegion::Rect(ClipRect {
6347 x0: 0,
6348 y0: 0,
6349 x1: 0,
6350 y1: 0,
6351 }));
6352 return;
6353 }
6354
6355 let ctm_is_identity = params.ctm.a == 1.0
6359 && params.ctm.b == 0.0
6360 && params.ctm.c == 0.0
6361 && params.ctm.d == 1.0
6362 && params.ctm.tx == 0.0
6363 && params.ctm.ty == 0.0;
6364 if x_start == 0
6365 && ctm_is_identity
6366 && params.stroke_params.is_none()
6367 && let Some(dev_rect) = detect_rect(path, ctx.out_w, u32::MAX)
6368 {
6369 let new_rect = translate_clip_rect(&dev_rect, y_start, ctx.out_h);
6370 match band_state.clip_region.take() {
6371 None => {
6372 band_state.clip_region = Some(ClipRegion::Rect(new_rect));
6373 }
6374 Some(ClipRegion::Rect(existing)) => {
6375 band_state.clip_region = Some(ClipRegion::Rect(existing.intersect(&new_rect)));
6376 }
6377 Some(ClipRegion::Mask(mut mask)) => {
6378 intersect_mask_with_rect(&mut mask, &new_rect, ctx.out_w, ctx.out_h);
6379 band_state.clip_region = Some(ClipRegion::Mask(mask));
6380 }
6381 }
6382 return;
6383 }
6384 }
6385
6386 let fill_rule = to_fill_rule(¶ms.fill_rule);
6388 let path_hash = hash_clip_path(path, ¶ms.fill_rule);
6389 let prev_region = band_state.clip_region.take();
6390
6391 let mut mask = band_state.take_mask(ctx.out_w, ctx.out_h);
6392
6393 let path_mask = if let Some(cached) = band_state.clip_mask_cache.get(&path_hash) {
6394 mask.data_mut().copy_from_slice(cached.data());
6395 mask
6396 } else {
6397 let Some(skia_path) = build_skia_path(path) else {
6398 band_state.recycle_mask(mask);
6399 band_state.clip_region = prev_region;
6400 return;
6401 };
6402 mask.data_mut().fill(0);
6403 if let Some(ref sp) = params.stroke_params {
6404 let stroke = build_stroke(sp, ctx.effective_dpi);
6407 let transform = ctx.transform(&sp.ctm);
6408 let res_scale = stet_tiny_skia::PathStroker::compute_resolution_scale(&transform);
6409 let dashed;
6410 let stroke_path = if let Some(ref dash) = stroke.dash {
6411 dashed = skia_path.dash(dash, res_scale);
6412 match dashed.as_ref() {
6413 Some(p) => p,
6414 None => &skia_path,
6415 }
6416 } else {
6417 &skia_path
6418 };
6419 if let Some(outline) = stroke_path.stroke(&stroke, res_scale) {
6420 mask.fill_path(
6421 &outline,
6422 stet_tiny_skia::FillRule::Winding,
6423 false,
6424 transform,
6425 );
6426 }
6427 } else {
6428 let transform = ctx.transform(¶ms.ctm);
6429 mask.fill_path(&skia_path, fill_rule, false, transform);
6430 }
6431 if !band_state.clip_mask_seen.insert(path_hash) {
6432 band_state.clip_mask_cache.insert(path_hash, mask.clone());
6433 }
6434 mask
6435 };
6436
6437 match prev_region {
6438 None => {
6439 band_state.clip_region = Some(ClipRegion::Mask(path_mask));
6440 }
6441 Some(ClipRegion::Rect(rect)) => {
6442 if rect.is_empty() {
6443 band_state.recycle_mask(path_mask);
6444 band_state.clip_region = Some(ClipRegion::Rect(rect));
6447 } else {
6448 let mut mask = path_mask;
6449 intersect_mask_with_rect(&mut mask, &rect, ctx.out_w, ctx.out_h);
6450 band_state.clip_region = Some(ClipRegion::Mask(mask));
6451 }
6452 }
6453 Some(ClipRegion::Mask(mut existing)) => {
6454 intersect_masks(&mut existing, &path_mask);
6455 band_state.recycle_mask(path_mask);
6456 band_state.clip_region = Some(ClipRegion::Mask(existing));
6457 }
6458 }
6459}
6460impl OutputDevice for SkiaDevice {
6461 fn fill_path(&mut self, path: &PsPath, params: &FillParams) {
6462 self.ensure_full_pixmap();
6463 let Some(skia_path) = build_skia_path(path) else {
6464 return;
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 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, self.no_aa);
6473 let transform = to_transform(¶ms.ctm);
6474 let fill_rule = to_fill_rule(¶ms.fill_rule);
6475
6476 self.pixmap
6477 .fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
6478 }
6479
6480 fn stroke_path(&mut self, path: &PsPath, params: &StrokeParams) {
6481 self.ensure_full_pixmap();
6482 let stroke = build_stroke(params, self.dpi);
6483 let adjusted;
6484 let draw_path =
6485 if params.stroke_adjust && stroke.width <= 2.0 && ctm_is_device_space(¶ms.ctm) {
6486 adjusted =
6487 stroke_adjust_path_viewport(path, stroke.width as f64, 1.0, 1.0, 0.0, 0.0);
6488 &adjusted
6489 } else {
6490 path
6491 };
6492 let Some(skia_path) = build_skia_path(draw_path) else {
6493 return;
6494 };
6495 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, self.no_aa);
6496 let transform = to_transform(¶ms.ctm);
6497
6498 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6499 let mut temp_mask = None;
6500 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6501 return; };
6503
6504 self.pixmap
6505 .stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
6506 }
6507
6508 fn clip_path(&mut self, path: &PsPath, params: &ClipParams) {
6509 self.ensure_full_pixmap();
6510 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6511
6512 if let Some(new_rect) = detect_rect(path, w, h) {
6514 match self.clip_region.take() {
6515 None => {
6516 self.clip_region = Some(ClipRegion::Rect(new_rect));
6517 }
6518 Some(ClipRegion::Rect(existing)) => {
6519 self.clip_region = Some(ClipRegion::Rect(existing.intersect(&new_rect)));
6521 }
6522 Some(ClipRegion::Mask(mut mask)) => {
6523 intersect_mask_with_rect(&mut mask, &new_rect, w, h);
6525 self.clip_region = Some(ClipRegion::Mask(mask));
6526 }
6527 }
6528 return;
6529 }
6530
6531 let fill_rule = to_fill_rule(¶ms.fill_rule);
6533 let path_hash = hash_clip_path(path, ¶ms.fill_rule);
6534 let prev_region = self.clip_region.take();
6535
6536 macro_rules! take_spare {
6538 ($self:expr, $w:expr, $h:expr) => {
6539 $self
6540 .spare_mask
6541 .take()
6542 .unwrap_or_else(|| Mask::new($w, $h).expect("Failed to create mask"))
6543 };
6544 }
6545
6546 let path_mask = if let Some(cached) = self.clip_mask_cache.get(&path_hash) {
6548 let mut mask = take_spare!(self, w, h);
6550 mask.data_mut().copy_from_slice(cached.data());
6551 mask
6552 } else {
6553 let Some(skia_path) = build_skia_path(path) else {
6554 self.clip_region = prev_region;
6555 return;
6556 };
6557 let transform = to_transform(¶ms.ctm);
6558 let mut mask = take_spare!(self, w, h);
6559 mask.data_mut().fill(0); mask.fill_path(&skia_path, fill_rule, false, transform);
6561 if !self.clip_mask_seen.insert(path_hash) {
6563 self.clip_mask_cache.insert(path_hash, mask.clone());
6565 }
6566 mask
6567 };
6568
6569 match prev_region {
6570 None => {
6571 self.clip_region = Some(ClipRegion::Mask(path_mask));
6572 }
6573 Some(ClipRegion::Rect(rect)) => {
6574 if rect.is_empty() {
6575 self.spare_mask = Some(path_mask); } else {
6577 let mut mask = path_mask;
6578 intersect_mask_with_rect(&mut mask, &rect, w, h);
6579 self.clip_region = Some(ClipRegion::Mask(mask));
6580 }
6581 }
6582 Some(ClipRegion::Mask(mut existing)) => {
6583 intersect_masks(&mut existing, &path_mask);
6584 self.spare_mask = Some(path_mask); self.clip_region = Some(ClipRegion::Mask(existing));
6586 }
6587 }
6588 }
6589
6590 fn init_clip(&mut self) {
6591 if let Some(ClipRegion::Mask(mask)) = self.clip_region.take() {
6592 self.spare_mask = Some(mask);
6593 }
6594 self.clip_region = None;
6595 }
6596
6597 fn erase_page(&mut self) {
6598 self.pixmap.fill(Color::WHITE);
6601 if let Some(ClipRegion::Mask(mask)) = self.clip_region.take() {
6602 self.spare_mask = Some(mask);
6603 }
6604 self.clip_region = None;
6605 }
6606
6607 fn show_page(&mut self, output_path: &str) -> Result<(), String> {
6608 let w = self.pixmap.width();
6609 let h = self.pixmap.height();
6610 composite_onto_white(self.pixmap.data_mut());
6612 let mut sink = self.sink_factory.create_sink(output_path)?;
6613 sink.begin_page(w, h)?;
6614 sink.write_rows(self.pixmap.data(), h)?;
6615 sink.end_page()
6616 }
6617
6618 fn draw_image(&mut self, sample_data: &[u8], params: &ImageParams) {
6619 self.ensure_full_pixmap();
6620 let w = params.width;
6621 let h = params.height;
6622 if w == 0 || h == 0 {
6623 return;
6624 }
6625 let mut rgba_data =
6626 samples_to_rgba(sample_data, params, self.render_icc_cache.as_ref(), false);
6627 if params.mask_color.is_some() {
6628 apply_mask_color_rgba(&mut rgba_data, sample_data, params);
6629 }
6630 let expected = (w * h * 4) as usize;
6631 if rgba_data.len() < expected {
6632 return;
6633 }
6634
6635 let Some(image_inv) = params.image_matrix.invert() else {
6636 return;
6637 };
6638 let combined = params.ctm.concat(&image_inv);
6639 let raw_transform = enforce_min_image_size(to_transform(&combined), w, h);
6640
6641 let prescaled = prescale_image(&rgba_data, w, h, raw_transform, params.interpolate);
6642 let (img_data, img_w, img_h, transform) = match &prescaled {
6643 Some((data, pw, ph, t)) => (data.as_slice(), *pw, *ph, *t),
6644 None => (rgba_data.as_slice(), w, h, raw_transform),
6645 };
6646
6647 let Some(img_pixmap) = stet_tiny_skia::PixmapRef::from_bytes(img_data, img_w, img_h) else {
6648 return;
6649 };
6650
6651 let (pw, ph) = (self.pixmap.width(), self.pixmap.height());
6652 let mut temp_mask = None;
6653 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, pw, ph) else {
6654 return;
6655 };
6656
6657 let paint = stet_tiny_skia::PixmapPaint {
6658 quality: image_filter_quality(transform, params.interpolate),
6659 opacity: params.alpha as f32,
6660 blend_mode: u8_to_blend_mode(params.blend_mode),
6661 };
6662 self.pixmap
6663 .draw_pixmap(0, 0, img_pixmap, &paint, transform, mask_ref);
6664 }
6665
6666 fn paint_axial_shading(&mut self, params: &AxialShadingParams) {
6667 self.ensure_full_pixmap();
6668 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6669 let mut temp_mask = None;
6670 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6671 return;
6672 };
6673 render_axial_shading(
6674 &mut self.pixmap,
6675 params,
6676 0.0,
6677 0.0,
6678 1.0,
6679 1.0,
6680 mask_ref,
6681 self.no_aa,
6682 None,
6683 None,
6684 );
6685 }
6686
6687 fn paint_radial_shading(&mut self, params: &RadialShadingParams) {
6688 self.ensure_full_pixmap();
6689 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6690 let mut temp_mask = None;
6691 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6692 return;
6693 };
6694 render_radial_shading(
6695 &mut self.pixmap,
6696 params,
6697 0.0,
6698 0.0,
6699 1.0,
6700 1.0,
6701 mask_ref,
6702 self.no_aa,
6703 None,
6704 None,
6705 );
6706 }
6707
6708 fn paint_mesh_shading(&mut self, params: &MeshShadingParams) {
6709 self.ensure_full_pixmap();
6710 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6711 let mut temp_mask = None;
6712 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6713 return;
6714 };
6715 render_mesh_shading(
6716 &mut self.pixmap,
6717 params,
6718 0.0,
6719 0.0,
6720 1.0,
6721 1.0,
6722 mask_ref,
6723 None,
6724 None,
6725 );
6726 }
6727
6728 fn paint_patch_shading(&mut self, params: &PatchShadingParams) {
6729 self.ensure_full_pixmap();
6730 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6731 let mut temp_mask = None;
6732 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6733 return;
6734 };
6735 render_patch_shading(
6736 &mut self.pixmap,
6737 params,
6738 0.0,
6739 0.0,
6740 1.0,
6741 1.0,
6742 mask_ref,
6743 None,
6744 None,
6745 );
6746 }
6747
6748 fn paint_pattern_fill(&mut self, params: &stet_graphics::device::PatternFillParams) {
6749 self.ensure_full_pixmap();
6750 let w = self.pixmap.width();
6751 let h = self.pixmap.height();
6752 let mut band_state = BandState {
6753 clip_region: self.clip_region.take(),
6754 spare_mask: self.spare_mask.take(),
6755 clip_mask_cache: HashMap::new(),
6756 clip_mask_seen: HashSet::new(),
6757 mask_pool: Vec::new(),
6758 cmyk_buffer: None,
6759 op_bg_snapshot: None,
6760 op_touched: None,
6761 spot_mask: None,
6762 };
6763 {
6764 let ctx = RenderContext {
6765 vp_x: 0.0,
6766 vp_y: 0.0,
6767 scale_x: 1.0,
6768 scale_y: 1.0,
6769 out_w: w,
6770 out_h: h,
6771 effective_dpi: self.dpi,
6772 icc: None,
6773 image_cache: None,
6774 preprocessed: None,
6775 elem_idx: 0,
6776 no_aa: self.no_aa,
6777 opm_zero_transparent: false,
6778 knockout_painter_pass: KnockoutPainterPass::None,
6779 parent_group_isolated: false,
6780 alpha_extraction_pass: false,
6781 layer_set: &self.layer_set,
6782 };
6783 render_pattern_fill(&mut self.pixmap, &mut band_state, params, &ctx);
6784 }
6785 self.clip_region = band_state.clip_region.take();
6786 if let Some(mask) = band_state.spare_mask.take() {
6787 self.spare_mask = Some(mask);
6788 }
6789 }
6790
6791 fn page_size(&self) -> (u32, u32) {
6792 (self.page_w, self.page_h)
6793 }
6794
6795 fn replay_and_show(&mut self, list: DisplayList, output_path: &str) -> Result<(), String> {
6796 self.join_pending()?;
6798
6799 let (page_w, page_h) = self.page_size();
6800
6801 if self.use_viewport_path {
6806 let icc_cache = build_icc_cache_for_list(&list, self.system_cmyk_bytes.as_ref(), false);
6807 let rgba = render_to_rgba_viewport(
6808 &list,
6809 page_w,
6810 page_h,
6811 self.dpi,
6812 Some(&icc_cache),
6813 self.no_aa,
6814 );
6815 let mut sink = self.sink_factory.create_sink(output_path)?;
6816 sink.begin_page(page_w, page_h)?;
6817 sink.write_rows(&rgba, page_h)?;
6818 sink.end_page()?;
6819 return Ok(());
6820 }
6821
6822 let band_h = select_band_height(page_w, page_h);
6823
6824 let icc_cache = build_icc_cache_for_list(&list, self.system_cmyk_bytes.as_ref(), false);
6826
6827 if band_h >= page_h {
6831 self.ensure_full_pixmap();
6832 let ctx = RenderContext {
6833 vp_x: 0.0,
6834 vp_y: 0.0,
6835 scale_x: 1.0,
6836 scale_y: 1.0,
6837 out_w: page_w,
6838 out_h: page_h,
6839 effective_dpi: self.dpi,
6840 icc: Some(&icc_cache),
6841 image_cache: None,
6842 preprocessed: None,
6843 elem_idx: 0,
6844 no_aa: self.no_aa,
6845 opm_zero_transparent: false,
6846 knockout_painter_pass: KnockoutPainterPass::None,
6847 parent_group_isolated: false,
6848 alpha_extraction_pass: false,
6849 layer_set: &self.layer_set,
6850 };
6851 let mut band_state = BandState {
6852 clip_region: None,
6853 spare_mask: None,
6854 clip_mask_cache: HashMap::new(),
6855 clip_mask_seen: HashSet::new(),
6856 mask_pool: Vec::new(),
6857 cmyk_buffer: None,
6858 op_bg_snapshot: None,
6859 op_touched: None,
6860 spot_mask: None,
6861 };
6862 for (idx, elem) in list.elements().iter().enumerate() {
6863 let elem_ctx = RenderContext {
6864 elem_idx: idx,
6865 ..ctx
6866 };
6867 render_element(&mut self.pixmap, &mut band_state, elem, &elem_ctx);
6868 }
6869 return self.show_page(output_path);
6870 }
6871
6872 if self.pixmap.width() > 1 {
6876 self.pixmap = Pixmap::new(1, 1).expect("Failed to create placeholder pixmap");
6877 }
6878
6879 let mut sink = self.sink_factory.create_sink(output_path)?;
6881 let dpi = self.dpi;
6882 let layer_set = self.layer_set.clone();
6883
6884 #[cfg(feature = "parallel")]
6885 {
6886 let no_aa = self.no_aa;
6890 let (tx, rx) = std::sync::mpsc::sync_channel(1);
6891 rayon::spawn(move || {
6892 let result = render_banded_to_sink(
6893 page_w, page_h, band_h, dpi, &list, &mut *sink, &icc_cache, no_aa, &layer_set,
6894 );
6895 let _ = tx.send(result);
6896 });
6897 self.pending_render = Some(rx);
6898 }
6899 #[cfg(not(feature = "parallel"))]
6900 {
6901 render_banded_to_sink(
6902 page_w, page_h, band_h, dpi, &list, &mut *sink, &icc_cache, self.no_aa, &layer_set,
6903 )?;
6904 }
6905
6906 Ok(())
6907 }
6908
6909 fn finish(&mut self) -> Result<(), String> {
6910 self.join_pending()
6911 }
6912}
6913
6914impl Drop for SkiaDevice {
6915 fn drop(&mut self) {
6916 if let Some(rx) = self.pending_render.take() {
6918 let _ = rx.recv();
6919 }
6920 }
6921}
6922
6923impl SkiaDevice {
6924 fn join_pending(&mut self) -> Result<(), String> {
6926 if let Some(rx) = self.pending_render.take() {
6927 match rx.recv() {
6928 Ok(result) => result?,
6929 Err(_) => return Err("Background render task failed".to_string()),
6930 }
6931 }
6932 Ok(())
6933 }
6934}
6935
6936fn has_cmyk_group(list: &DisplayList) -> bool {
6941 use stet_graphics::display_list::GroupColorSpace;
6942 for elem in list.elements() {
6943 match elem {
6944 DisplayElement::Group { elements, params } => {
6945 if params.color_space == GroupColorSpace::DeviceCMYK {
6946 return true;
6947 }
6948 if has_cmyk_group(elements) {
6949 return true;
6950 }
6951 }
6952 DisplayElement::SoftMasked { content, mask, .. } => {
6953 if has_cmyk_group(content) || has_cmyk_group(mask) {
6954 return true;
6955 }
6956 }
6957 DisplayElement::OcgGroup { elements, .. } => {
6958 if has_cmyk_group(elements) {
6959 return true;
6960 }
6961 }
6962 _ => {}
6963 }
6964 }
6965 false
6966}
6967
6968fn group_only_native_cmyk_fills(elements: &DisplayList) -> bool {
6975 let mut found_paint = false;
6976 for elem in elements.elements() {
6977 match elem {
6978 DisplayElement::InitClip => continue,
6979 DisplayElement::Clip { .. } => continue,
6980 DisplayElement::Fill { params, .. } => {
6981 if params.color.native_cmyk.is_none() {
6982 return false;
6983 }
6984 found_paint = true;
6985 }
6986 DisplayElement::Stroke { params, .. } => {
6987 if params.color.native_cmyk.is_none() {
6993 return false;
6994 }
6995 found_paint = true;
6996 }
6997 _ => return false,
6998 }
6999 }
7000 found_paint
7001}
7002
7003fn group_content_is_native_cmyk(elements: &DisplayList) -> bool {
7024 let mut found_paint = false;
7025 for elem in elements.elements() {
7026 match elem {
7027 DisplayElement::InitClip => continue,
7028 DisplayElement::Clip { .. } => continue,
7029 DisplayElement::Text { .. } => continue,
7030 DisplayElement::ErasePage => continue,
7031 DisplayElement::Fill { params, .. } => {
7032 if params.color.native_cmyk.is_none() {
7033 return false;
7034 }
7035 found_paint = true;
7036 }
7037 DisplayElement::Stroke { params, .. } => {
7038 if params.color.native_cmyk.is_none() {
7039 return false;
7040 }
7041 found_paint = true;
7042 }
7043 DisplayElement::Image { params, .. } => {
7044 if !is_cmyk_color_space(¶ms.color_space) {
7045 return false;
7046 }
7047 found_paint = true;
7048 }
7049 DisplayElement::AxialShading { .. }
7050 | DisplayElement::RadialShading { .. }
7051 | DisplayElement::MeshShading { .. }
7052 | DisplayElement::PatchShading { .. } => {
7053 return false;
7056 }
7057 DisplayElement::PatternFill { .. } => {
7058 return false;
7062 }
7063 DisplayElement::Group { elements: sub, .. } => {
7064 if !group_content_is_native_cmyk(sub) {
7065 return false;
7066 }
7067 found_paint = true;
7068 }
7069 DisplayElement::SoftMasked { .. } => {
7070 return false;
7081 }
7082 DisplayElement::OcgGroup { elements: sub, .. } => {
7083 if !group_content_is_native_cmyk(sub) {
7084 return false;
7085 }
7086 found_paint = true;
7087 }
7088 _ => return false,
7089 }
7090 }
7091 found_paint
7092}
7093
7094fn content_list_is_simple_native_cmyk(list: &DisplayList) -> bool {
7101 let mut found_paint = false;
7102 for elem in list.elements() {
7103 match elem {
7104 DisplayElement::InitClip
7105 | DisplayElement::Clip { .. }
7106 | DisplayElement::Text { .. }
7107 | DisplayElement::ErasePage => continue,
7108 DisplayElement::Fill { params, .. } => {
7109 if params.color.native_cmyk.is_none() {
7110 return false;
7111 }
7112 if params.blend_mode != 0 || params.alpha != 1.0 {
7113 return false;
7114 }
7115 found_paint = true;
7116 }
7117 DisplayElement::Stroke { params, .. } => {
7118 if params.color.native_cmyk.is_none() {
7119 return false;
7120 }
7121 if params.blend_mode != 0 || params.alpha != 1.0 {
7122 return false;
7123 }
7124 found_paint = true;
7125 }
7126 DisplayElement::Group { params, elements } => {
7134 if params.blend_mode != 0 || params.alpha != 1.0 {
7135 return false;
7136 }
7137 if !content_list_is_simple_native_cmyk(elements) {
7138 return false;
7139 }
7140 if elements.elements().iter().any(|e| {
7144 matches!(
7145 e,
7146 DisplayElement::Fill { .. } | DisplayElement::Stroke { .. }
7147 )
7148 }) {
7149 found_paint = true;
7150 }
7151 }
7152 _ => return false,
7153 }
7154 }
7155 found_paint
7156}
7157
7158fn has_overprint_elements(list: &DisplayList) -> bool {
7160 for elem in list.elements() {
7161 match elem {
7162 DisplayElement::Fill { params, .. } => {
7163 if params.overprint {
7164 return true;
7165 }
7166 }
7167 DisplayElement::Stroke { params, .. } => {
7168 if params.overprint {
7169 return true;
7170 }
7171 }
7172 DisplayElement::Image { params, .. } => {
7173 if params.overprint {
7174 return true;
7175 }
7176 }
7177 DisplayElement::AxialShading { params } => {
7178 if params.overprint {
7179 return true;
7180 }
7181 }
7182 DisplayElement::RadialShading { params } => {
7183 if params.overprint {
7184 return true;
7185 }
7186 }
7187 DisplayElement::MeshShading { params } => {
7188 if params.overprint {
7189 return true;
7190 }
7191 }
7192 DisplayElement::PatchShading { params } => {
7193 if params.overprint {
7194 return true;
7195 }
7196 }
7197 DisplayElement::Group { elements, .. } => {
7198 if has_overprint_elements(elements) {
7199 return true;
7200 }
7201 }
7202 DisplayElement::SoftMasked { content, mask, .. } => {
7203 if has_overprint_elements(content) || has_overprint_elements(mask) {
7204 return true;
7205 }
7206 }
7207 DisplayElement::OcgGroup { elements, .. } => {
7208 if has_overprint_elements(elements) {
7209 return true;
7210 }
7211 }
7212 _ => {}
7213 }
7214 }
7215 false
7216}
7217
7218#[allow(clippy::too_many_arguments)]
7221fn render_overprint_fill(
7222 pixmap: &mut Pixmap,
7223 cmyk_buf: &mut [f32],
7224 op_bg: &mut [u8],
7225 op_touched: &mut [u8],
7226 spot_mask: &[u8],
7227 band_state: &mut BandState,
7228 path: &PsPath,
7229 params: &FillParams,
7230 vp_x: f32,
7231 vp_y: f32,
7232 scale_x: f32,
7233 scale_y: f32,
7234 out_w: u32,
7235 out_h: u32,
7236 icc: Option<&IccCache>,
7237 no_aa: bool,
7238) {
7239 let Some(skia_path) = build_skia_path(path) else {
7240 return;
7241 };
7242 let fill_rule = to_fill_rule(¶ms.fill_rule);
7243
7244 let mut coverage_mask = match Mask::new(out_w, out_h) {
7245 Some(m) => m,
7246 None => return,
7247 };
7248 let transform = viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
7249 coverage_mask.fill_path(&skia_path, fill_rule, !no_aa, transform);
7250
7251 let (bbox_x0, bbox_y0, bbox_x1, bbox_y1) =
7253 path_device_bbox(&skia_path, transform, out_w, out_h);
7254
7255 let clip_coverage: Option<&[u8]> = match &band_state.clip_region {
7257 None => None,
7258 Some(ClipRegion::Rect(r)) => {
7259 let data = coverage_mask.data_mut();
7261 let stride = out_w as usize;
7262 for y in bbox_y0..bbox_y1 {
7263 let row_start = y * stride;
7264 for x in bbox_x0..bbox_x1 {
7265 let yu = y as u32;
7266 let xu = x as u32;
7267 if yu < r.y0 || yu >= r.y1 || xu < r.x0 || xu >= r.x1 {
7268 data[row_start + x] = 0;
7269 }
7270 }
7271 }
7272 None
7273 }
7274 Some(ClipRegion::Mask(clip_mask)) => Some(clip_mask.data()),
7275 };
7276
7277 let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
7283
7284 let (src_c, src_m, src_y, src_k) = if !is_custom_spot && let Some(c) = params.color.process_cmyk
7300 {
7301 c
7302 } else if let Some(c) = params.color.native_cmyk {
7303 c
7304 } else {
7305 let r = params.color.r;
7306 let g = params.color.g;
7307 let b = params.color.b;
7308 (1.0 - r, 1.0 - g, 1.0 - b, 0.0)
7309 };
7310
7311 let mut channels = params.painted_channels;
7312 if channels == 0 {
7315 channels = stet_graphics::device::CMYK_ALL;
7316 }
7317 if params.overprint_mode == 1
7319 && channels == stet_graphics::device::CMYK_ALL
7320 && params.is_device_cmyk
7321 {
7322 channels = 0;
7323 if src_c != 0.0 {
7324 channels |= stet_graphics::device::CMYK_C;
7325 }
7326 if src_m != 0.0 {
7327 channels |= stet_graphics::device::CMYK_M;
7328 }
7329 if src_y != 0.0 {
7330 channels |= stet_graphics::device::CMYK_Y;
7331 }
7332 if src_k != 0.0 {
7333 channels |= stet_graphics::device::CMYK_K;
7334 }
7335 if channels == 0 && !params.opm_paired {
7346 channels = stet_graphics::device::CMYK_ALL;
7347 }
7348 }
7349
7350 let is_k_only_cmyk = params.is_device_cmyk
7358 && params.overprint_mode == 0
7359 && src_c == 0.0
7360 && src_m == 0.0
7361 && src_y == 0.0;
7362 if channels == stet_graphics::device::CMYK_ALL && !is_custom_spot && !is_k_only_cmyk {
7363 let cov_data = coverage_mask.data();
7364 let stride = out_w as usize;
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 let ci = mi * 4;
7374 cmyk_buf[ci] = src_c as f32;
7375 cmyk_buf[ci + 1] = src_m as f32;
7376 cmyk_buf[ci + 2] = src_y as f32;
7377 cmyk_buf[ci + 3] = src_k as f32;
7378 }
7379 }
7380 }
7381 let mut temp_mask = None;
7382 let Some(mask_ref) =
7383 resolve_clip_mask(&band_state.clip_region, &mut temp_mask, out_w, out_h)
7384 else {
7385 return;
7386 };
7387 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, no_aa);
7388 pixmap.fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
7389 return;
7390 }
7391
7392 let cov_data = coverage_mask.data();
7393 let stride = out_w as usize;
7394 let px_data = pixmap.data_mut();
7395 let px_stride = out_w as usize * 4;
7396
7397 for y in bbox_y0..bbox_y1 {
7398 for x in bbox_x0..bbox_x1 {
7399 let mi = y * stride + x;
7400 let mut cov = cov_data[mi] as f32 / 255.0;
7401 if let Some(clip) = clip_coverage {
7402 cov *= clip[mi] as f32 / 255.0;
7403 }
7404 if cov <= 0.0 {
7405 continue;
7406 }
7407
7408 let ci = mi * 4;
7409 let pi = y * px_stride + x * 4;
7410 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
7417 op_bg[pi] = px_data[pi];
7418 op_bg[pi + 1] = px_data[pi + 1];
7419 op_bg[pi + 2] = px_data[pi + 2];
7420 op_bg[pi + 3] = px_data[pi + 3];
7421 op_touched[mi] = 1;
7422 }
7423 let cur_c = cmyk_buf[ci] as f64;
7424 let cur_m = cmyk_buf[ci + 1] as f64;
7425 let cur_y = cmyk_buf[ci + 2] as f64;
7426 let cur_k = cmyk_buf[ci + 3] as f64;
7427 let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
7439 let pixmap_has_colour = px_data[pi + 3] > 0
7440 && (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
7441 let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
7449
7450 let is_promoted_gray = params.painted_channels == stet_graphics::device::CMYK_K
7466 && channels == stet_graphics::device::CMYK_K
7467 && params.is_device_cmyk
7468 && src_c == 0.0
7469 && src_m == 0.0
7470 && src_y == 0.0;
7471 let effective_channels = if is_promoted_gray && spot_mask[mi] == 0 {
7472 stet_graphics::device::CMYK_ALL
7473 } else {
7474 channels
7475 };
7476
7477 let new_c = if effective_channels & stet_graphics::device::CMYK_C != 0 {
7478 src_c
7479 } else {
7480 cur_c
7481 };
7482 let new_m = if effective_channels & stet_graphics::device::CMYK_M != 0 {
7483 src_m
7484 } else {
7485 cur_m
7486 };
7487 let new_y = if effective_channels & stet_graphics::device::CMYK_Y != 0 {
7488 src_y
7489 } else {
7490 cur_y
7491 };
7492 let new_k = if effective_channels & stet_graphics::device::CMYK_K != 0 {
7493 src_k
7494 } else {
7495 cur_k
7496 };
7497
7498 if !is_custom_spot {
7502 cmyk_buf[ci] = new_c as f32;
7503 cmyk_buf[ci + 1] = new_m as f32;
7504 cmyk_buf[ci + 2] = new_y as f32;
7505 cmyk_buf[ci + 3] = new_k as f32;
7506 }
7507
7508 let delta = (new_c - cur_c)
7522 .abs()
7523 .max((new_m - cur_m).abs())
7524 .max((new_y - cur_y).abs())
7525 .max((new_k - cur_k).abs());
7526 if delta < 1e-4 && spot_mask[mi] != 0 && pixmap_has_colour && !is_custom_spot {
7527 continue;
7528 }
7529
7530 let (r, g, b) =
7531 if is_promoted_gray && effective_channels == stet_graphics::device::CMYK_ALL {
7532 (params.color.r, params.color.g, params.color.b)
7547 } else if use_multiplicative {
7548 let bg_r = px_data[pi] as f64 / 255.0;
7554 let bg_g = px_data[pi + 1] as f64 / 255.0;
7555 let bg_b = px_data[pi + 2] as f64 / 255.0;
7556 let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
7557 1.0 - src_c
7558 } else {
7559 1.0
7560 };
7561 let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
7562 1.0 - src_m
7563 } else {
7564 1.0
7565 };
7566 let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
7567 1.0 - src_y
7568 } else {
7569 1.0
7570 };
7571 let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
7572 1.0 - src_k
7573 } else {
7574 1.0
7575 };
7576 (
7577 (bg_r * over_r * k_fac).clamp(0.0, 1.0),
7578 (bg_g * over_g * k_fac).clamp(0.0, 1.0),
7579 (bg_b * over_b * k_fac).clamp(0.0, 1.0),
7580 )
7581 } else if let Some(icc_cache) = icc {
7582 icc_cache
7583 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
7584 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
7585 } else {
7586 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
7587 };
7588
7589 let a = (cov * params.alpha as f32).min(1.0);
7590 let (bk_r, bk_g, bk_b, bk_a) = if op_touched[mi] != 0 {
7603 let new_r = (r as f32 * 255.0).clamp(0.0, 255.0);
7604 let new_g = (g as f32 * 255.0).clamp(0.0, 255.0);
7605 let new_b = (b as f32 * 255.0).clamp(0.0, 255.0);
7606 let dr = (op_bg[pi] as f32 - new_r).abs();
7607 let dg = (op_bg[pi + 1] as f32 - new_g).abs();
7608 let db = (op_bg[pi + 2] as f32 - new_b).abs();
7609 if dr.max(dg).max(db) <= 4.0 {
7610 (op_bg[pi], op_bg[pi + 1], op_bg[pi + 2], op_bg[pi + 3])
7611 } else {
7612 (
7613 px_data[pi],
7614 px_data[pi + 1],
7615 px_data[pi + 2],
7616 px_data[pi + 3],
7617 )
7618 }
7619 } else {
7620 (
7621 px_data[pi],
7622 px_data[pi + 1],
7623 px_data[pi + 2],
7624 px_data[pi + 3],
7625 )
7626 };
7627 let dst_a = bk_a as f32 / 255.0;
7628 let one_minus_a = 1.0 - a;
7629 let out_a = a + dst_a * one_minus_a;
7630 if out_a > 0.0 {
7631 px_data[pi] = ((r as f32 * a + (bk_r as f32 / 255.0) * one_minus_a) * 255.0)
7636 .clamp(0.0, 255.0)
7637 .round() as u8;
7638 px_data[pi + 1] = ((g as f32 * a + (bk_g as f32 / 255.0) * one_minus_a) * 255.0)
7639 .clamp(0.0, 255.0)
7640 .round() as u8;
7641 px_data[pi + 2] = ((b as f32 * a + (bk_b as f32 / 255.0) * one_minus_a) * 255.0)
7642 .clamp(0.0, 255.0)
7643 .round() as u8;
7644 px_data[pi + 3] = (out_a * 255.0).round() as u8;
7645 }
7646 }
7647 }
7648}
7649fn cmyk_to_rgb_plrm(c: f64, m: f64, y: f64, k: f64) -> (f64, f64, f64) {
7651 (
7652 1.0 - (c + k).min(1.0),
7653 1.0 - (m + k).min(1.0),
7654 1.0 - (y + k).min(1.0),
7655 )
7656}
7657
7658#[allow(clippy::too_many_arguments)]
7660fn path_device_bbox(
7663 skia_path: &stet_tiny_skia::Path,
7664 transform: Transform,
7665 w: u32,
7666 h: u32,
7667) -> (usize, usize, usize, usize) {
7668 let b = skia_path.bounds();
7669 let mut corners = [
7670 stet_tiny_skia::Point {
7671 x: b.left(),
7672 y: b.top(),
7673 },
7674 stet_tiny_skia::Point {
7675 x: b.right(),
7676 y: b.top(),
7677 },
7678 stet_tiny_skia::Point {
7679 x: b.right(),
7680 y: b.bottom(),
7681 },
7682 stet_tiny_skia::Point {
7683 x: b.left(),
7684 y: b.bottom(),
7685 },
7686 ];
7687 transform.map_points(&mut corners);
7688 let min_x = corners.iter().map(|p| p.x).fold(f32::INFINITY, f32::min);
7689 let min_y = corners.iter().map(|p| p.y).fold(f32::INFINITY, f32::min);
7690 let max_x = corners
7691 .iter()
7692 .map(|p| p.x)
7693 .fold(f32::NEG_INFINITY, f32::max);
7694 let max_y = corners
7695 .iter()
7696 .map(|p| p.y)
7697 .fold(f32::NEG_INFINITY, f32::max);
7698 let x0 = (min_x.floor() as i32 - 1).max(0) as usize;
7700 let y0 = (min_y.floor() as i32 - 1).max(0) as usize;
7701 let x1 = (max_x.ceil() as i32 + 1).clamp(0, w as i32) as usize;
7702 let y1 = (max_y.ceil() as i32 + 1).clamp(0, h as i32) as usize;
7703 (x0, y0, x1, y1)
7704}
7705
7706fn update_cmyk_buffer_for_fill(
7707 cmyk_buf: &mut [f32],
7708 spot_mask: &mut [u8],
7709 path: &PsPath,
7710 params: &FillParams,
7711 vp_x: f32,
7712 vp_y: f32,
7713 scale_x: f32,
7714 scale_y: f32,
7715 out_w: u32,
7716 out_h: u32,
7717 clip_region: &Option<ClipRegion>,
7718 no_aa: bool,
7719 icc: Option<&IccCache>,
7720) {
7721 let is_custom_spot = params.painted_channels == 0
7741 && !params.is_device_cmyk
7742 && params.color.process_cmyk.is_some();
7743
7744 let has_spot_contrib = (is_custom_spot && params.color.native_cmyk.is_some())
7758 || matches!(
7759 (params.color.native_cmyk, params.color.process_cmyk),
7760 (Some(nat), Some(proc_))
7761 if (nat.0 - proc_.0).abs() > 1e-6
7762 || (nat.1 - proc_.1).abs() > 1e-6
7763 || (nat.2 - proc_.2).abs() > 1e-6
7764 || (nat.3 - proc_.3).abs() > 1e-6
7765 );
7766
7767 let (src_c, src_m, src_y, src_k) = if is_custom_spot {
7775 (0.0, 0.0, 0.0, 0.0)
7776 } else if let Some(c) = params.color.process_cmyk {
7777 c
7778 } else if let Some(c) = params.color.native_cmyk {
7779 c
7780 } else if let Some(cmyk) = icc.and_then(|i| {
7781 i.convert_rgb_to_cmyk_readonly(params.color.r, params.color.g, params.color.b)
7782 }) {
7783 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
7784 } else {
7785 (
7786 (1.0 - params.color.r).clamp(0.0, 1.0),
7787 (1.0 - params.color.g).clamp(0.0, 1.0),
7788 (1.0 - params.color.b).clamp(0.0, 1.0),
7789 0.0,
7790 )
7791 };
7792 let Some(skia_path) = build_skia_path(path) else {
7793 return;
7794 };
7795
7796 let mut coverage_mask = match Mask::new(out_w, out_h) {
7797 Some(m) => m,
7798 None => return,
7799 };
7800 let transform = viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
7801 let fill_rule = to_fill_rule(¶ms.fill_rule);
7802 coverage_mask.fill_path(&skia_path, fill_rule, !no_aa, transform);
7803
7804 let cov_data = coverage_mask.data();
7805 let clip_data: Option<&[u8]> = match clip_region {
7806 Some(ClipRegion::Mask(m)) => Some(m.data()),
7807 _ => None,
7808 };
7809
7810 let (mut bx0, mut by0, mut bx1, mut by1) =
7812 path_device_bbox(&skia_path, transform, out_w, out_h);
7813 if let Some(ClipRegion::Rect(r)) = clip_region {
7814 bx0 = bx0.max(r.x0 as usize);
7815 by0 = by0.max(r.y0 as usize);
7816 bx1 = bx1.min(r.x1 as usize);
7817 by1 = by1.min(r.y1 as usize);
7818 }
7819
7820 let stride = out_w as usize;
7821 for y in by0..by1 {
7822 for x in bx0..bx1 {
7823 let mi = y * stride + x;
7824 let mut cov = cov_data[mi] as f32 / 255.0;
7825 if let Some(clip) = clip_data {
7826 cov *= clip[mi] as f32 / 255.0;
7827 }
7828 if cov > 0.0 {
7829 let ci = mi * 4;
7830 cmyk_buf[ci] = src_c as f32;
7831 cmyk_buf[ci + 1] = src_m as f32;
7832 cmyk_buf[ci + 2] = src_y as f32;
7833 cmyk_buf[ci + 3] = src_k as f32;
7834 if has_spot_contrib {
7835 spot_mask[mi] = 1;
7836 }
7837 }
7838 }
7839 }
7840}
7841
7842#[allow(clippy::too_many_arguments)]
7847fn render_overprint_stroke(
7848 pixmap: &mut Pixmap,
7849 cmyk_buf: &mut [f32],
7850 op_bg: &mut [u8],
7851 op_touched: &mut [u8],
7852 spot_mask: &[u8],
7853 band_state: &mut BandState,
7854 skia_path: &stet_tiny_skia::Path,
7855 stroke: &Stroke,
7856 transform: Transform,
7857 params: &StrokeParams,
7858 out_w: u32,
7859 out_h: u32,
7860 icc: Option<&IccCache>,
7861 no_aa: bool,
7862) {
7863 let resolution_scale = (transform.sx * transform.sx + transform.sy * transform.sy)
7865 .sqrt()
7866 .max(1.0);
7867 let dashed_op;
7868 let stroke_src = if let Some(ref dash) = stroke.dash {
7869 dashed_op = skia_path.dash(dash, resolution_scale);
7870 match dashed_op.as_ref() {
7871 Some(p) => p,
7872 None => skia_path,
7873 }
7874 } else {
7875 skia_path
7876 };
7877 let Some(stroked_user) = stroke_src.stroke(stroke, resolution_scale) else {
7878 return;
7879 };
7880 let Some(stroked) = stroked_user.transform(transform) else {
7881 return;
7882 };
7883
7884 let mut coverage_mask = match Mask::new(out_w, out_h) {
7885 Some(m) => m,
7886 None => return,
7887 };
7888 coverage_mask.fill_path(
7889 &stroked,
7890 SkiaFillRule::Winding,
7891 !no_aa,
7892 Transform::identity(),
7893 );
7894
7895 let (bbox_x0, bbox_y0, bbox_x1, bbox_y1) =
7896 path_device_bbox(&stroked, Transform::identity(), out_w, out_h);
7897
7898 let clip_coverage: Option<&[u8]> = match &band_state.clip_region {
7900 None => None,
7901 Some(ClipRegion::Rect(r)) => {
7902 let data = coverage_mask.data_mut();
7903 let stride = out_w as usize;
7904 for y in bbox_y0..bbox_y1 {
7905 let row_start = y * stride;
7906 for x in bbox_x0..bbox_x1 {
7907 let yu = y as u32;
7908 let xu = x as u32;
7909 if yu < r.y0 || yu >= r.y1 || xu < r.x0 || xu >= r.x1 {
7910 data[row_start + x] = 0;
7911 }
7912 }
7913 }
7914 None
7915 }
7916 Some(ClipRegion::Mask(clip_mask)) => Some(clip_mask.data()),
7917 };
7918
7919 let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
7923
7924 let (src_c, src_m, src_y, src_k) = if !is_custom_spot && let Some(c) = params.color.process_cmyk
7931 {
7932 c
7933 } else if let Some(c) = params.color.native_cmyk {
7934 c
7935 } else {
7936 let r = params.color.r;
7937 let g = params.color.g;
7938 let b = params.color.b;
7939 (1.0 - r, 1.0 - g, 1.0 - b, 0.0)
7940 };
7941
7942 let mut channels = params.painted_channels;
7943 if channels == 0 {
7944 channels = stet_graphics::device::CMYK_ALL;
7945 }
7946 if params.overprint_mode == 1
7947 && channels == stet_graphics::device::CMYK_ALL
7948 && params.is_device_cmyk
7949 {
7950 channels = 0;
7951 if src_c != 0.0 {
7952 channels |= stet_graphics::device::CMYK_C;
7953 }
7954 if src_m != 0.0 {
7955 channels |= stet_graphics::device::CMYK_M;
7956 }
7957 if src_y != 0.0 {
7958 channels |= stet_graphics::device::CMYK_Y;
7959 }
7960 if src_k != 0.0 {
7961 channels |= stet_graphics::device::CMYK_K;
7962 }
7963 if channels == 0 && !params.opm_paired {
7968 channels = stet_graphics::device::CMYK_ALL;
7969 }
7970 }
7971
7972 let is_k_only_cmyk = params.is_device_cmyk
7973 && params.overprint_mode == 0
7974 && src_c == 0.0
7975 && src_m == 0.0
7976 && src_y == 0.0;
7977 if channels == stet_graphics::device::CMYK_ALL && !is_custom_spot && !is_k_only_cmyk {
7978 let cov_data = coverage_mask.data();
7983 let stride = out_w as usize;
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 let ci = mi * 4;
7993 cmyk_buf[ci] = src_c as f32;
7994 cmyk_buf[ci + 1] = src_m as f32;
7995 cmyk_buf[ci + 2] = src_y as f32;
7996 cmyk_buf[ci + 3] = src_k as f32;
7997 }
7998 }
7999 }
8000 let mut temp_mask = None;
8001 let Some(mask_ref) =
8002 resolve_clip_mask(&band_state.clip_region, &mut temp_mask, out_w, out_h)
8003 else {
8004 return;
8005 };
8006 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, no_aa);
8007 pixmap.stroke_path(skia_path, &paint, stroke, transform, mask_ref);
8008 return;
8009 }
8010
8011 let cov_data = coverage_mask.data();
8012 let stride = out_w as usize;
8013 let px_data = pixmap.data_mut();
8014 let px_stride = out_w as usize * 4;
8015
8016 for y in bbox_y0..bbox_y1 {
8017 for x in bbox_x0..bbox_x1 {
8018 let mi = y * stride + x;
8019 let mut cov = cov_data[mi] as f32 / 255.0;
8020 if let Some(clip) = clip_coverage {
8021 cov *= clip[mi] as f32 / 255.0;
8022 }
8023 if cov <= 0.0 {
8024 continue;
8025 }
8026
8027 let ci = mi * 4;
8028 let pi = y * px_stride + x * 4;
8029 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
8034 op_bg[pi] = px_data[pi];
8035 op_bg[pi + 1] = px_data[pi + 1];
8036 op_bg[pi + 2] = px_data[pi + 2];
8037 op_bg[pi + 3] = px_data[pi + 3];
8038 op_touched[mi] = 1;
8039 }
8040 let cur_c = cmyk_buf[ci] as f64;
8041 let cur_m = cmyk_buf[ci + 1] as f64;
8042 let cur_y = cmyk_buf[ci + 2] as f64;
8043 let cur_k = cmyk_buf[ci + 3] as f64;
8044 let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
8045 let pixmap_has_colour = px_data[pi + 3] > 0
8046 && (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
8047 let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
8055
8056 let is_promoted_gray = params.painted_channels == stet_graphics::device::CMYK_K
8059 && channels == stet_graphics::device::CMYK_K
8060 && params.is_device_cmyk
8061 && src_c == 0.0
8062 && src_m == 0.0
8063 && src_y == 0.0;
8064 let effective_channels = if is_promoted_gray && spot_mask[mi] == 0 {
8065 stet_graphics::device::CMYK_ALL
8066 } else {
8067 channels
8068 };
8069
8070 let new_c = if effective_channels & stet_graphics::device::CMYK_C != 0 {
8071 src_c
8072 } else {
8073 cur_c
8074 };
8075 let new_m = if effective_channels & stet_graphics::device::CMYK_M != 0 {
8076 src_m
8077 } else {
8078 cur_m
8079 };
8080 let new_y = if effective_channels & stet_graphics::device::CMYK_Y != 0 {
8081 src_y
8082 } else {
8083 cur_y
8084 };
8085 let new_k = if effective_channels & stet_graphics::device::CMYK_K != 0 {
8086 src_k
8087 } else {
8088 cur_k
8089 };
8090
8091 if !is_custom_spot {
8092 cmyk_buf[ci] = new_c as f32;
8093 cmyk_buf[ci + 1] = new_m as f32;
8094 cmyk_buf[ci + 2] = new_y as f32;
8095 cmyk_buf[ci + 3] = new_k as f32;
8096 }
8097
8098 let delta = (new_c - cur_c)
8100 .abs()
8101 .max((new_m - cur_m).abs())
8102 .max((new_y - cur_y).abs())
8103 .max((new_k - cur_k).abs());
8104 if delta < 1e-4 && spot_mask[mi] != 0 && pixmap_has_colour && !is_custom_spot {
8105 continue;
8106 }
8107
8108 let (r, g, b) =
8109 if is_promoted_gray && effective_channels == stet_graphics::device::CMYK_ALL {
8110 (params.color.r, params.color.g, params.color.b)
8116 } else if use_multiplicative {
8117 let bg_r = px_data[pi] as f64 / 255.0;
8118 let bg_g = px_data[pi + 1] as f64 / 255.0;
8119 let bg_b = px_data[pi + 2] as f64 / 255.0;
8120 let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
8121 1.0 - src_c
8122 } else {
8123 1.0
8124 };
8125 let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
8126 1.0 - src_m
8127 } else {
8128 1.0
8129 };
8130 let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
8131 1.0 - src_y
8132 } else {
8133 1.0
8134 };
8135 let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
8136 1.0 - src_k
8137 } else {
8138 1.0
8139 };
8140 (
8141 (bg_r * over_r * k_fac).clamp(0.0, 1.0),
8142 (bg_g * over_g * k_fac).clamp(0.0, 1.0),
8143 (bg_b * over_b * k_fac).clamp(0.0, 1.0),
8144 )
8145 } else if let Some(icc_cache) = icc {
8146 icc_cache
8147 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
8148 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
8149 } else {
8150 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
8151 };
8152
8153 let a = (cov * params.alpha as f32).min(1.0);
8154 let (bk_r, bk_g, bk_b, bk_a) = if op_touched[mi] != 0 {
8159 let new_r = (r as f32 * 255.0).clamp(0.0, 255.0);
8160 let new_g = (g as f32 * 255.0).clamp(0.0, 255.0);
8161 let new_b = (b as f32 * 255.0).clamp(0.0, 255.0);
8162 let dr = (op_bg[pi] as f32 - new_r).abs();
8163 let dg = (op_bg[pi + 1] as f32 - new_g).abs();
8164 let db = (op_bg[pi + 2] as f32 - new_b).abs();
8165 if dr.max(dg).max(db) <= 4.0 {
8166 (op_bg[pi], op_bg[pi + 1], op_bg[pi + 2], op_bg[pi + 3])
8167 } else {
8168 (
8169 px_data[pi],
8170 px_data[pi + 1],
8171 px_data[pi + 2],
8172 px_data[pi + 3],
8173 )
8174 }
8175 } else {
8176 (
8177 px_data[pi],
8178 px_data[pi + 1],
8179 px_data[pi + 2],
8180 px_data[pi + 3],
8181 )
8182 };
8183 let dst_a = bk_a as f32 / 255.0;
8184 let one_minus_a = 1.0 - a;
8185 let out_a = a + dst_a * one_minus_a;
8186 if out_a > 0.0 {
8187 px_data[pi] = ((r as f32 * a + (bk_r as f32 / 255.0) * one_minus_a) * 255.0)
8189 .clamp(0.0, 255.0)
8190 .round() as u8;
8191 px_data[pi + 1] = ((g as f32 * a + (bk_g as f32 / 255.0) * one_minus_a) * 255.0)
8192 .clamp(0.0, 255.0)
8193 .round() as u8;
8194 px_data[pi + 2] = ((b as f32 * a + (bk_b as f32 / 255.0) * one_minus_a) * 255.0)
8195 .clamp(0.0, 255.0)
8196 .round() as u8;
8197 px_data[pi + 3] = (out_a * 255.0).round() as u8;
8198 }
8199 }
8200 }
8201}
8202
8203#[allow(clippy::too_many_arguments)]
8208fn update_cmyk_buffer_for_stroke(
8209 cmyk_buf: &mut [f32],
8210 spot_mask: &mut [u8],
8211 path: &PsPath,
8212 params: &StrokeParams,
8213 stroke: &Stroke,
8214 transform: Transform,
8215 out_w: u32,
8216 out_h: u32,
8217 clip_region: &Option<ClipRegion>,
8218 no_aa: bool,
8219 icc: Option<&IccCache>,
8220) {
8221 let is_custom_spot = params.painted_channels == 0
8228 && !params.is_device_cmyk
8229 && params.color.process_cmyk.is_some();
8230 let has_spot_contrib = (is_custom_spot && params.color.native_cmyk.is_some())
8232 || matches!(
8233 (params.color.native_cmyk, params.color.process_cmyk),
8234 (Some(nat), Some(proc_))
8235 if (nat.0 - proc_.0).abs() > 1e-6
8236 || (nat.1 - proc_.1).abs() > 1e-6
8237 || (nat.2 - proc_.2).abs() > 1e-6
8238 || (nat.3 - proc_.3).abs() > 1e-6
8239 );
8240
8241 let (src_c, src_m, src_y, src_k) = if is_custom_spot {
8242 (0.0, 0.0, 0.0, 0.0)
8243 } else if let Some(c) = params.color.process_cmyk {
8244 c
8245 } else if let Some(c) = params.color.native_cmyk {
8246 c
8247 } else if let Some(cmyk) = icc.and_then(|i| {
8248 i.convert_rgb_to_cmyk_readonly(params.color.r, params.color.g, params.color.b)
8249 }) {
8250 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
8251 } else {
8252 (
8253 (1.0 - params.color.r).clamp(0.0, 1.0),
8254 (1.0 - params.color.g).clamp(0.0, 1.0),
8255 (1.0 - params.color.b).clamp(0.0, 1.0),
8256 0.0,
8257 )
8258 };
8259
8260 let Some(skia_path) = build_skia_path(path) else {
8261 return;
8262 };
8263
8264 let resolution_scale = (transform.sx * transform.sx + transform.sy * transform.sy)
8268 .sqrt()
8269 .max(1.0);
8270 let dashed_op;
8271 let stroke_src = if let Some(ref dash) = stroke.dash {
8272 dashed_op = skia_path.dash(dash, resolution_scale);
8273 match dashed_op.as_ref() {
8274 Some(p) => p,
8275 None => &skia_path,
8276 }
8277 } else {
8278 &skia_path
8279 };
8280 let Some(stroked_user) = stroke_src.stroke(stroke, resolution_scale) else {
8281 return;
8282 };
8283 let Some(stroked) = stroked_user.transform(transform) else {
8284 return;
8285 };
8286
8287 let mut coverage_mask = match Mask::new(out_w, out_h) {
8288 Some(m) => m,
8289 None => return,
8290 };
8291 coverage_mask.fill_path(
8292 &stroked,
8293 SkiaFillRule::Winding,
8294 !no_aa,
8295 Transform::identity(),
8296 );
8297
8298 let cov_data = coverage_mask.data();
8299 let clip_data: Option<&[u8]> = match clip_region {
8300 Some(ClipRegion::Mask(m)) => Some(m.data()),
8301 _ => None,
8302 };
8303
8304 let (mut bx0, mut by0, mut bx1, mut by1) =
8305 path_device_bbox(&stroked, Transform::identity(), out_w, out_h);
8306 if let Some(ClipRegion::Rect(r)) = clip_region {
8307 bx0 = bx0.max(r.x0 as usize);
8308 by0 = by0.max(r.y0 as usize);
8309 bx1 = bx1.min(r.x1 as usize);
8310 by1 = by1.min(r.y1 as usize);
8311 }
8312
8313 let stride = out_w as usize;
8314 for y in by0..by1 {
8315 for x in bx0..bx1 {
8316 let mi = y * stride + x;
8317 let mut cov = cov_data[mi] as f32 / 255.0;
8318 if let Some(clip) = clip_data {
8319 cov *= clip[mi] as f32 / 255.0;
8320 }
8321 if cov > 0.0 {
8322 let ci = mi * 4;
8323 cmyk_buf[ci] = src_c as f32;
8324 cmyk_buf[ci + 1] = src_m as f32;
8325 cmyk_buf[ci + 2] = src_y as f32;
8326 cmyk_buf[ci + 3] = src_k as f32;
8327 if has_spot_contrib {
8328 spot_mask[mi] = 1;
8329 }
8330 }
8331 }
8332 }
8333}
8334
8335#[allow(clippy::too_many_arguments)]
8337fn render_overprint_image(
8338 pixmap: &mut Pixmap,
8339 cmyk_buf: &mut [f32],
8340 op_bg: &mut [u8],
8341 op_touched: &mut [u8],
8342 band_state: &mut BandState,
8343 sample_data: &[u8],
8344 params: &ImageParams,
8345 vp_x: f32,
8346 vp_y: f32,
8347 scale_x: f32,
8348 scale_y: f32,
8349 out_w: u32,
8350 out_h: u32,
8351 icc: Option<&IccCache>,
8352) {
8353 let iw = params.width as usize;
8354 let ih = params.height as usize;
8355 let Some(image_inv) = params.image_matrix.invert() else {
8356 return;
8357 };
8358 let combined = params.ctm.concat(&image_inv);
8359 let Some(inv_combined) = combined.invert() else {
8360 return;
8361 };
8362
8363 let px_data = pixmap.data_mut();
8364 let stride = out_w as usize;
8365 let inv_sx = 1.0 / scale_x as f64;
8366 let inv_sy = 1.0 / scale_y as f64;
8367
8368 let clip_data: Option<&[u8]> = match &band_state.clip_region {
8369 Some(ClipRegion::Mask(m)) => Some(m.data()),
8370 _ => None,
8371 };
8372 let clip_rect = match &band_state.clip_region {
8373 Some(ClipRegion::Rect(r)) => Some(*r),
8374 _ => None,
8375 };
8376
8377 let mask_info = if let ImageColorSpace::Mask {
8378 color, polarity, ..
8379 } = ¶ms.color_space
8380 {
8381 let (src_c, src_m, src_y, src_k) = color.native_cmyk.unwrap_or_else(|| {
8382 let r = color.r;
8383 let g = color.g;
8384 let b = color.b;
8385 (1.0 - r, 1.0 - g, 1.0 - b, 0.0)
8386 });
8387 Some((src_c, src_m, src_y, src_k, *polarity, iw.div_ceil(8)))
8388 } else {
8389 None
8390 };
8391
8392 for by in 0..out_h as usize {
8393 for bx in 0..out_w as usize {
8394 if let Some(ref r) = clip_rect
8395 && ((by as u32) < r.y0
8396 || (by as u32) >= r.y1
8397 || (bx as u32) < r.x0
8398 || (bx as u32) >= r.x1)
8399 {
8400 continue;
8401 }
8402 if let Some(clip) = clip_data {
8403 let ci_clip = by * stride + bx;
8404 if clip[ci_clip] == 0 {
8405 let bh = out_h as usize;
8406 let has_neighbor = (bx > 0 && clip[ci_clip - 1] != 0)
8407 || (bx + 1 < stride && clip[ci_clip + 1] != 0)
8408 || (by > 0 && clip[ci_clip - stride] != 0)
8409 || (by + 1 < bh && clip[ci_clip + stride] != 0)
8410 || (bx > 0 && by > 0 && clip[ci_clip - stride - 1] != 0)
8411 || (bx + 1 < stride && by > 0 && clip[ci_clip - stride + 1] != 0)
8412 || (bx > 0 && by + 1 < bh && clip[ci_clip + stride - 1] != 0)
8413 || (bx + 1 < stride && by + 1 < bh && clip[ci_clip + stride + 1] != 0);
8414 if !has_neighbor {
8415 continue;
8416 }
8417 }
8418 }
8419
8420 let dx = (bx as f64 + 0.5) * inv_sx + vp_x as f64;
8422 let dy = (by as f64 + 0.5) * inv_sy + vp_y as f64;
8423 let ix = inv_combined.a * dx + inv_combined.c * dy + inv_combined.tx;
8424 let iy = inv_combined.b * dx + inv_combined.d * dy + inv_combined.ty;
8425
8426 let col = ix.floor() as i64;
8427 let row = iy.floor() as i64;
8428 if col < 0 || col >= iw as i64 || row < 0 || row >= ih as i64 {
8429 continue;
8430 }
8431 let col = col as usize;
8432 let row = row as usize;
8433
8434 let (src_c, src_m, src_y, src_k) =
8435 if let Some((mc, mm, my, mk, polarity, bytes_per_row)) = mask_info {
8436 let byte_idx = row * bytes_per_row + col / 8;
8437 let bit_offset = 7 - (col % 8);
8438 let bit = if byte_idx < sample_data.len() {
8439 (sample_data[byte_idx] >> bit_offset) & 1
8440 } else {
8441 0
8442 };
8443 let paint = if polarity { bit == 1 } else { bit == 0 };
8444 if !paint {
8445 continue;
8446 }
8447 (mc, mm, my, mk)
8448 } else if let Some(cmyk) =
8449 sample_pixel_cmyk(sample_data, ¶ms.color_space, iw, row, col)
8450 {
8451 cmyk
8452 } else {
8453 continue;
8454 };
8455
8456 let mi = by * stride + bx;
8457 let ci = mi * 4;
8458 let pi = mi * 4;
8459
8460 if image_cs_has_spot_tint_transform(¶ms.color_space) {
8479 let cur_c = cmyk_buf[ci] as f64;
8480 let cur_m = cmyk_buf[ci + 1] as f64;
8481 let cur_y = cmyk_buf[ci + 2] as f64;
8482 let cur_k = cmyk_buf[ci + 3] as f64;
8483 let cur_is_zero = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
8484 let named = params.painted_channels;
8485 let opm1 = params.overprint_mode == 1;
8492 let (new_c, new_m, new_y, new_k) = if cur_is_zero {
8493 (src_c, src_m, src_y, src_k)
8494 } else {
8495 let nc =
8496 if named & stet_graphics::device::CMYK_C != 0 && !(opm1 && src_c == 0.0) {
8497 src_c
8498 } else {
8499 cur_c
8500 };
8501 let nm =
8502 if named & stet_graphics::device::CMYK_M != 0 && !(opm1 && src_m == 0.0) {
8503 src_m
8504 } else {
8505 cur_m
8506 };
8507 let ny =
8508 if named & stet_graphics::device::CMYK_Y != 0 && !(opm1 && src_y == 0.0) {
8509 src_y
8510 } else {
8511 cur_y
8512 };
8513 let nk =
8514 if named & stet_graphics::device::CMYK_K != 0 && !(opm1 && src_k == 0.0) {
8515 src_k
8516 } else {
8517 cur_k
8518 };
8519 (nc, nm, ny, nk)
8520 };
8521 cmyk_buf[ci] = new_c as f32;
8522 cmyk_buf[ci + 1] = new_m as f32;
8523 cmyk_buf[ci + 2] = new_y as f32;
8524 cmyk_buf[ci + 3] = new_k as f32;
8525 let alt_is_non_cmyk = image_cs_alt_is_non_cmyk(¶ms.color_space);
8534 let (r, g, b) = if cur_is_zero
8535 && alt_is_non_cmyk
8536 && let Some(rgb) =
8537 sample_pixel_visual_rgb(sample_data, ¶ms.color_space, iw, row, col)
8538 {
8539 rgb
8540 } else if let Some(icc_cache) = icc {
8541 icc_cache
8542 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
8543 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
8544 } else {
8545 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
8546 };
8547 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
8548 op_bg[pi] = px_data[pi];
8549 op_bg[pi + 1] = px_data[pi + 1];
8550 op_bg[pi + 2] = px_data[pi + 2];
8551 op_bg[pi + 3] = px_data[pi + 3];
8552 op_touched[mi] = 1;
8553 }
8554 px_data[pi] = (r * 255.0).round() as u8;
8555 px_data[pi + 1] = (g * 255.0).round() as u8;
8556 px_data[pi + 2] = (b * 255.0).round() as u8;
8557 px_data[pi + 3] = 255;
8558 continue;
8559 }
8560
8561 let mut channels = params.painted_channels;
8562 if channels == 0 {
8565 channels = stet_graphics::device::CMYK_ALL;
8566 }
8567 let is_direct_cmyk = matches!(
8568 ¶ms.color_space,
8569 ImageColorSpace::DeviceCMYK
8570 | ImageColorSpace::ICCBased { n: 4, .. }
8571 | ImageColorSpace::Mask { .. }
8572 );
8573 let is_custom_spot = params.painted_channels == 0
8583 && !is_cmyk_color_space(¶ms.color_space)
8584 && match ¶ms.color_space {
8585 ImageColorSpace::Mask { color, .. } => color.native_cmyk.is_some(),
8586 _ => true,
8587 };
8588 if params.overprint_mode == 1
8589 && channels == stet_graphics::device::CMYK_ALL
8590 && is_direct_cmyk
8591 {
8592 channels = 0;
8593 if src_c != 0.0 {
8594 channels |= stet_graphics::device::CMYK_C;
8595 }
8596 if src_m != 0.0 {
8597 channels |= stet_graphics::device::CMYK_M;
8598 }
8599 if src_y != 0.0 {
8600 channels |= stet_graphics::device::CMYK_Y;
8601 }
8602 if src_k != 0.0 {
8603 channels |= stet_graphics::device::CMYK_K;
8604 }
8605 }
8606
8607 let cur_c = cmyk_buf[ci] as f64;
8608 let cur_m = cmyk_buf[ci + 1] as f64;
8609 let cur_y = cmyk_buf[ci + 2] as f64;
8610 let cur_k = cmyk_buf[ci + 3] as f64;
8611 let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
8612 let pixmap_has_colour = px_data[pi + 3] > 0
8613 && (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
8614 let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
8622
8623 let new_c = if channels & stet_graphics::device::CMYK_C != 0 {
8624 src_c
8625 } else {
8626 cur_c
8627 };
8628 let new_m = if channels & stet_graphics::device::CMYK_M != 0 {
8629 src_m
8630 } else {
8631 cur_m
8632 };
8633 let new_y = if channels & stet_graphics::device::CMYK_Y != 0 {
8634 src_y
8635 } else {
8636 cur_y
8637 };
8638 let new_k = if channels & stet_graphics::device::CMYK_K != 0 {
8639 src_k
8640 } else {
8641 cur_k
8642 };
8643
8644 if !is_custom_spot {
8645 cmyk_buf[ci] = new_c as f32;
8646 cmyk_buf[ci + 1] = new_m as f32;
8647 cmyk_buf[ci + 2] = new_y as f32;
8648 cmyk_buf[ci + 3] = new_k as f32;
8649 }
8650
8651 let (r, g, b) = if use_multiplicative {
8652 let bg_r = px_data[pi] as f64 / 255.0;
8653 let bg_g = px_data[pi + 1] as f64 / 255.0;
8654 let bg_b = px_data[pi + 2] as f64 / 255.0;
8655 let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
8656 1.0 - src_c
8657 } else {
8658 1.0
8659 };
8660 let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
8661 1.0 - src_m
8662 } else {
8663 1.0
8664 };
8665 let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
8666 1.0 - src_y
8667 } else {
8668 1.0
8669 };
8670 let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
8671 1.0 - src_k
8672 } else {
8673 1.0
8674 };
8675 (
8676 (bg_r * over_r * k_fac).clamp(0.0, 1.0),
8677 (bg_g * over_g * k_fac).clamp(0.0, 1.0),
8678 (bg_b * over_b * k_fac).clamp(0.0, 1.0),
8679 )
8680 } else if let Some(icc_cache) = icc {
8681 icc_cache
8682 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
8683 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
8684 } else {
8685 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
8686 };
8687
8688 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
8691 op_bg[pi] = px_data[pi];
8692 op_bg[pi + 1] = px_data[pi + 1];
8693 op_bg[pi + 2] = px_data[pi + 2];
8694 op_bg[pi + 3] = px_data[pi + 3];
8695 op_touched[mi] = 1;
8696 }
8697
8698 px_data[pi] = (r * 255.0).round() as u8;
8699 px_data[pi + 1] = (g * 255.0).round() as u8;
8700 px_data[pi + 2] = (b * 255.0).round() as u8;
8701 px_data[pi + 3] = 255;
8702 }
8703 }
8704}
8705
8706#[allow(clippy::too_many_arguments)]
8716fn update_cmyk_buffer_for_image(
8717 cmyk_buf: &mut [f32],
8718 sample_data: &[u8],
8719 pixmap_rgba: &[u8],
8720 params: &ImageParams,
8721 vp_x: f32,
8722 vp_y: f32,
8723 scale_x: f32,
8724 scale_y: f32,
8725 out_w: u32,
8726 out_h: u32,
8727 clip_region: &Option<ClipRegion>,
8728 icc: Option<&IccCache>,
8729) {
8730 let iw = params.width as usize;
8731 let ih = params.height as usize;
8732 let Some(image_inv) = params.image_matrix.invert() else {
8733 return;
8734 };
8735 let combined = params.ctm.concat(&image_inv);
8736 let Some(inv_combined) = combined.invert() else {
8737 return;
8738 };
8739 let stride = out_w as usize;
8740 let inv_sx = 1.0 / scale_x as f64;
8741 let inv_sy = 1.0 / scale_y as f64;
8742
8743 let mask_info = if let ImageColorSpace::Mask {
8744 color, polarity, ..
8745 } = ¶ms.color_space
8746 {
8747 let Some((c, m, y, k)) = color.native_cmyk else {
8748 return;
8749 };
8750 Some((
8751 c as f32,
8752 m as f32,
8753 y as f32,
8754 k as f32,
8755 *polarity,
8756 iw.div_ceil(8),
8757 ))
8758 } else {
8759 None
8760 };
8761
8762 let clip_data: Option<&[u8]> = match clip_region {
8763 Some(ClipRegion::Mask(m)) => Some(m.data()),
8764 _ => None,
8765 };
8766 let clip_rect = match clip_region {
8767 Some(ClipRegion::Rect(r)) => Some(*r),
8768 _ => None,
8769 };
8770
8771 for by in 0..out_h as usize {
8772 for bx in 0..out_w as usize {
8773 if let Some(ref r) = clip_rect
8774 && ((by as u32) < r.y0
8775 || (by as u32) >= r.y1
8776 || (bx as u32) < r.x0
8777 || (bx as u32) >= r.x1)
8778 {
8779 continue;
8780 }
8781 if let Some(clip) = clip_data
8782 && clip[by * stride + bx] == 0
8783 {
8784 continue;
8785 }
8786
8787 let dx = (bx as f64 + 0.5) * inv_sx + vp_x as f64;
8788 let dy = (by as f64 + 0.5) * inv_sy + vp_y as f64;
8789 let ix = inv_combined.a * dx + inv_combined.c * dy + inv_combined.tx;
8790 let iy = inv_combined.b * dx + inv_combined.d * dy + inv_combined.ty;
8791
8792 let col = ix.floor() as i64;
8793 let row = iy.floor() as i64;
8794 if col < 0 || col >= iw as i64 || row < 0 || row >= ih as i64 {
8795 continue;
8796 }
8797 let col = col as usize;
8798 let row = row as usize;
8799
8800 let ci = (by * stride + bx) * 4;
8801 if let Some((sc, sm, sy, sk, polarity, bytes_per_row)) = mask_info {
8802 let byte_idx = row * bytes_per_row + col / 8;
8803 let bit_offset = 7 - (col % 8);
8804 let bit = if byte_idx < sample_data.len() {
8805 (sample_data[byte_idx] >> bit_offset) & 1
8806 } else {
8807 0
8808 };
8809 let paint = if polarity { bit == 1 } else { bit == 0 };
8810 if paint {
8811 cmyk_buf[ci] = sc;
8812 cmyk_buf[ci + 1] = sm;
8813 cmyk_buf[ci + 2] = sy;
8814 cmyk_buf[ci + 3] = sk;
8815 }
8816 } else if let Some((sc, sm, sy, sk)) =
8817 sample_pixel_cmyk(sample_data, ¶ms.color_space, iw, row, col)
8818 {
8819 cmyk_buf[ci] = sc as f32;
8820 cmyk_buf[ci + 1] = sm as f32;
8821 cmyk_buf[ci + 2] = sy as f32;
8822 cmyk_buf[ci + 3] = sk as f32;
8823 } else if ci + 3 < pixmap_rgba.len() && pixmap_rgba[ci + 3] > 0 {
8824 let r = pixmap_rgba[ci] as f64 / 255.0;
8828 let g = pixmap_rgba[ci + 1] as f64 / 255.0;
8829 let b = pixmap_rgba[ci + 2] as f64 / 255.0;
8830 let cmyk =
8831 if let Some(c) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(r, g, b)) {
8832 c
8833 } else {
8834 [
8835 (1.0 - r).clamp(0.0, 1.0),
8836 (1.0 - g).clamp(0.0, 1.0),
8837 (1.0 - b).clamp(0.0, 1.0),
8838 0.0,
8839 ]
8840 };
8841 cmyk_buf[ci] = cmyk[0] as f32;
8842 cmyk_buf[ci + 1] = cmyk[1] as f32;
8843 cmyk_buf[ci + 2] = cmyk[2] as f32;
8844 cmyk_buf[ci + 3] = cmyk[3] as f32;
8845 }
8846 }
8847 }
8848}
8849fn image_supports_overprint(cs: &ImageColorSpace) -> bool {
8853 use stet_graphics::device::cmyk_channel_for_name;
8854 match cs {
8855 ImageColorSpace::Mask { .. } => true,
8856 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. } => true,
8857 ImageColorSpace::Separation {
8858 alt_space, name, ..
8859 } => {
8860 matches!(
8861 alt_space.as_ref(),
8862 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8863 ) || cmyk_channel_for_name(name) != 0
8864 }
8865 ImageColorSpace::DeviceN {
8866 alt_space, names, ..
8867 } => {
8868 matches!(
8869 alt_space.as_ref(),
8870 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8871 ) || names.iter().any(|n| cmyk_channel_for_name(n) != 0)
8872 }
8873 ImageColorSpace::Indexed { base, .. } => image_supports_overprint(base),
8874 _ => false,
8875 }
8876}
8877
8878fn is_cmyk_color_space(cs: &ImageColorSpace) -> bool {
8880 match cs {
8881 ImageColorSpace::DeviceCMYK => true,
8882 ImageColorSpace::ICCBased { n: 4, .. } => true,
8883 ImageColorSpace::Indexed { base, .. } => is_cmyk_color_space(base),
8884 _ => false,
8885 }
8886}
8887
8888fn image_cs_has_spot_tint_transform(cs: &ImageColorSpace) -> bool {
8895 use stet_graphics::device::cmyk_channel_for_name;
8896 let is_cmyk_alt = |alt: &ImageColorSpace| {
8897 matches!(
8898 alt,
8899 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8900 )
8901 };
8902 match cs {
8903 ImageColorSpace::Separation {
8904 name, alt_space, ..
8905 } => cmyk_channel_for_name(name) == 0 && is_cmyk_alt(alt_space.as_ref()),
8906 ImageColorSpace::DeviceN {
8907 names, alt_space, ..
8908 } => {
8909 let has_spot = names.iter().any(|n| cmyk_channel_for_name(n) == 0);
8910 let has_process = names.iter().any(|n| cmyk_channel_for_name(n) != 0);
8911 has_spot && (is_cmyk_alt(alt_space.as_ref()) || has_process)
8912 }
8913 ImageColorSpace::Indexed { base, .. } => image_cs_has_spot_tint_transform(base),
8914 _ => false,
8915 }
8916}
8917
8918fn image_cs_alt_is_non_cmyk(cs: &ImageColorSpace) -> bool {
8924 let is_cmyk_alt = |alt: &ImageColorSpace| {
8925 matches!(
8926 alt,
8927 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8928 )
8929 };
8930 match cs {
8931 ImageColorSpace::Separation { alt_space, .. }
8932 | ImageColorSpace::DeviceN { alt_space, .. } => !is_cmyk_alt(alt_space.as_ref()),
8933 ImageColorSpace::Indexed { base, .. } => image_cs_alt_is_non_cmyk(base),
8934 _ => false,
8935 }
8936}
8937
8938fn sample_pixel_visual_rgb(
8943 sample_data: &[u8],
8944 cs: &ImageColorSpace,
8945 iw: usize,
8946 row: usize,
8947 col: usize,
8948) -> Option<(f64, f64, f64)> {
8949 let to_f64 = |(r, g, b): (u8, u8, u8)| (r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0);
8950 match cs {
8951 ImageColorSpace::Separation {
8952 alt_space,
8953 tint_table,
8954 ..
8955 } => {
8956 let si = row * iw + col;
8957 if si >= sample_data.len() {
8958 return None;
8959 }
8960 let tint = sample_data[si] as f32 / 255.0;
8961 let no = tint_table.num_outputs as usize;
8962 let mut comps = vec![0.0f32; no];
8963 tint_table.lookup_1d(tint, &mut comps);
8964 Some(to_f64(alt_comps_to_rgb(&comps, alt_space)))
8965 }
8966 ImageColorSpace::DeviceN {
8967 alt_space,
8968 tint_table,
8969 ..
8970 } => {
8971 let ni = tint_table.num_inputs as usize;
8972 let si = (row * iw + col) * ni;
8973 if si + ni > sample_data.len() {
8974 return None;
8975 }
8976 let mut inputs = vec![0.0f32; ni];
8977 for (c, inp) in inputs.iter_mut().enumerate() {
8978 *inp = sample_data[si + c] as f32 / 255.0;
8979 }
8980 let no = tint_table.num_outputs as usize;
8981 let mut comps = vec![0.0f32; no];
8982 tint_table.lookup_nd(&inputs, &mut comps);
8983 Some(to_f64(alt_comps_to_rgb(&comps, alt_space)))
8984 }
8985 ImageColorSpace::Indexed {
8986 base,
8987 hival,
8988 lookup,
8989 } => {
8990 let pi = row * iw + col;
8991 if pi >= sample_data.len() {
8992 return None;
8993 }
8994 let idx = (sample_data[pi] as usize).min(*hival as usize);
8995 let base_ncomp = base.num_components() as usize;
8996 let li = idx * base_ncomp;
8997 if li + base_ncomp > lookup.len() {
8998 return None;
8999 }
9000 match base.as_ref() {
9001 ImageColorSpace::Separation {
9002 alt_space,
9003 tint_table,
9004 ..
9005 } => {
9006 let tint = lookup[li] as f32 / 255.0;
9007 let no = tint_table.num_outputs as usize;
9008 let mut comps = vec![0.0f32; no];
9009 tint_table.lookup_1d(tint, &mut comps);
9010 Some(to_f64(alt_comps_to_rgb(&comps, alt_space)))
9011 }
9012 ImageColorSpace::DeviceN {
9013 alt_space,
9014 tint_table,
9015 ..
9016 } => {
9017 let ni = tint_table.num_inputs as usize;
9018 let mut inputs = vec![0.0f32; ni];
9019 for (c, inp) in inputs.iter_mut().enumerate() {
9020 if c < base_ncomp {
9021 *inp = lookup[li + c] as f32 / 255.0;
9022 }
9023 }
9024 let no = tint_table.num_outputs as usize;
9025 let mut comps = vec![0.0f32; no];
9026 tint_table.lookup_nd(&inputs, &mut comps);
9027 Some(to_f64(alt_comps_to_rgb(&comps, alt_space)))
9028 }
9029 _ => None,
9030 }
9031 }
9032 _ => None,
9033 }
9034}
9035
9036fn devicen_named_cmyk(names: &[Vec<u8>], inputs: &[u8]) -> (f64, f64, f64, f64) {
9042 use stet_graphics::device::{CMYK_C, CMYK_K, CMYK_M, CMYK_Y, cmyk_channel_for_name};
9043 let mut c = 0.0;
9044 let mut m = 0.0;
9045 let mut y = 0.0;
9046 let mut k = 0.0;
9047 for (i, name) in names.iter().enumerate() {
9048 let bit = cmyk_channel_for_name(name);
9049 if bit == 0 {
9050 continue;
9051 }
9052 let v = inputs.get(i).copied().unwrap_or(0) as f64 / 255.0;
9053 if bit & CMYK_C != 0 {
9054 c = v;
9055 }
9056 if bit & CMYK_M != 0 {
9057 m = v;
9058 }
9059 if bit & CMYK_Y != 0 {
9060 y = v;
9061 }
9062 if bit & CMYK_K != 0 {
9063 k = v;
9064 }
9065 }
9066 (c, m, y, k)
9067}
9068
9069fn sample_pixel_cmyk(
9074 sample_data: &[u8],
9075 cs: &ImageColorSpace,
9076 iw: usize,
9077 row: usize,
9078 col: usize,
9079) -> Option<(f64, f64, f64, f64)> {
9080 use stet_graphics::device::cmyk_channel_for_name;
9081 let is_cmyk_alt = |alt: &ImageColorSpace| {
9082 matches!(
9083 alt,
9084 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
9085 )
9086 };
9087 match cs {
9088 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. } => {
9089 let si = (row * iw + col) * 4;
9090 if si + 3 < sample_data.len() {
9091 Some((
9092 sample_data[si] as f64 / 255.0,
9093 sample_data[si + 1] as f64 / 255.0,
9094 sample_data[si + 2] as f64 / 255.0,
9095 sample_data[si + 3] as f64 / 255.0,
9096 ))
9097 } else {
9098 None
9099 }
9100 }
9101 ImageColorSpace::Separation {
9102 alt_space,
9103 tint_table,
9104 name,
9105 } => {
9106 let si = row * iw + col;
9107 if si >= sample_data.len() {
9108 return None;
9109 }
9110 let tint = sample_data[si] as f32 / 255.0;
9111 if is_cmyk_alt(alt_space.as_ref()) {
9112 let mut alt = [0.0f32; 4];
9113 tint_table.lookup_1d(tint, &mut alt);
9114 return Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64));
9115 }
9116 let bit = cmyk_channel_for_name(name);
9118 if bit == 0 {
9119 return None;
9120 }
9121 let names = vec![name.clone()];
9122 let inputs = [(tint * 255.0).round() as u8];
9123 Some(devicen_named_cmyk(&names, &inputs))
9124 }
9125 ImageColorSpace::DeviceN {
9126 alt_space,
9127 tint_table,
9128 names,
9129 } => {
9130 let ni = tint_table.num_inputs as usize;
9131 let si = (row * iw + col) * ni;
9132 if si + ni > sample_data.len() {
9133 return None;
9134 }
9135 if is_cmyk_alt(alt_space.as_ref()) {
9136 let mut inputs = vec![0.0f32; ni];
9137 for (c, inp) in inputs.iter_mut().enumerate() {
9138 *inp = sample_data[si + c] as f32 / 255.0;
9139 }
9140 let mut alt = [0.0f32; 4];
9141 tint_table.lookup_nd(&inputs, &mut alt);
9142 return Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64));
9143 }
9144 if !names.iter().any(|n| cmyk_channel_for_name(n) != 0) {
9146 return None;
9147 }
9148 Some(devicen_named_cmyk(names, &sample_data[si..si + ni]))
9149 }
9150 ImageColorSpace::Indexed {
9151 base,
9152 hival,
9153 lookup,
9154 } => {
9155 let pi = row * iw + col;
9156 if pi >= sample_data.len() {
9157 return None;
9158 }
9159 let idx = sample_data[pi] as usize;
9160 let idx = idx.min(*hival as usize);
9161 let base_ncomp = base.num_components() as usize;
9162 let li = idx * base_ncomp;
9163 if is_cmyk_color_space(base) && base_ncomp == 4 && li + 3 < lookup.len() {
9165 return Some((
9166 lookup[li] as f64 / 255.0,
9167 lookup[li + 1] as f64 / 255.0,
9168 lookup[li + 2] as f64 / 255.0,
9169 lookup[li + 3] as f64 / 255.0,
9170 ));
9171 }
9172 if li + base_ncomp <= lookup.len() {
9174 match base.as_ref() {
9175 ImageColorSpace::Separation {
9176 alt_space,
9177 tint_table,
9178 name,
9179 } => {
9180 let tint = lookup[li] as f32 / 255.0;
9181 if is_cmyk_alt(alt_space.as_ref()) {
9182 let mut alt = [0.0f32; 4];
9183 tint_table.lookup_1d(tint, &mut alt);
9184 return Some((
9185 alt[0] as f64,
9186 alt[1] as f64,
9187 alt[2] as f64,
9188 alt[3] as f64,
9189 ));
9190 }
9191 let bit = cmyk_channel_for_name(name);
9193 if bit == 0 {
9194 return None;
9195 }
9196 let names = vec![name.clone()];
9197 let inputs = [(tint * 255.0).round() as u8];
9198 return Some(devicen_named_cmyk(&names, &inputs));
9199 }
9200 ImageColorSpace::DeviceN {
9201 alt_space,
9202 tint_table,
9203 names,
9204 } => {
9205 let ni = tint_table.num_inputs as usize;
9206 if is_cmyk_alt(alt_space.as_ref()) {
9207 let mut inputs = vec![0.0f32; ni];
9208 for (c, inp) in inputs.iter_mut().enumerate() {
9209 if c < base_ncomp {
9210 *inp = lookup[li + c] as f32 / 255.0;
9211 }
9212 }
9213 let mut alt = [0.0f32; 4];
9214 tint_table.lookup_nd(&inputs, &mut alt);
9215 return Some((
9216 alt[0] as f64,
9217 alt[1] as f64,
9218 alt[2] as f64,
9219 alt[3] as f64,
9220 ));
9221 }
9222 if !names.iter().any(|n| cmyk_channel_for_name(n) != 0) {
9224 return None;
9225 }
9226 let take = ni.min(base_ncomp);
9227 return Some(devicen_named_cmyk(names, &lookup[li..li + take]));
9228 }
9229 _ => {}
9230 }
9231 }
9232 None
9233 }
9234 _ => None,
9235 }
9236}
9237#[allow(clippy::too_many_arguments)]
9243fn render_banded_to_sink(
9244 page_w: u32,
9245 page_h: u32,
9246 band_h: u32,
9247 dpi: f64,
9248 list: &DisplayList,
9249 sink: &mut dyn stet_graphics::device::PageSink,
9250 icc_cache: &IccCache,
9251 no_aa: bool,
9252 layer_set: &LayerSet,
9253) -> Result<(), String> {
9254 let bboxes = precompute_bboxes(list, dpi);
9256
9257 let epochs = build_clip_epochs(list, &bboxes);
9261
9262 let clip_seen = precompute_clip_seen(list);
9264
9265 use stet_graphics::display_list::GroupColorSpace;
9270 let needs_cmyk_buffer = has_overprint_elements(list)
9271 || list.page_group_color_space() == GroupColorSpace::DeviceCMYK
9272 || has_cmyk_group(list);
9273
9274 let preprocessed_images = preprocess_images_for_bands(list, Some(icc_cache));
9276
9277 const BAND_OVERLAP: u32 = 6;
9281
9282 let render_h = band_h + 2 * BAND_OVERLAP;
9283
9284 sink.begin_page(page_w, page_h)?;
9286
9287 let num_bands = page_h.div_ceil(band_h);
9288 let elements = list.elements();
9289 let row_bytes = page_w as usize * 4;
9290 let icc_ref = Some(icc_cache);
9291
9292 let render_band = |band_idx: u32| -> Vec<u8> {
9294 let y_start = band_idx * band_h;
9295 let actual_h = (page_h - y_start).min(band_h);
9296
9297 let render_y_start = y_start.saturating_sub(BAND_OVERLAP);
9298 let render_y_end_f = ((y_start + actual_h + BAND_OVERLAP).min(page_h)) as f64;
9299 let band_offset = y_start - render_y_start;
9300
9301 let mut band_pixmap = Pixmap::new(page_w, render_h).expect("Failed to create band pixmap");
9302 band_pixmap.as_mut().data_mut().fill(0x00);
9304
9305 let cmyk_buf = if needs_cmyk_buffer {
9306 Some(vec![0.0f32; page_w as usize * render_h as usize * 4])
9308 } else {
9309 None
9310 };
9311
9312 let mut band_state = BandState {
9313 clip_region: None,
9314 spare_mask: None,
9315 clip_mask_cache: HashMap::new(),
9316 clip_mask_seen: clip_seen.clone(),
9317 mask_pool: Vec::new(),
9318 cmyk_buffer: cmyk_buf,
9319 op_bg_snapshot: None,
9320 op_touched: None,
9321 spot_mask: None,
9322 };
9323
9324 for epoch in &epochs {
9326 if !epoch.has_erase_page {
9327 match epoch.paint_bbox {
9328 Some(ref pb)
9329 if pb.y_max <= render_y_start as f64 || pb.y_min >= render_y_end_f =>
9330 {
9331 continue;
9332 }
9333 None => continue,
9334 _ => {}
9335 }
9336 }
9337
9338 for i in epoch.start_idx..epoch.end_idx {
9339 let force_process = matches!(
9345 &elements[i],
9346 DisplayElement::OcgGroup { elements: inner, .. }
9347 if contains_clip_op(inner)
9348 );
9349 if !force_process
9350 && let Some(ref bbox) = bboxes[i]
9351 && (bbox.y_max <= render_y_start as f64 || bbox.y_min >= render_y_end_f)
9352 {
9353 continue;
9354 }
9355 let ctx = RenderContext {
9356 vp_x: 0.0,
9357 vp_y: render_y_start as f32,
9358 scale_x: 1.0,
9359 scale_y: 1.0,
9360 out_w: page_w,
9361 out_h: render_h,
9362 effective_dpi: dpi,
9363 icc: icc_ref,
9364 image_cache: None,
9365 preprocessed: Some(&preprocessed_images),
9366 elem_idx: i,
9367 no_aa,
9368 opm_zero_transparent: false,
9369 knockout_painter_pass: KnockoutPainterPass::None,
9370 parent_group_isolated: false,
9371 alpha_extraction_pass: false,
9372 layer_set,
9373 };
9374 render_element(&mut band_pixmap, &mut band_state, &elements[i], &ctx);
9375 }
9376 }
9377
9378 composite_onto_white(band_pixmap.data_mut());
9380
9381 let start_byte = band_offset as usize * row_bytes;
9383 let total_bytes = actual_h as usize * row_bytes;
9384 band_pixmap.data()[start_byte..start_byte + total_bytes].to_vec()
9385 };
9386
9387 #[cfg(feature = "parallel")]
9389 {
9390 let chunk_size = rayon::current_num_threads().max(1);
9395
9396 for chunk_start in (0..num_bands).step_by(chunk_size) {
9397 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
9398
9399 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
9400 .into_par_iter()
9401 .map(&render_band)
9402 .collect();
9403
9404 for (i, band_data) in rendered.iter().enumerate() {
9405 let band_idx = chunk_start + i as u32;
9406 let y_start = band_idx * band_h;
9407 let actual_h = (page_h - y_start).min(band_h);
9408 sink.write_rows(band_data, actual_h)?;
9409 }
9410 }
9411 }
9412 #[cfg(not(feature = "parallel"))]
9413 {
9414 for band_idx in 0..num_bands {
9416 let band_data = render_band(band_idx);
9417 let y_start = band_idx * band_h;
9418 let actual_h = (page_h - y_start).min(band_h);
9419 sink.write_rows(&band_data, actual_h)?;
9420 }
9421 }
9422
9423 sink.end_page()
9424}
9425
9426#[derive(Clone, Copy)]
9428struct BBox2D {
9429 x_min: f64,
9430 y_min: f64,
9431 x_max: f64,
9432 y_max: f64,
9433}
9434
9435fn precompute_full_bboxes(list: &DisplayList, dpi: f64) -> Vec<Option<BBox2D>> {
9437 list.elements()
9438 .iter()
9439 .map(|elem| match elem {
9440 DisplayElement::Fill { path, params } => fill_device_full_bbox(path, ¶ms.ctm),
9441 DisplayElement::Stroke { path, params } => {
9442 path_full_bbox(path).map(|mut bbox| {
9443 let effective_lw = params.line_width.max(hairline_min_width(¶ms.ctm, dpi));
9445 let expand = effective_lw * params.miter_limit * 0.5;
9446 let m = ¶ms.ctm;
9447 let is_identity = m.a == 1.0
9448 && m.b == 0.0
9449 && m.c == 0.0
9450 && m.d == 1.0
9451 && m.tx == 0.0
9452 && m.ty == 0.0;
9453 if is_identity {
9454 bbox.x_min -= expand;
9455 bbox.x_max += expand;
9456 bbox.y_min -= expand;
9457 bbox.y_max += expand;
9458 } else {
9459 let col_x_len = (m.a * m.a + m.b * m.b).sqrt().max(1.0);
9462 let col_y_len = (m.c * m.c + m.d * m.d).sqrt().max(1.0);
9463 let expand_x = effective_lw * col_x_len * params.miter_limit * 0.5;
9464 let expand_y = effective_lw * col_y_len * params.miter_limit * 0.5;
9465 bbox.x_min -= expand_x;
9466 bbox.x_max += expand_x;
9467 bbox.y_min -= expand_y;
9468 bbox.y_max += expand_y;
9469 let corners = [
9471 (
9472 m.a * bbox.x_min + m.c * bbox.y_min + m.tx,
9473 m.b * bbox.x_min + m.d * bbox.y_min + m.ty,
9474 ),
9475 (
9476 m.a * bbox.x_max + m.c * bbox.y_min + m.tx,
9477 m.b * bbox.x_max + m.d * bbox.y_min + m.ty,
9478 ),
9479 (
9480 m.a * bbox.x_min + m.c * bbox.y_max + m.tx,
9481 m.b * bbox.x_min + m.d * bbox.y_max + m.ty,
9482 ),
9483 (
9484 m.a * bbox.x_max + m.c * bbox.y_max + m.tx,
9485 m.b * bbox.x_max + m.d * bbox.y_max + m.ty,
9486 ),
9487 ];
9488 bbox.x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
9489 bbox.x_max = corners
9490 .iter()
9491 .map(|c| c.0)
9492 .fold(f64::NEG_INFINITY, f64::max);
9493 bbox.y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
9494 bbox.y_max = corners
9495 .iter()
9496 .map(|c| c.1)
9497 .fold(f64::NEG_INFINITY, f64::max);
9498 }
9499 bbox
9500 })
9501 }
9502 DisplayElement::Image { params, .. } => image_full_bbox(params),
9503 DisplayElement::AxialShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9504 DisplayElement::RadialShading { params } => {
9505 shading_full_bbox(¶ms.bbox, ¶ms.ctm)
9506 }
9507 DisplayElement::MeshShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9508 DisplayElement::PatchShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9509 DisplayElement::PatternFill { params } => pattern_fill_full_bbox(params),
9510 DisplayElement::Group { params, .. } => Some(BBox2D {
9511 x_min: params.bbox[0],
9512 y_min: params.bbox[1],
9513 x_max: params.bbox[2],
9514 y_max: params.bbox[3],
9515 }),
9516 DisplayElement::SoftMasked { params, .. } => Some(BBox2D {
9517 x_min: params.bbox[0],
9518 y_min: params.bbox[1],
9519 x_max: params.bbox[2],
9520 y_max: params.bbox[3],
9521 }),
9522 DisplayElement::OcgGroup {
9523 elements,
9524 visibility,
9525 } => {
9526 if !visibility.default_visible() && !contains_clip_op(elements) {
9531 return None;
9532 }
9533 let child_bboxes = precompute_full_bboxes(elements, dpi);
9534 let mut x_min = f64::INFINITY;
9535 let mut y_min = f64::INFINITY;
9536 let mut x_max = f64::NEG_INFINITY;
9537 let mut y_max = f64::NEG_INFINITY;
9538 for cb in child_bboxes.into_iter().flatten() {
9539 x_min = x_min.min(cb.x_min);
9540 y_min = y_min.min(cb.y_min);
9541 x_max = x_max.max(cb.x_max);
9542 y_max = y_max.max(cb.y_max);
9543 }
9544 if x_min <= x_max && y_min <= y_max {
9545 Some(BBox2D {
9546 x_min,
9547 y_min,
9548 x_max,
9549 y_max,
9550 })
9551 } else {
9552 None
9553 }
9554 }
9555 _ => None, })
9557 .collect()
9558}
9559
9560fn clip_path_bbox(path: &PsPath, params: &ClipParams) -> Option<BBox2D> {
9569 let mut bbox = path_full_bbox(path)?;
9570 let ctm = ¶ms.ctm;
9571 let is_identity = ctm.a == 1.0
9572 && ctm.b == 0.0
9573 && ctm.c == 0.0
9574 && ctm.d == 1.0
9575 && ctm.tx == 0.0
9576 && ctm.ty == 0.0;
9577 if !is_identity {
9578 let corners = [
9579 ctm.transform_point(bbox.x_min, bbox.y_min),
9580 ctm.transform_point(bbox.x_max, bbox.y_min),
9581 ctm.transform_point(bbox.x_min, bbox.y_max),
9582 ctm.transform_point(bbox.x_max, bbox.y_max),
9583 ];
9584 bbox.x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
9585 bbox.x_max = corners
9586 .iter()
9587 .map(|c| c.0)
9588 .fold(f64::NEG_INFINITY, f64::max);
9589 bbox.y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
9590 bbox.y_max = corners
9591 .iter()
9592 .map(|c| c.1)
9593 .fold(f64::NEG_INFINITY, f64::max);
9594 }
9595 if let Some(sp) = ¶ms.stroke_params {
9596 let scale = (ctm.a * ctm.a + ctm.b * ctm.b)
9597 .sqrt()
9598 .max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt())
9599 .max(1.0);
9600 let expand = sp.line_width * 0.5 * scale;
9601 bbox.x_min -= expand;
9602 bbox.x_max += expand;
9603 bbox.y_min -= expand;
9604 bbox.y_max += expand;
9605 }
9606 Some(bbox)
9607}
9608
9609fn intersect_bbox(a: &BBox2D, b: &BBox2D) -> Option<BBox2D> {
9611 let x_min = a.x_min.max(b.x_min);
9612 let y_min = a.y_min.max(b.y_min);
9613 let x_max = a.x_max.min(b.x_max);
9614 let y_max = a.y_max.min(b.y_max);
9615 if x_min < x_max && y_min < y_max {
9616 Some(BBox2D {
9617 x_min,
9618 y_min,
9619 x_max,
9620 y_max,
9621 })
9622 } else {
9623 None
9624 }
9625}
9626
9627fn compute_paint_bounds(list: &DisplayList, _dpi: f64) -> Option<BBox2D> {
9648 let mut clip_stack: Vec<BBox2D> = Vec::new();
9652 let mut union: Option<BBox2D> = None;
9653
9654 let push_paint = |union: &mut Option<BBox2D>, clip_stack: &[BBox2D], bbox: BBox2D| {
9655 let visible = match clip_stack.last() {
9659 Some(clip) => match intersect_bbox(clip, &bbox) {
9660 Some(b) => b,
9661 None => return,
9662 },
9663 None => bbox,
9664 };
9665 *union = Some(match union.take() {
9666 None => visible,
9667 Some(u) => BBox2D {
9668 x_min: u.x_min.min(visible.x_min),
9669 y_min: u.y_min.min(visible.y_min),
9670 x_max: u.x_max.max(visible.x_max),
9671 y_max: u.y_max.max(visible.y_max),
9672 },
9673 });
9674 };
9675
9676 for elem in list.elements() {
9677 match elem {
9678 DisplayElement::Clip { path, params } => {
9679 if let Some(cb) = clip_path_bbox(path, params) {
9680 let new_top = match clip_stack.last() {
9681 Some(prev) => match intersect_bbox(prev, &cb) {
9682 Some(b) => b,
9683 None => BBox2D {
9687 x_min: 0.0,
9688 y_min: 0.0,
9689 x_max: 0.0,
9690 y_max: 0.0,
9691 },
9692 },
9693 None => cb,
9694 };
9695 clip_stack.push(new_top);
9696 }
9697 }
9698 DisplayElement::InitClip | DisplayElement::ErasePage => {
9699 clip_stack.clear();
9700 }
9701 DisplayElement::Fill { path, .. } => {
9702 if let Some(b) = path_full_bbox(path) {
9703 push_paint(&mut union, &clip_stack, b);
9704 }
9705 }
9706 DisplayElement::Stroke { path, params } => {
9707 if let Some(mut b) = path_full_bbox(path) {
9708 let expand = params.line_width * params.miter_limit * 0.5;
9709 b.x_min -= expand;
9710 b.x_max += expand;
9711 b.y_min -= expand;
9712 b.y_max += expand;
9713 push_paint(&mut union, &clip_stack, b);
9714 }
9715 }
9716 DisplayElement::Image { params, .. } => {
9717 if let Some(b) = image_full_bbox(params) {
9718 push_paint(&mut union, &clip_stack, b);
9719 }
9720 }
9721 DisplayElement::AxialShading { params } => {
9722 let b = match ¶ms.bbox {
9723 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9724 None => clip_stack.last().copied(),
9725 };
9726 if let Some(b) = b {
9727 push_paint(&mut union, &clip_stack, b);
9728 }
9729 }
9730 DisplayElement::RadialShading { params } => {
9731 let b = match ¶ms.bbox {
9732 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9733 None => clip_stack.last().copied(),
9734 };
9735 if let Some(b) = b {
9736 push_paint(&mut union, &clip_stack, b);
9737 }
9738 }
9739 DisplayElement::MeshShading { params } => {
9740 let b = match ¶ms.bbox {
9741 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9742 None => clip_stack.last().copied(),
9743 };
9744 if let Some(b) = b {
9745 push_paint(&mut union, &clip_stack, b);
9746 }
9747 }
9748 DisplayElement::PatchShading { params } => {
9749 let b = match ¶ms.bbox {
9750 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9751 None => clip_stack.last().copied(),
9752 };
9753 if let Some(b) = b {
9754 push_paint(&mut union, &clip_stack, b);
9755 }
9756 }
9757 DisplayElement::PatternFill { params } => {
9758 if let Some(b) = pattern_fill_full_bbox(params) {
9759 push_paint(&mut union, &clip_stack, b);
9760 }
9761 }
9762 DisplayElement::Group { params, .. } => {
9763 push_paint(
9764 &mut union,
9765 &clip_stack,
9766 BBox2D {
9767 x_min: params.bbox[0],
9768 y_min: params.bbox[1],
9769 x_max: params.bbox[2],
9770 y_max: params.bbox[3],
9771 },
9772 );
9773 }
9774 DisplayElement::SoftMasked { params, .. } => {
9775 push_paint(
9776 &mut union,
9777 &clip_stack,
9778 BBox2D {
9779 x_min: params.bbox[0],
9780 y_min: params.bbox[1],
9781 x_max: params.bbox[2],
9782 y_max: params.bbox[3],
9783 },
9784 );
9785 }
9786 DisplayElement::Text { .. } => {} DisplayElement::OcgGroup { .. } => {
9788 }
9793 _ => {}
9794 }
9795 }
9796 union
9797}
9798
9799fn fill_device_full_bbox(path: &PsPath, ctm: &Matrix) -> Option<BBox2D> {
9804 let bbox = path_full_bbox(path)?;
9805 let is_identity = ctm.a == 1.0
9806 && ctm.b == 0.0
9807 && ctm.c == 0.0
9808 && ctm.d == 1.0
9809 && ctm.tx == 0.0
9810 && ctm.ty == 0.0;
9811 if is_identity {
9812 return Some(bbox);
9813 }
9814 let corners = [
9815 (bbox.x_min, bbox.y_min),
9816 (bbox.x_max, bbox.y_min),
9817 (bbox.x_min, bbox.y_max),
9818 (bbox.x_max, bbox.y_max),
9819 ];
9820 let mut x_min = f64::INFINITY;
9821 let mut x_max = f64::NEG_INFINITY;
9822 let mut y_min = f64::INFINITY;
9823 let mut y_max = f64::NEG_INFINITY;
9824 for (x, y) in &corners {
9825 let dx = ctm.a * x + ctm.c * y + ctm.tx;
9826 let dy = ctm.b * x + ctm.d * y + ctm.ty;
9827 x_min = x_min.min(dx);
9828 x_max = x_max.max(dx);
9829 y_min = y_min.min(dy);
9830 y_max = y_max.max(dy);
9831 }
9832 Some(BBox2D {
9833 x_min,
9834 y_min,
9835 x_max,
9836 y_max,
9837 })
9838}
9839
9840fn path_full_bbox(path: &PsPath) -> Option<BBox2D> {
9841 let mut x_min = f64::INFINITY;
9842 let mut x_max = f64::NEG_INFINITY;
9843 let mut y_min = f64::INFINITY;
9844 let mut y_max = f64::NEG_INFINITY;
9845 for seg in &path.segments {
9846 match seg {
9847 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => {
9848 x_min = x_min.min(*x);
9849 x_max = x_max.max(*x);
9850 y_min = y_min.min(*y);
9851 y_max = y_max.max(*y);
9852 }
9853 PathSegment::CurveTo {
9854 x1,
9855 y1,
9856 x2,
9857 y2,
9858 x3,
9859 y3,
9860 } => {
9861 x_min = x_min.min(*x1).min(*x2).min(*x3);
9862 x_max = x_max.max(*x1).max(*x2).max(*x3);
9863 y_min = y_min.min(*y1).min(*y2).min(*y3);
9864 y_max = y_max.max(*y1).max(*y2).max(*y3);
9865 }
9866 PathSegment::ClosePath => {}
9867 }
9868 }
9869 if x_min <= x_max {
9870 Some(BBox2D {
9871 x_min,
9872 y_min,
9873 x_max,
9874 y_max,
9875 })
9876 } else {
9877 None
9878 }
9879}
9880
9881fn pattern_fill_full_bbox(params: &stet_graphics::device::PatternFillParams) -> Option<BBox2D> {
9886 if let Some(ref sp) = params.stroke_params {
9887 let bbox = path_full_bbox(¶ms.path)?;
9888 let ctm = &sp.ctm;
9889 let corners = [
9890 ctm.transform_point(bbox.x_min, bbox.y_min),
9891 ctm.transform_point(bbox.x_max, bbox.y_min),
9892 ctm.transform_point(bbox.x_min, bbox.y_max),
9893 ctm.transform_point(bbox.x_max, bbox.y_max),
9894 ];
9895 let mut dev_bbox = BBox2D {
9896 x_min: f64::INFINITY,
9897 y_min: f64::INFINITY,
9898 x_max: f64::NEG_INFINITY,
9899 y_max: f64::NEG_INFINITY,
9900 };
9901 for (x, y) in &corners {
9902 dev_bbox.x_min = dev_bbox.x_min.min(*x);
9903 dev_bbox.y_min = dev_bbox.y_min.min(*y);
9904 dev_bbox.x_max = dev_bbox.x_max.max(*x);
9905 dev_bbox.y_max = dev_bbox.y_max.max(*y);
9906 }
9907 let half_w = sp.line_width
9908 * 0.5
9909 * (ctm.a * ctm.a + ctm.b * ctm.b)
9910 .sqrt()
9911 .max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt());
9912 dev_bbox.x_min -= half_w;
9913 dev_bbox.y_min -= half_w;
9914 dev_bbox.x_max += half_w;
9915 dev_bbox.y_max += half_w;
9916 Some(dev_bbox)
9917 } else {
9918 path_full_bbox(¶ms.path)
9919 }
9920}
9921
9922fn pattern_fill_y_bbox(params: &stet_graphics::device::PatternFillParams) -> Option<YBBox> {
9924 let bbox = pattern_fill_full_bbox(params)?;
9925 Some(YBBox {
9926 y_min: bbox.y_min,
9927 y_max: bbox.y_max,
9928 })
9929}
9930
9931fn image_full_bbox(params: &ImageParams) -> Option<BBox2D> {
9933 let m = ¶ms.ctm;
9934 let im = ¶ms.image_matrix;
9935 let im_inv = im.invert()?;
9936 let combined = m.concat(&im_inv);
9937 let w = params.width as f64;
9939 let h = params.height as f64;
9940 let corners = [
9941 combined.transform_point(0.0, 0.0),
9942 combined.transform_point(w, 0.0),
9943 combined.transform_point(0.0, h),
9944 combined.transform_point(w, h),
9945 ];
9946 let mut x_min = f64::INFINITY;
9947 let mut x_max = f64::NEG_INFINITY;
9948 let mut y_min = f64::INFINITY;
9949 let mut y_max = f64::NEG_INFINITY;
9950 for (x, y) in &corners {
9951 x_min = x_min.min(*x);
9952 x_max = x_max.max(*x);
9953 y_min = y_min.min(*y);
9954 y_max = y_max.max(*y);
9955 }
9956 Some(BBox2D {
9957 x_min,
9958 y_min,
9959 x_max,
9960 y_max,
9961 })
9962}
9963
9964fn shading_full_bbox(bbox: &Option<[f64; 4]>, ctm: &Matrix) -> Option<BBox2D> {
9966 if let Some(bbox) = bbox {
9967 let corners = [
9968 ctm.transform_point(bbox[0], bbox[1]),
9969 ctm.transform_point(bbox[2], bbox[1]),
9970 ctm.transform_point(bbox[0], bbox[3]),
9971 ctm.transform_point(bbox[2], bbox[3]),
9972 ];
9973 let mut x_min = f64::INFINITY;
9974 let mut x_max = f64::NEG_INFINITY;
9975 let mut y_min = f64::INFINITY;
9976 let mut y_max = f64::NEG_INFINITY;
9977 for (x, y) in &corners {
9978 x_min = x_min.min(*x);
9979 x_max = x_max.max(*x);
9980 y_min = y_min.min(*y);
9981 y_max = y_max.max(*y);
9982 }
9983 Some(BBox2D {
9984 x_min,
9985 y_min,
9986 x_max,
9987 y_max,
9988 })
9989 } else {
9990 Some(BBox2D {
9991 x_min: 0.0,
9992 y_min: 0.0,
9993 x_max: 1e9,
9994 y_max: 1e9,
9995 })
9996 }
9997}
9998
9999fn build_viewport_epochs(list: &DisplayList, bboxes: &[Option<BBox2D>]) -> Vec<ViewportEpoch> {
10001 let elements = list.elements();
10002 let mut epochs = Vec::new();
10003 let mut epoch_start = 0;
10004 let mut x_min = f64::INFINITY;
10005 let mut x_max = f64::NEG_INFINITY;
10006 let mut y_min = f64::INFINITY;
10007 let mut y_max = f64::NEG_INFINITY;
10008 let mut has_erase = false;
10009
10010 for (i, element) in elements.iter().enumerate() {
10011 if matches!(element, DisplayElement::InitClip) && i > epoch_start {
10012 epochs.push(ViewportEpoch {
10013 start_idx: epoch_start,
10014 end_idx: i,
10015 paint_bbox: if x_min <= x_max {
10016 Some(BBox2D {
10017 x_min,
10018 y_min,
10019 x_max,
10020 y_max,
10021 })
10022 } else {
10023 None
10024 },
10025 has_erase_page: has_erase,
10026 });
10027 epoch_start = i;
10028 x_min = f64::INFINITY;
10029 x_max = f64::NEG_INFINITY;
10030 y_min = f64::INFINITY;
10031 y_max = f64::NEG_INFINITY;
10032 has_erase = false;
10033 }
10034 if matches!(element, DisplayElement::ErasePage) {
10035 has_erase = true;
10036 }
10037 if let Some(ref bbox) = bboxes[i] {
10038 x_min = x_min.min(bbox.x_min);
10039 x_max = x_max.max(bbox.x_max);
10040 y_min = y_min.min(bbox.y_min);
10041 y_max = y_max.max(bbox.y_max);
10042 }
10043 }
10044 if epoch_start < elements.len() {
10045 epochs.push(ViewportEpoch {
10046 start_idx: epoch_start,
10047 end_idx: elements.len(),
10048 paint_bbox: if x_min <= x_max {
10049 Some(BBox2D {
10050 x_min,
10051 y_min,
10052 x_max,
10053 y_max,
10054 })
10055 } else {
10056 None
10057 },
10058 has_erase_page: has_erase,
10059 });
10060 }
10061 epochs
10062}
10063
10064struct ViewportEpoch {
10066 start_idx: usize,
10067 end_idx: usize,
10068 paint_bbox: Option<BBox2D>,
10069 has_erase_page: bool,
10070}
10071
10072pub struct PreparedDisplayList {
10078 bboxes: Vec<Option<BBox2D>>,
10079 epochs: Vec<ViewportEpoch>,
10080 clip_seen: HashSet<u64>,
10081}
10082
10083pub fn prepare_display_list(list: &DisplayList) -> PreparedDisplayList {
10088 let bboxes = precompute_full_bboxes(list, 72.0);
10089 let epochs = build_viewport_epochs(list, &bboxes);
10090 let clip_seen = precompute_clip_seen(list);
10091 PreparedDisplayList {
10092 bboxes,
10093 epochs,
10094 clip_seen,
10095 }
10096}
10097
10098struct PreprocessedImage {
10103 data: Vec<u8>,
10105 width: u32,
10107 height: u32,
10108 adj_sx: f32,
10112 adj_ky: f32,
10113 adj_kx: f32,
10114 adj_sy: f32,
10115 quality: stet_tiny_skia::FilterQuality,
10117}
10118
10119pub struct ImageCache {
10125 entries: Vec<Option<Vec<u8>>>,
10127}
10128
10129impl ImageCache {
10130 pub fn build(list: &DisplayList, icc: Option<&IccCache>) -> Self {
10132 let entries = list
10133 .elements()
10134 .iter()
10135 .map(|elem| {
10136 if let DisplayElement::Image {
10137 sample_data,
10138 params,
10139 } = elem
10140 {
10141 if params.width == 0 || params.height == 0 {
10142 return None;
10143 }
10144 let mut rgba = samples_to_rgba(sample_data, params, icc, false);
10145 if params.mask_color.is_some() {
10146 apply_mask_color_rgba(&mut rgba, sample_data, params);
10147 }
10148 Some(rgba)
10149 } else {
10150 None
10151 }
10152 })
10153 .collect();
10154 Self { entries }
10155 }
10156
10157 pub fn get(&self, index: usize) -> Option<&[u8]> {
10159 self.entries.get(index).and_then(|e| e.as_deref())
10160 }
10161}
10162
10163fn preprocess_images_for_bands(
10168 list: &DisplayList,
10169 icc: Option<&IccCache>,
10170) -> Vec<Option<PreprocessedImage>> {
10171 list.elements()
10172 .iter()
10173 .map(|elem| {
10174 let DisplayElement::Image {
10175 sample_data,
10176 params,
10177 } = elem
10178 else {
10179 return None;
10180 };
10181 let iw = params.width;
10182 let ih = params.height;
10183 if iw == 0 || ih == 0 {
10184 return None;
10185 }
10186 if params.overprint {
10188 return None;
10189 }
10190
10191 let mut rgba = samples_to_rgba(sample_data, params, icc, false);
10193 if params.mask_color.is_some() {
10194 apply_mask_color_rgba(&mut rgba, sample_data, params);
10195 }
10196
10197 let image_inv = params.image_matrix.invert()?;
10199 let combined = params.ctm.concat(&image_inv);
10200 let base_transform = enforce_min_image_size(to_transform(&combined), iw, ih);
10201
10202 let (data, width, height, adj_t) =
10204 match prescale_image(&rgba, iw, ih, base_transform, params.interpolate) {
10205 Some((d, w, h, t)) => {
10206 drop(rgba); (d, w, h, t)
10208 }
10209 None => (rgba, iw, ih, base_transform),
10210 };
10211
10212 let quality = image_filter_quality(adj_t, params.interpolate);
10213
10214 Some(PreprocessedImage {
10215 data,
10216 width,
10217 height,
10218 adj_sx: adj_t.sx,
10219 adj_ky: adj_t.ky,
10220 adj_kx: adj_t.kx,
10221 adj_sy: adj_t.sy,
10222 quality,
10223 })
10224 })
10225 .collect()
10226}
10227
10228#[allow(clippy::too_many_arguments)]
10234pub fn render_region_prepared(
10235 list: &DisplayList,
10236 prepared: &PreparedDisplayList,
10237 vp_x: f64,
10238 vp_y: f64,
10239 vp_w: f64,
10240 vp_h: f64,
10241 pixel_w: u32,
10242 pixel_h: u32,
10243 dpi: f64,
10244 icc: Option<&IccCache>,
10245 image_cache: Option<&ImageCache>,
10246 no_aa: bool,
10247) -> Vec<u8> {
10248 if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
10249 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10250 }
10251
10252 let layer_set = LayerSet::new();
10253 let scale_x = pixel_w as f64 / vp_w;
10254 let scale_y = pixel_h as f64 / vp_h;
10255 let effective_dpi = dpi * scale_x;
10256
10257 const OVERLAP: u32 = 6;
10268 let render_h = pixel_h + 2 * OVERLAP;
10269 let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create viewport pixmap");
10270 pixmap.fill(Color::TRANSPARENT);
10272
10273 let cmyk_buf = if has_overprint_elements(list)
10274 || list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
10275 || has_cmyk_group(list)
10276 {
10277 Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
10278 } else {
10279 None
10280 };
10281
10282 let mut state = BandState {
10283 clip_region: None,
10284 spare_mask: None,
10285 clip_mask_cache: HashMap::new(),
10286 clip_mask_seen: prepared.clip_seen.clone(),
10287 mask_pool: Vec::new(),
10288 cmyk_buffer: cmyk_buf,
10289 op_bg_snapshot: None,
10290 op_touched: None,
10291 spot_mask: None,
10292 };
10293
10294 let elements = list.elements();
10295 let vp_x_f = vp_x as f32;
10296 let vp_y_f = vp_y as f32;
10297 let sx = scale_x as f32;
10298 let sy = scale_y as f32;
10299 let vp_x_max = vp_x + vp_w;
10300 let vp_y_max = vp_y + vp_h;
10301
10302 for epoch in &prepared.epochs {
10303 if !epoch.has_erase_page {
10304 match epoch.paint_bbox {
10305 Some(ref pb)
10306 if pb.x_max <= vp_x
10307 || pb.x_min >= vp_x_max
10308 || pb.y_max <= vp_y
10309 || pb.y_min >= vp_y_max =>
10310 {
10311 continue;
10312 }
10313 None => continue,
10314 _ => {}
10315 }
10316 }
10317
10318 #[allow(clippy::needless_range_loop)]
10319 for i in epoch.start_idx..epoch.end_idx {
10320 let force_process = matches!(
10323 &elements[i],
10324 DisplayElement::OcgGroup { elements: inner, .. }
10325 if contains_clip_op(inner)
10326 );
10327 if !force_process
10328 && let Some(ref bbox) = prepared.bboxes[i]
10329 && (bbox.x_max <= vp_x
10330 || bbox.x_min >= vp_x_max
10331 || bbox.y_max <= vp_y
10332 || bbox.y_min >= vp_y_max)
10333 {
10334 continue;
10335 }
10336 let ctx = RenderContext {
10337 vp_x: vp_x_f,
10338 vp_y: vp_y_f,
10339 scale_x: sx,
10340 scale_y: sy,
10341 out_w: pixel_w,
10342 out_h: render_h,
10343 effective_dpi,
10344 icc,
10345 image_cache,
10346 preprocessed: None,
10347 elem_idx: i,
10348 no_aa,
10349 opm_zero_transparent: false,
10350 knockout_painter_pass: KnockoutPainterPass::None,
10351 parent_group_isolated: false,
10352 alpha_extraction_pass: false,
10353 layer_set: &layer_set,
10354 };
10355 render_element(&mut pixmap, &mut state, &elements[i], &ctx);
10356 }
10357 }
10358
10359 composite_onto_white(pixmap.data_mut());
10361 let row_bytes = pixel_w as usize * 4;
10363 let end = pixel_h as usize * row_bytes;
10364 pixmap.data()[..end].to_vec()
10365}
10366
10367pub fn viewport_band_count(pixel_w: u32, pixel_h: u32) -> (u32, u32) {
10372 let band_h = select_band_height(pixel_w, pixel_h);
10373 let num_bands = if band_h >= pixel_h {
10374 1
10375 } else {
10376 pixel_h.div_ceil(band_h)
10377 };
10378 (num_bands, band_h)
10379}
10380
10381#[allow(clippy::too_many_arguments)]
10390pub fn render_region_single_band(
10391 list: &DisplayList,
10392 prepared: &PreparedDisplayList,
10393 vp_x: f64,
10394 vp_y: f64,
10395 vp_w: f64,
10396 vp_h: f64,
10397 pixel_w: u32,
10398 pixel_h: u32,
10399 band_idx: u32,
10400 band_h: u32,
10401 num_bands: u32,
10402 dpi: f64,
10403 icc: Option<&IccCache>,
10404 image_cache: Option<&ImageCache>,
10405 no_aa: bool,
10406) -> Vec<u8> {
10407 if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
10408 let actual_h = if band_idx < num_bands - 1 {
10409 band_h
10410 } else {
10411 pixel_h - band_idx * band_h
10412 };
10413 return vec![0xFF; pixel_w as usize * actual_h as usize * 4];
10414 }
10415
10416 let layer_set = LayerSet::new();
10417 let scale_x = pixel_w as f64 / vp_w;
10418 let scale_y = pixel_h as f64 / vp_h;
10419 let effective_dpi = dpi * scale_x;
10420
10421 let out_y_start = band_idx * band_h;
10423 let actual_h = if band_idx < num_bands - 1 {
10424 band_h
10425 } else {
10426 pixel_h - out_y_start
10427 };
10428
10429 const OVERLAP: u32 = 6;
10440 let render_y_start = out_y_start.saturating_sub(OVERLAP);
10441 let render_y_end = (out_y_start + actual_h + OVERLAP).min(pixel_h);
10442 let render_h = band_h + 2 * OVERLAP;
10443 let overlap_top = out_y_start - render_y_start;
10444
10445 let src_y_min = vp_y + render_y_start as f64 / scale_y;
10447 let src_y_max = vp_y + render_y_end as f64 / scale_y;
10448
10449 let band_vp_y = vp_y + render_y_start as f64 / scale_y;
10451
10452 let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create band pixmap");
10453 pixmap.fill(Color::TRANSPARENT);
10454
10455 let cmyk_buf = if has_overprint_elements(list)
10456 || list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
10457 || has_cmyk_group(list)
10458 {
10459 Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
10460 } else {
10461 None
10462 };
10463
10464 let mut state = BandState {
10465 clip_region: None,
10466 spare_mask: None,
10467 clip_mask_cache: HashMap::new(),
10468 clip_mask_seen: prepared.clip_seen.clone(),
10469 mask_pool: Vec::new(),
10470 cmyk_buffer: cmyk_buf,
10471 op_bg_snapshot: None,
10472 op_touched: None,
10473 spot_mask: None,
10474 };
10475
10476 let elements = list.elements();
10477 let vp_x_f = vp_x as f32;
10478 let band_vp_y_f = band_vp_y as f32;
10479 let sx = scale_x as f32;
10480 let sy = scale_y as f32;
10481 let vp_x_max = vp_x + vp_w;
10482
10483 for epoch in &prepared.epochs {
10484 if !epoch.has_erase_page {
10485 match epoch.paint_bbox {
10486 Some(ref pb)
10487 if pb.x_max <= vp_x
10488 || pb.x_min >= vp_x_max
10489 || pb.y_max <= src_y_min
10490 || pb.y_min >= src_y_max =>
10491 {
10492 continue;
10493 }
10494 None => continue,
10495 _ => {}
10496 }
10497 }
10498
10499 #[allow(clippy::needless_range_loop)]
10500 for i in epoch.start_idx..epoch.end_idx {
10501 let force_process = matches!(
10505 &elements[i],
10506 DisplayElement::OcgGroup { elements: inner, .. }
10507 if contains_clip_op(inner)
10508 );
10509 if !force_process
10510 && let Some(ref bbox) = prepared.bboxes[i]
10511 && (bbox.x_max <= vp_x
10512 || bbox.x_min >= vp_x_max
10513 || bbox.y_max <= src_y_min
10514 || bbox.y_min >= src_y_max)
10515 {
10516 continue;
10517 }
10518 let ctx = RenderContext {
10519 vp_x: vp_x_f,
10520 vp_y: band_vp_y_f,
10521 scale_x: sx,
10522 scale_y: sy,
10523 out_w: pixel_w,
10524 out_h: render_h,
10525 effective_dpi,
10526 icc,
10527 image_cache,
10528 preprocessed: None,
10529 elem_idx: i,
10530 no_aa,
10531 opm_zero_transparent: false,
10532 knockout_painter_pass: KnockoutPainterPass::None,
10533 parent_group_isolated: false,
10534 alpha_extraction_pass: false,
10535 layer_set: &layer_set,
10536 };
10537 render_element(&mut pixmap, &mut state, &elements[i], &ctx);
10538 }
10539 }
10540
10541 composite_onto_white(pixmap.data_mut());
10543
10544 let row_bytes = pixel_w as usize * 4;
10546 let start = overlap_top as usize * row_bytes;
10547 let end = start + actual_h as usize * row_bytes;
10548 pixmap.data()[start..end].to_vec()
10549}
10550
10551#[allow(clippy::too_many_arguments)]
10560pub fn render_region_prepared_parallel(
10561 list: &DisplayList,
10562 prepared: &PreparedDisplayList,
10563 vp_x: f64,
10564 vp_y: f64,
10565 vp_w: f64,
10566 vp_h: f64,
10567 pixel_w: u32,
10568 pixel_h: u32,
10569 dpi: f64,
10570 icc: Option<&IccCache>,
10571 image_cache: Option<&ImageCache>,
10572 no_aa: bool,
10573) -> Vec<u8> {
10574 let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
10575
10576 if num_bands <= 1 {
10577 return render_region_prepared(
10579 list,
10580 prepared,
10581 vp_x,
10582 vp_y,
10583 vp_w,
10584 vp_h,
10585 pixel_w,
10586 pixel_h,
10587 dpi,
10588 icc,
10589 image_cache,
10590 no_aa,
10591 );
10592 }
10593
10594 let render_band = |band_idx: u32| -> Vec<u8> {
10595 render_region_single_band(
10596 list,
10597 prepared,
10598 vp_x,
10599 vp_y,
10600 vp_w,
10601 vp_h,
10602 pixel_w,
10603 pixel_h,
10604 band_idx,
10605 band_h,
10606 num_bands,
10607 dpi,
10608 icc,
10609 image_cache,
10610 no_aa,
10611 )
10612 };
10613
10614 let row_bytes = pixel_w as usize * 4;
10615 let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
10616
10617 #[cfg(feature = "parallel")]
10618 {
10619 let chunk_size = rayon::current_num_threads().max(1);
10620
10621 for chunk_start in (0..num_bands).step_by(chunk_size) {
10622 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
10623
10624 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
10625 .into_par_iter()
10626 .map(&render_band)
10627 .collect();
10628
10629 for (i, band_data) in rendered.iter().enumerate() {
10630 let band_idx = chunk_start + i as u32;
10631 let y_start = (band_idx * band_h) as usize;
10632 let dest_start = y_start * row_bytes;
10633 let len = band_data.len();
10634 result[dest_start..dest_start + len].copy_from_slice(band_data);
10635 }
10636 }
10637 }
10638 #[cfg(not(feature = "parallel"))]
10639 {
10640 for band_idx in 0..num_bands {
10641 let band_data = render_band(band_idx);
10642 let y_start = (band_idx * band_h) as usize;
10643 let dest_start = y_start * row_bytes;
10644 let len = band_data.len();
10645 result[dest_start..dest_start + len].copy_from_slice(&band_data);
10646 }
10647 }
10648
10649 result
10650}
10651
10652#[allow(clippy::too_many_arguments)]
10657pub fn render_region_prepared_parallel_with_progress(
10658 list: &DisplayList,
10659 prepared: &PreparedDisplayList,
10660 vp_x: f64,
10661 vp_y: f64,
10662 vp_w: f64,
10663 vp_h: f64,
10664 pixel_w: u32,
10665 pixel_h: u32,
10666 dpi: f64,
10667 icc: Option<&IccCache>,
10668 image_cache: Option<&ImageCache>,
10669 no_aa: bool,
10670 progress: &std::sync::atomic::AtomicU32,
10671) -> Vec<u8> {
10672 let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
10673
10674 if num_bands <= 1 {
10675 let result = render_region_prepared(
10676 list,
10677 prepared,
10678 vp_x,
10679 vp_y,
10680 vp_w,
10681 vp_h,
10682 pixel_w,
10683 pixel_h,
10684 dpi,
10685 icc,
10686 image_cache,
10687 no_aa,
10688 );
10689 progress.store(1, std::sync::atomic::Ordering::Relaxed);
10690 return result;
10691 }
10692
10693 let render_band = |band_idx: u32| -> Vec<u8> {
10694 render_region_single_band(
10695 list,
10696 prepared,
10697 vp_x,
10698 vp_y,
10699 vp_w,
10700 vp_h,
10701 pixel_w,
10702 pixel_h,
10703 band_idx,
10704 band_h,
10705 num_bands,
10706 dpi,
10707 icc,
10708 image_cache,
10709 no_aa,
10710 )
10711 };
10712
10713 let row_bytes = pixel_w as usize * 4;
10714 let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
10715
10716 #[cfg(feature = "parallel")]
10717 {
10718 let chunk_size = rayon::current_num_threads().max(1);
10719
10720 for chunk_start in (0..num_bands).step_by(chunk_size) {
10721 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
10722
10723 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
10724 .into_par_iter()
10725 .map(&render_band)
10726 .collect();
10727
10728 for (i, band_data) in rendered.iter().enumerate() {
10729 let band_idx = chunk_start + i as u32;
10730 let y_start = (band_idx * band_h) as usize;
10731 let dest_start = y_start * row_bytes;
10732 let len = band_data.len();
10733 result[dest_start..dest_start + len].copy_from_slice(band_data);
10734 }
10735 progress.store(chunk_end, std::sync::atomic::Ordering::Relaxed);
10736 }
10737 }
10738 #[cfg(not(feature = "parallel"))]
10739 {
10740 for band_idx in 0..num_bands {
10741 let band_data = render_band(band_idx);
10742 let y_start = (band_idx * band_h) as usize;
10743 let dest_start = y_start * row_bytes;
10744 let len = band_data.len();
10745 result[dest_start..dest_start + len].copy_from_slice(&band_data);
10746 progress.store(band_idx + 1, std::sync::atomic::Ordering::Relaxed);
10747 }
10748 }
10749
10750 result
10751}
10752
10753#[allow(clippy::too_many_arguments)]
10756pub fn render_region_prepared_parallel_cancellable(
10757 list: &DisplayList,
10758 prepared: &PreparedDisplayList,
10759 vp_x: f64,
10760 vp_y: f64,
10761 vp_w: f64,
10762 vp_h: f64,
10763 pixel_w: u32,
10764 pixel_h: u32,
10765 dpi: f64,
10766 icc: Option<&IccCache>,
10767 image_cache: Option<&ImageCache>,
10768 no_aa: bool,
10769 cancelled: &std::sync::atomic::AtomicBool,
10770) -> Option<Vec<u8>> {
10771 if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
10772 return None;
10773 }
10774
10775 let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
10776
10777 if num_bands <= 1 {
10778 return Some(render_region_prepared(
10779 list,
10780 prepared,
10781 vp_x,
10782 vp_y,
10783 vp_w,
10784 vp_h,
10785 pixel_w,
10786 pixel_h,
10787 dpi,
10788 icc,
10789 image_cache,
10790 no_aa,
10791 ));
10792 }
10793
10794 let render_band = |band_idx: u32| -> Vec<u8> {
10795 render_region_single_band(
10796 list,
10797 prepared,
10798 vp_x,
10799 vp_y,
10800 vp_w,
10801 vp_h,
10802 pixel_w,
10803 pixel_h,
10804 band_idx,
10805 band_h,
10806 num_bands,
10807 dpi,
10808 icc,
10809 image_cache,
10810 no_aa,
10811 )
10812 };
10813
10814 let row_bytes = pixel_w as usize * 4;
10815 let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
10816
10817 #[cfg(feature = "parallel")]
10818 {
10819 let chunk_size = rayon::current_num_threads().max(1);
10820
10821 for chunk_start in (0..num_bands).step_by(chunk_size) {
10822 if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
10823 return None;
10824 }
10825 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
10826
10827 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
10828 .into_par_iter()
10829 .map(&render_band)
10830 .collect();
10831
10832 for (i, band_data) in rendered.iter().enumerate() {
10833 let band_idx = chunk_start + i as u32;
10834 let y_start = (band_idx * band_h) as usize;
10835 let dest_start = y_start * row_bytes;
10836 let len = band_data.len();
10837 result[dest_start..dest_start + len].copy_from_slice(band_data);
10838 }
10839 }
10840 }
10841 #[cfg(not(feature = "parallel"))]
10842 {
10843 for band_idx in 0..num_bands {
10844 if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
10845 return None;
10846 }
10847 let band_data = render_band(band_idx);
10848 let y_start = (band_idx * band_h) as usize;
10849 let dest_start = y_start * row_bytes;
10850 let len = band_data.len();
10851 result[dest_start..dest_start + len].copy_from_slice(&band_data);
10852 }
10853 }
10854
10855 Some(result)
10856}
10857
10858pub fn render_to_rgba(
10866 list: &DisplayList,
10867 pixel_w: u32,
10868 pixel_h: u32,
10869 dpi: f64,
10870 icc: Option<&IccCache>,
10871 no_aa: bool,
10872) -> Vec<u8> {
10873 render_to_rgba_with_layers(list, pixel_w, pixel_h, dpi, icc, no_aa, &LayerSet::new())
10874}
10875
10876#[allow(clippy::too_many_arguments)]
10883pub fn render_to_rgba_with_layers(
10884 list: &DisplayList,
10885 pixel_w: u32,
10886 pixel_h: u32,
10887 dpi: f64,
10888 icc: Option<&IccCache>,
10889 no_aa: bool,
10890 layer_set: &LayerSet,
10891) -> Vec<u8> {
10892 if pixel_w == 0 || pixel_h == 0 {
10893 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10894 }
10895
10896 let mut icc_cache = match icc {
10897 Some(c) => c.clone(),
10898 None => IccCache::new(),
10899 };
10900 register_shading_icc_profiles(list, &mut icc_cache);
10903
10904 let mut sink = MemorySink {
10905 data: Vec::new(),
10906 width: 0,
10907 };
10908
10909 let band_h = select_band_height(pixel_w, pixel_h);
10910 if let Err(e) = render_banded_to_sink(
10911 pixel_w, pixel_h, band_h, dpi, list, &mut sink, &icc_cache, no_aa, layer_set,
10912 ) {
10913 eprintln!("render_to_rgba: banded render failed: {e}");
10914 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10915 }
10916
10917 sink.data
10918}
10919
10920pub fn render_to_rgba_viewport(
10933 list: &DisplayList,
10934 pixel_w: u32,
10935 pixel_h: u32,
10936 dpi: f64,
10937 icc: Option<&IccCache>,
10938 no_aa: bool,
10939) -> Vec<u8> {
10940 if pixel_w == 0 || pixel_h == 0 {
10941 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10942 }
10943
10944 let mut icc_cache = match icc {
10945 Some(c) => c.clone(),
10946 None => IccCache::new(),
10947 };
10948 register_shading_icc_profiles(list, &mut icc_cache);
10949
10950 let prepared = prepare_display_list(list);
10951 render_region_prepared_parallel(
10952 list,
10953 &prepared,
10954 0.0,
10955 0.0,
10956 pixel_w as f64,
10957 pixel_h as f64,
10958 pixel_w,
10959 pixel_h,
10960 dpi,
10961 Some(&icc_cache),
10962 None,
10963 no_aa,
10964 )
10965}
10966
10967fn debug_bbox_lines(list: &DisplayList, dpi: f64, depth: usize, out: &mut Vec<String>) {
10974 let y_bboxes = precompute_bboxes(list, dpi);
10975 let full_bboxes = precompute_full_bboxes(list, dpi);
10976 let elements = list.elements();
10977 let indent = " ".repeat(depth);
10978 for (i, elem) in elements.iter().enumerate() {
10979 let kind = match elem {
10980 DisplayElement::Fill { .. } => "Fill",
10981 DisplayElement::Stroke { .. } => "Stroke",
10982 DisplayElement::Image { .. } => "Image",
10983 DisplayElement::AxialShading { .. } => "AxialShading",
10984 DisplayElement::RadialShading { .. } => "RadialShading",
10985 DisplayElement::MeshShading { .. } => "MeshShading",
10986 DisplayElement::PatchShading { .. } => "PatchShading",
10987 DisplayElement::PatternFill { .. } => "PatternFill",
10988 DisplayElement::Group { .. } => "Group",
10989 DisplayElement::SoftMasked { .. } => "SoftMasked",
10990 DisplayElement::OcgGroup { .. } => "OcgGroup",
10991 DisplayElement::Clip { .. } => "Clip",
10992 DisplayElement::InitClip => "InitClip",
10993 DisplayElement::ErasePage => "ErasePage",
10994 DisplayElement::Text { .. } => "Text",
10995 _ => "Unknown",
10996 };
10997 let yb = &y_bboxes[i];
10998 let fb = &full_bboxes[i];
10999 let mut diff = false;
11000 if yb.is_some() != fb.is_some() {
11001 diff = true;
11002 }
11003 if let (Some(yb), Some(fb)) = (yb, fb)
11004 && ((yb.y_min - fb.y_min).abs() > 1e-9 || (yb.y_max - fb.y_max).abs() > 1e-9)
11005 {
11006 diff = true;
11007 }
11008 let yb_s = match yb {
11009 Some(b) => format!("Y[{:8.3}..{:8.3}]", b.y_min, b.y_max),
11010 None => "Y[None]".to_string(),
11011 };
11012 let fb_s = match fb {
11013 Some(b) => format!(
11014 "2D[x {:8.3}..{:8.3} y {:8.3}..{:8.3}]",
11015 b.x_min, b.x_max, b.y_min, b.y_max
11016 ),
11017 None => "2D[None]".to_string(),
11018 };
11019 out.push(format!(
11020 "{}{:4} {:15} {:30} {:55} {}",
11021 indent,
11022 i,
11023 kind,
11024 yb_s,
11025 fb_s,
11026 if diff { "DIFF" } else { "" }
11027 ));
11028 if let DisplayElement::Stroke { path, params } = elem {
11029 let rp = path_full_bbox(path);
11030 let m = ¶ms.ctm;
11031 out.push(format!(
11032 "{} ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] lw={:.4} miter={:.4} raw={}",
11033 indent,
11034 m.a,
11035 m.b,
11036 m.c,
11037 m.d,
11038 m.tx,
11039 m.ty,
11040 params.line_width,
11041 params.miter_limit,
11042 match rp {
11043 Some(b) => format!(
11044 "x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
11045 b.x_min, b.x_max, b.y_min, b.y_max
11046 ),
11047 None => "None".to_string(),
11048 }
11049 ));
11050 }
11051 if let DisplayElement::Clip { path, params } = elem {
11052 let rp = path_full_bbox(path);
11053 let m = ¶ms.ctm;
11054 out.push(format!(
11055 "{} clip ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] rule={:?} raw={}",
11056 indent,
11057 m.a,
11058 m.b,
11059 m.c,
11060 m.d,
11061 m.tx,
11062 m.ty,
11063 params.fill_rule,
11064 match rp {
11065 Some(b) => format!(
11066 "x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
11067 b.x_min, b.x_max, b.y_min, b.y_max
11068 ),
11069 None => "None".to_string(),
11070 }
11071 ));
11072 }
11073 if let DisplayElement::PatchShading { params } = elem {
11074 out.push(format!(
11075 "{} patch ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] bbox={:?} patches={}",
11076 indent,
11077 params.ctm.a,
11078 params.ctm.b,
11079 params.ctm.c,
11080 params.ctm.d,
11081 params.ctm.tx,
11082 params.ctm.ty,
11083 params.bbox,
11084 params.patches.len()
11085 ));
11086 if !params.patches.is_empty() {
11087 let patch = ¶ms.patches[0];
11088 let mut x_min = f64::INFINITY;
11090 let mut y_min = f64::INFINITY;
11091 let mut x_max = f64::NEG_INFINITY;
11092 let mut y_max = f64::NEG_INFINITY;
11093 for &(px, py) in &patch.points {
11094 let (dx, dy) = params.ctm.transform_point(px, py);
11095 x_min = x_min.min(dx);
11096 y_min = y_min.min(dy);
11097 x_max = x_max.max(dx);
11098 y_max = y_max.max(dy);
11099 }
11100 out.push(format!(
11101 "{} patch[0] pts={} dev x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
11102 indent,
11103 patch.points.len(),
11104 x_min,
11105 x_max,
11106 y_min,
11107 y_max
11108 ));
11109 }
11110 }
11111 if let DisplayElement::Group {
11112 elements: inner,
11113 params,
11114 } = elem
11115 {
11116 out.push(format!(
11117 "{} group bbox={:?} iso={} ko={} alpha={} bm={} cs={:?}",
11118 indent,
11119 params.bbox,
11120 params.isolated,
11121 params.knockout,
11122 params.alpha,
11123 params.blend_mode,
11124 params.color_space
11125 ));
11126 debug_bbox_lines(inner, dpi, depth + 1, out);
11127 }
11128 if let DisplayElement::SoftMasked {
11129 content, params, ..
11130 } = elem
11131 {
11132 out.push(format!(
11133 "{} softmasked bbox={:?}",
11134 indent, params.bbox
11135 ));
11136 debug_bbox_lines(content, dpi, depth + 1, out);
11137 }
11138 if let DisplayElement::OcgGroup {
11139 elements: inner,
11140 visibility,
11141 } = elem
11142 {
11143 out.push(format!(
11144 "{} ocg default_visible={}",
11145 indent,
11146 visibility.default_visible()
11147 ));
11148 debug_bbox_lines(inner, dpi, depth + 1, out);
11149 }
11150 }
11151}
11152
11153pub fn debug_bbox_comparison(list: &DisplayList, dpi: f64) -> Vec<String> {
11154 let mut out = Vec::new();
11155 debug_bbox_lines(list, dpi, 0, &mut out);
11156 out
11157}
11158
11159struct MemorySink {
11161 data: Vec<u8>,
11162 width: u32,
11163}
11164
11165impl stet_graphics::device::PageSink for MemorySink {
11166 fn begin_page(&mut self, width: u32, height: u32) -> Result<(), String> {
11167 self.width = width;
11168 self.data.reserve(width as usize * height as usize * 4);
11169 Ok(())
11170 }
11171
11172 fn write_rows(&mut self, rgba_rows: &[u8], _num_rows: u32) -> Result<(), String> {
11173 self.data.extend_from_slice(rgba_rows);
11174 Ok(())
11175 }
11176
11177 fn end_page(&mut self) -> Result<(), String> {
11178 Ok(())
11179 }
11180}
11181
11182#[allow(clippy::too_many_arguments)]
11191pub fn render_region(
11192 list: &DisplayList,
11193 vp_x: f64,
11194 vp_y: f64,
11195 vp_w: f64,
11196 vp_h: f64,
11197 pixel_w: u32,
11198 pixel_h: u32,
11199 dpi: f64,
11200 icc: Option<&IccCache>,
11201 image_cache: Option<&ImageCache>,
11202 no_aa: bool,
11203) -> Vec<u8> {
11204 if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
11205 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
11206 }
11207
11208 let layer_set = LayerSet::new();
11209 let scale_x = pixel_w as f64 / vp_w;
11210 let scale_y = pixel_h as f64 / vp_h;
11211 let effective_dpi = dpi * scale_x;
11213
11214 let bboxes = precompute_full_bboxes(list, effective_dpi);
11215 let epochs = build_viewport_epochs(list, &bboxes);
11216 let clip_seen = precompute_clip_seen(list);
11217
11218 const OVERLAP: u32 = 6;
11222 let render_h = pixel_h + 2 * OVERLAP;
11223
11224 let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create viewport pixmap");
11225 pixmap.fill(Color::TRANSPARENT);
11226
11227 let cmyk_buf = if has_overprint_elements(list)
11228 || list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
11229 || has_cmyk_group(list)
11230 {
11231 Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
11232 } else {
11233 None
11234 };
11235
11236 let mut state = BandState {
11237 clip_region: None,
11238 spare_mask: None,
11239 clip_mask_cache: HashMap::new(),
11240 clip_mask_seen: clip_seen,
11241 mask_pool: Vec::new(),
11242 cmyk_buffer: cmyk_buf,
11243 op_bg_snapshot: None,
11244 op_touched: None,
11245 spot_mask: None,
11246 };
11247
11248 let elements = list.elements();
11249 let vp_x_f = vp_x as f32;
11250 let vp_y_f = vp_y as f32;
11251 let sx = scale_x as f32;
11252 let sy = scale_y as f32;
11253 let vp_x_max = vp_x + vp_w;
11254 let vp_y_max = vp_y + vp_h;
11255
11256 for epoch in &epochs {
11257 if !epoch.has_erase_page {
11259 match epoch.paint_bbox {
11260 Some(ref pb)
11261 if pb.x_max <= vp_x
11262 || pb.x_min >= vp_x_max
11263 || pb.y_max <= vp_y
11264 || pb.y_min >= vp_y_max =>
11265 {
11266 continue;
11267 }
11268 None => continue,
11269 _ => {}
11270 }
11271 }
11272
11273 for i in epoch.start_idx..epoch.end_idx {
11274 let force_process = matches!(
11277 &elements[i],
11278 DisplayElement::OcgGroup { elements: inner, .. }
11279 if contains_clip_op(inner)
11280 );
11281 if !force_process
11283 && let Some(ref bbox) = bboxes[i]
11284 && (bbox.x_max <= vp_x
11285 || bbox.x_min >= vp_x_max
11286 || bbox.y_max <= vp_y
11287 || bbox.y_min >= vp_y_max)
11288 {
11289 continue;
11290 }
11291 let ctx = RenderContext {
11292 vp_x: vp_x_f,
11293 vp_y: vp_y_f,
11294 scale_x: sx,
11295 scale_y: sy,
11296 out_w: pixel_w,
11297 out_h: render_h,
11298 effective_dpi,
11299 icc,
11300 image_cache,
11301 preprocessed: None,
11302 elem_idx: i,
11303 no_aa,
11304 opm_zero_transparent: false,
11305 knockout_painter_pass: KnockoutPainterPass::None,
11306 parent_group_isolated: false,
11307 alpha_extraction_pass: false,
11308 layer_set: &layer_set,
11309 };
11310 render_element(&mut pixmap, &mut state, &elements[i], &ctx);
11311 }
11312 }
11313
11314 composite_onto_white(pixmap.data_mut());
11315 let row_bytes = pixel_w as usize * 4;
11317 let end = pixel_h as usize * row_bytes;
11318 pixmap.data()[..end].to_vec()
11319}
11320fn copy_backdrop_crop(
11322 parent: &Pixmap,
11323 crop_x: i32,
11324 crop_y: i32,
11325 crop_w: u32,
11326 crop_h: u32,
11327) -> Vec<u8> {
11328 let pw = parent.width() as usize;
11329 let src = parent.data();
11330 let cw = crop_w as usize;
11331 let ch = crop_h as usize;
11332 let cx = crop_x as usize;
11333 let cy = crop_y as usize;
11334 let mut backdrop = vec![0u8; cw * ch * 4];
11335 for row in 0..ch {
11336 let src_off = ((cy + row) * pw + cx) * 4;
11337 let dst_off = row * cw * 4;
11338 backdrop[dst_off..dst_off + cw * 4].copy_from_slice(&src[src_off..src_off + cw * 4]);
11339 }
11340 backdrop
11341}
11342fn clip_polygon_halfplane(
11347 poly: &[(f32, f32)],
11348 nx: f32,
11349 ny: f32,
11350 px: f32,
11351 py: f32,
11352) -> Vec<(f32, f32)> {
11353 if poly.is_empty() {
11354 return vec![];
11355 }
11356 let dot = |x: f32, y: f32| nx * (x - px) + ny * (y - py);
11357 let mut out = Vec::with_capacity(poly.len() + 1);
11358 let n = poly.len();
11359 for i in 0..n {
11360 let (ax, ay) = poly[i];
11361 let (bx, by) = poly[(i + 1) % n];
11362 let da = dot(ax, ay);
11363 let db = dot(bx, by);
11364 if da >= 0.0 {
11365 out.push((ax, ay));
11366 }
11367 if (da >= 0.0) != (db >= 0.0) {
11368 let t = da / (da - db);
11370 out.push((ax + t * (bx - ax), ay + t * (by - ay)));
11371 }
11372 }
11373 out
11374}
11375
11376#[allow(clippy::too_many_arguments)]
11378fn render_axial_shading(
11379 pixmap: &mut Pixmap,
11380 params: &AxialShadingParams,
11381 vp_x: f32,
11382 vp_y: f32,
11383 scale_x: f32,
11384 scale_y: f32,
11385 clip_mask: Option<&Mask>,
11386 no_aa: bool,
11387 cmyk_buf: Option<&mut [f32]>,
11388 icc: Option<&IccCache>,
11389) {
11390 let pw = pixmap.width();
11391 let ph = pixmap.height();
11392 if params.color_stops.is_empty() || pw == 0 || ph == 0 {
11393 return;
11394 }
11395
11396 let (mut rx_min, mut ry_min, mut rx_max, mut ry_max) = if let Some(bbox) = ¶ms.bbox {
11397 let corners = [
11398 params.ctm.transform_point(bbox[0], bbox[1]),
11399 params.ctm.transform_point(bbox[2], bbox[1]),
11400 params.ctm.transform_point(bbox[0], bbox[3]),
11401 params.ctm.transform_point(bbox[2], bbox[3]),
11402 ];
11403 let x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
11404 let y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
11405 let x_max = corners
11406 .iter()
11407 .map(|c| c.0)
11408 .fold(f64::NEG_INFINITY, f64::max);
11409 let y_max = corners
11410 .iter()
11411 .map(|c| c.1)
11412 .fold(f64::NEG_INFINITY, f64::max);
11413 (
11414 ((x_min as f32 - vp_x) * scale_x).max(0.0),
11415 ((y_min as f32 - vp_y) * scale_y).max(0.0),
11416 ((x_max as f32 - vp_x) * scale_x).min(pw as f32),
11417 ((y_max as f32 - vp_y) * scale_y).min(ph as f32),
11418 )
11419 } else {
11420 (0.0, 0.0, pw as f32, ph as f32)
11421 };
11422
11423 if rx_max <= rx_min || ry_max <= ry_min {
11424 return;
11425 }
11426
11427 let (dx0, dy0) = params.ctm.transform_point(params.x0, params.y0);
11429 let (dx1, dy1) = params.ctm.transform_point(params.x1, params.y1);
11430
11431 let needs_perpendicular_clip = (!params.extend_start || !params.extend_end) && {
11435 let axis_x = dx1 - dx0;
11436 let axis_y = dy1 - dy0;
11437 axis_x.abs() > 1e-6 && axis_y.abs() > 1e-6
11438 };
11439
11440 let bbox_is_rotated =
11443 params.bbox.is_some() && (params.ctm.b.abs() > 1e-10 || params.ctm.c.abs() > 1e-10);
11444
11445 if needs_perpendicular_clip {
11446 let stops = build_gradient_stops(¶ms.color_stops);
11449 if stops.is_empty() {
11450 return;
11451 }
11452 let start = stet_tiny_skia::Point::from_xy(params.x0 as f32, params.y0 as f32);
11453 let end = stet_tiny_skia::Point::from_xy(params.x1 as f32, params.y1 as f32);
11454 let gradient_transform =
11455 viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
11456 let Some(gradient) = stet_tiny_skia::LinearGradient::new(
11457 start,
11458 end,
11459 stops,
11460 stet_tiny_skia::SpreadMode::Pad,
11461 gradient_transform,
11462 ) else {
11463 return;
11464 };
11465 let paint = Paint {
11466 shader: gradient,
11467 anti_alias: !no_aa,
11468 ..Paint::default()
11469 };
11470
11471 let mut poly: Vec<(f32, f32)> = if bbox_is_rotated {
11473 let bbox = params.bbox.as_ref().unwrap();
11474 let corners = [
11475 params.ctm.transform_point(bbox[0], bbox[1]),
11476 params.ctm.transform_point(bbox[2], bbox[1]),
11477 params.ctm.transform_point(bbox[2], bbox[3]),
11478 params.ctm.transform_point(bbox[0], bbox[3]),
11479 ];
11480 corners
11481 .iter()
11482 .map(|(x, y)| ((*x as f32 - vp_x) * scale_x, (*y as f32 - vp_y) * scale_y))
11483 .collect()
11484 } else {
11485 vec![
11486 (rx_min, ry_min),
11487 (rx_max, ry_min),
11488 (rx_max, ry_max),
11489 (rx_min, ry_max),
11490 ]
11491 };
11492 let ax = (dx1 - dx0) as f32 * scale_x;
11493 let ay = (dy1 - dy0) as f32 * scale_y;
11494 if !params.extend_start {
11495 let px = (dx0 as f32 - vp_x) * scale_x;
11496 let py = (dy0 as f32 - vp_y) * scale_y;
11497 poly = clip_polygon_halfplane(&poly, ax, ay, px, py);
11498 }
11499 if !params.extend_end {
11500 let px = (dx1 as f32 - vp_x) * scale_x;
11501 let py = (dy1 as f32 - vp_y) * scale_y;
11502 poly = clip_polygon_halfplane(&poly, -ax, -ay, px, py);
11503 }
11504 if poly.len() >= 3 {
11505 let mut pb = PathBuilder::new();
11506 pb.move_to(poly[0].0, poly[0].1);
11507 for &(x, y) in &poly[1..] {
11508 pb.line_to(x, y);
11509 }
11510 pb.close();
11511 if let Some(path) = pb.finish() {
11512 pixmap.fill_path(
11513 &path,
11514 &paint,
11515 SkiaFillRule::Winding,
11516 Transform::identity(),
11517 clip_mask,
11518 );
11519 }
11520 }
11521 } else {
11522 if !params.extend_start || !params.extend_end {
11525 let axis_x = dx1 - dx0;
11526 let axis_y = dy1 - dy0;
11527 let gx0 = (dx0 as f32 - vp_x) * scale_x;
11528 let gy0 = (dy0 as f32 - vp_y) * scale_y;
11529 let gx1 = (dx1 as f32 - vp_x) * scale_x;
11530 let gy1 = (dy1 as f32 - vp_y) * scale_y;
11531
11532 if axis_x.abs() >= axis_y.abs() {
11533 if !params.extend_start {
11534 if axis_x >= 0.0 {
11535 rx_min = rx_min.max(gx0);
11536 } else {
11537 rx_max = rx_max.min(gx0);
11538 }
11539 }
11540 if !params.extend_end {
11541 if axis_x >= 0.0 {
11542 rx_max = rx_max.min(gx1);
11543 } else {
11544 rx_min = rx_min.max(gx1);
11545 }
11546 }
11547 } else {
11548 if !params.extend_start {
11549 if axis_y >= 0.0 {
11550 ry_min = ry_min.max(gy0);
11551 } else {
11552 ry_max = ry_max.min(gy0);
11553 }
11554 }
11555 if !params.extend_end {
11556 if axis_y >= 0.0 {
11557 ry_max = ry_max.min(gy1);
11558 } else {
11559 ry_min = ry_min.max(gy1);
11560 }
11561 }
11562 }
11563 if rx_max <= rx_min || ry_max <= ry_min {
11564 return;
11565 }
11566 }
11567
11568 let ax = params.x1 - params.x0;
11570 let ay = params.y1 - params.y0;
11571 let axis_sq = ax * ax + ay * ay;
11572 if axis_sq < 1e-20 {
11573 return;
11574 }
11575
11576 let pixel_dx = (dx1 - dx0) * scale_x as f64;
11580 let pixel_dy = (dy1 - dy0) * scale_y as f64;
11581 let pixel_axis_len = (pixel_dx * pixel_dx + pixel_dy * pixel_dy).sqrt();
11582 let lut_size = (pixel_axis_len as usize)
11583 .max(params.color_stops.len())
11584 .max(256)
11585 .min(16384);
11586 let lut = build_gradient_lut(¶ms.color_stops, lut_size);
11587
11588 let Some(inv) = params.ctm.invert() else {
11589 return;
11590 };
11591 let inv_sx = 1.0 / scale_x as f64;
11592 let inv_sy = 1.0 / scale_y as f64;
11593 let dev_origin_x = vp_x as f64;
11594 let dev_origin_y = vp_y as f64;
11595
11596 let sx_base = inv.a * dev_origin_x + inv.c * dev_origin_y + inv.tx;
11600 let sy_base = inv.b * dev_origin_x + inv.d * dev_origin_y + inv.ty;
11601 let dsx_dx = inv.a * inv_sx;
11602 let dsx_dy = inv.c * inv_sy;
11603 let dsy_dx = inv.b * inv_sx;
11604 let dsy_dy = inv.d * inv_sy;
11605
11606 let inv_axis_sq = 1.0 / axis_sq;
11608 let t_origin = ((sx_base - params.x0) * ax + (sy_base - params.y0) * ay) * inv_axis_sq;
11609 let dt_dx = (dsx_dx * ax + dsy_dx * ay) * inv_axis_sq;
11610 let dt_dy = (dsx_dy * ax + dsy_dy * ay) * inv_axis_sq;
11611
11612 let bbox_pixel_clip = if bbox_is_rotated {
11615 let bbox = params.bbox.as_ref().unwrap();
11616 let (bx0, bx1) = (bbox[0].min(bbox[2]), bbox[0].max(bbox[2]));
11617 let (by0, by1) = (bbox[1].min(bbox[3]), bbox[1].max(bbox[3]));
11618 Some((
11619 dsx_dx, dsx_dy, sx_base, dsy_dx, dsy_dy, sy_base, bx0, by0, bx1, by1,
11620 ))
11621 } else {
11622 None
11623 };
11624
11625 let ix_min = rx_min.floor() as u32;
11626 let ix_max = rx_max.ceil().min(pw as f32) as u32;
11627 let iy_min = ry_min.floor() as u32;
11628 let iy_max = ry_max.ceil().min(ph as f32) as u32;
11629
11630 let stride = pw as usize * 4;
11631 let data = pixmap.data_mut();
11632 let mask_data = clip_mask.map(|m| m.data());
11633 let alpha = (params.alpha.clamp(0.0, 1.0) * 255.0 + 0.5) as u16;
11634
11635 for py in iy_min..iy_max {
11636 let t_row = t_origin + dt_dy * py as f64;
11637 let row_offset = py as usize * stride;
11638
11639 let (ux_row, uy_row) =
11641 if let Some((_, dux_dy, ux_base, _, duy_dy, uy_base, ..)) = &bbox_pixel_clip {
11642 (ux_base + dux_dy * py as f64, uy_base + duy_dy * py as f64)
11643 } else {
11644 (0.0, 0.0)
11645 };
11646
11647 for px in ix_min..ix_max {
11648 if let Some(md) = mask_data {
11650 if md[py as usize * pw as usize + px as usize] == 0 {
11651 continue;
11652 }
11653 }
11654
11655 if let Some((dux_dx, _, _, duy_dx, _, _, bx0, by0, bx1, by1)) = &bbox_pixel_clip {
11657 let ux = ux_row + dux_dx * px as f64;
11658 let uy = uy_row + duy_dx * px as f64;
11659 if ux < *bx0 || ux > *bx1 || uy < *by0 || uy > *by1 {
11660 continue;
11661 }
11662 }
11663
11664 let t = t_row + dt_dx * px as f64;
11665 let t_clamped = t.clamp(0.0, 1.0);
11666 let idx = (t_clamped * (lut_size - 1) as f64 + 0.5) as usize;
11667 let [r, g, b, _] = lut[idx.min(lut_size - 1)];
11668
11669 let offset = row_offset + px as usize * 4;
11670 if alpha >= 255 {
11671 data[offset] = r;
11672 data[offset + 1] = g;
11673 data[offset + 2] = b;
11674 data[offset + 3] = 255;
11675 } else {
11676 let a = alpha as u16;
11678 let inv_a = 255 - a;
11679 data[offset] = ((r as u16 * a + data[offset] as u16 * inv_a + 127) / 255) as u8;
11680 data[offset + 1] =
11681 ((g as u16 * a + data[offset + 1] as u16 * inv_a + 127) / 255) as u8;
11682 data[offset + 2] =
11683 ((b as u16 * a + data[offset + 2] as u16 * inv_a + 127) / 255) as u8;
11684 data[offset + 3] = ((a + data[offset + 3] as u16 * inv_a / 255).min(255)) as u8;
11685 }
11686 }
11687 }
11688 }
11689
11690 if let Some(buf) = cmyk_buf {
11692 let pw = pixmap.width();
11693 let inv_sx = 1.0 / scale_x as f64;
11694 let inv_sy = 1.0 / scale_y as f64;
11695 let axis_x = params.x1 - params.x0;
11696 let axis_y = params.y1 - params.y0;
11697 let axis_len_sq = axis_x * axis_x + axis_y * axis_y;
11698 let Some(inv_ctm) = params.ctm.invert() else {
11699 return;
11700 };
11701
11702 let iy_min = ry_min.floor() as u32;
11703 let iy_max = ry_max.ceil().min(pixmap.height() as f32) as u32;
11704 let ix_min = rx_min.floor() as u32;
11705 let ix_max = rx_max.ceil().min(pw as f32) as u32;
11706
11707 for py in iy_min..iy_max {
11708 let dev_y = py as f64 * inv_sy + vp_y as f64;
11709 for px in ix_min..ix_max {
11710 let dev_x = px as f64 * inv_sx + vp_x as f64;
11711 let (ux, uy) = inv_ctm.transform_point(dev_x, dev_y);
11712 let t = if axis_len_sq > 1e-10 {
11713 ((ux - params.x0) * axis_x + (uy - params.y0) * axis_y) / axis_len_sq
11714 } else {
11715 0.0
11716 };
11717 if t < 0.0 && !params.extend_start {
11718 continue;
11719 }
11720 if t > 1.0 && !params.extend_end {
11721 continue;
11722 }
11723 let clamped = t.clamp(0.0, 1.0);
11724
11725 if let Some(mask) = clip_mask {
11726 let mi = py as usize * pw as usize + px as usize;
11727 if mask.data()[mi] == 0 {
11728 continue;
11729 }
11730 }
11731
11732 let color = interpolate_color_stops(¶ms.color_stops, clamped);
11733 let cmyk = interpolate_cmyk_from_stops(
11734 ¶ms.color_stops,
11735 ¶ms.color_space,
11736 clamped,
11737 &color,
11738 icc,
11739 );
11740 let ci = (py as usize * pw as usize + px as usize) * 4;
11741 if ci + 3 < buf.len() {
11742 if params.spot_tint_blend && params.overprint {
11743 let cur_c = buf[ci] as f64;
11776 let cur_m = buf[ci + 1] as f64;
11777 let cur_y = buf[ci + 2] as f64;
11778 let cur_k = buf[ci + 3] as f64;
11779 let cur_is_zero =
11780 cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
11781 let named = params.painted_channels;
11782 if cur_is_zero {
11783 if named & stet_graphics::device::CMYK_C != 0 {
11784 buf[ci] = cmyk.0 as f32;
11785 }
11786 if named & stet_graphics::device::CMYK_M != 0 {
11787 buf[ci + 1] = cmyk.1 as f32;
11788 }
11789 if named & stet_graphics::device::CMYK_Y != 0 {
11790 buf[ci + 2] = cmyk.2 as f32;
11791 }
11792 if named & stet_graphics::device::CMYK_K != 0 {
11793 buf[ci + 3] = cmyk.3 as f32;
11794 }
11795 } else {
11796 let new_c = if named & stet_graphics::device::CMYK_C != 0 {
11797 cmyk.0
11798 } else {
11799 cur_c
11800 };
11801 let new_m = if named & stet_graphics::device::CMYK_M != 0 {
11802 cmyk.1
11803 } else {
11804 cur_m
11805 };
11806 let new_y = if named & stet_graphics::device::CMYK_Y != 0 {
11807 cmyk.2
11808 } else {
11809 cur_y
11810 };
11811 let new_k = if named & stet_graphics::device::CMYK_K != 0 {
11812 cmyk.3
11813 } else {
11814 cur_k
11815 };
11816 buf[ci] = new_c as f32;
11817 buf[ci + 1] = new_m as f32;
11818 buf[ci + 2] = new_y as f32;
11819 buf[ci + 3] = new_k as f32;
11820 let (rv, gv, bv) = if let Some(icc_cache) = icc {
11821 icc_cache
11822 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
11823 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
11824 } else {
11825 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
11826 };
11827 let stride = pixmap.data().len() / pixmap.height() as usize;
11828 let offset = py as usize * stride + px as usize * 4;
11829 let data = pixmap.data_mut();
11830 data[offset] = (rv * 255.0).round().clamp(0.0, 255.0) as u8;
11831 data[offset + 1] = (gv * 255.0).round().clamp(0.0, 255.0) as u8;
11832 data[offset + 2] = (bv * 255.0).round().clamp(0.0, 255.0) as u8;
11833 }
11834 } else if params.overprint
11835 && params.painted_channels != stet_graphics::device::CMYK_ALL
11836 {
11837 if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
11838 buf[ci] = cmyk.0 as f32;
11839 }
11840 if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
11841 buf[ci + 1] = cmyk.1 as f32;
11842 }
11843 if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
11844 buf[ci + 2] = cmyk.2 as f32;
11845 }
11846 if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
11847 buf[ci + 3] = cmyk.3 as f32;
11848 }
11849 let c = buf[ci] as f64;
11851 let m = buf[ci + 1] as f64;
11852 let y = buf[ci + 2] as f64;
11853 let k = buf[ci + 3] as f64;
11854 let (rv, gv, bv) = if let Some(icc_cache) = icc {
11855 icc_cache
11856 .convert_cmyk_readonly(c, m, y, k)
11857 .unwrap_or_else(|| cmyk_to_rgb_plrm(c, m, y, k))
11858 } else {
11859 cmyk_to_rgb_plrm(c, m, y, k)
11860 };
11861 let stride = pixmap.data().len() / pixmap.height() as usize;
11862 let offset = py as usize * stride + px as usize * 4;
11863 let data = pixmap.data_mut();
11864 data[offset] = (rv * 255.0).round().clamp(0.0, 255.0) as u8;
11865 data[offset + 1] = (gv * 255.0).round().clamp(0.0, 255.0) as u8;
11866 data[offset + 2] = (bv * 255.0).round().clamp(0.0, 255.0) as u8;
11867 } else {
11868 buf[ci] = cmyk.0 as f32;
11885 buf[ci + 1] = cmyk.1 as f32;
11886 buf[ci + 2] = cmyk.2 as f32;
11887 buf[ci + 3] = cmyk.3 as f32;
11888 }
11889 }
11890 }
11891 }
11892 }
11893}
11894
11895#[allow(clippy::too_many_arguments)]
11897fn render_radial_shading(
11898 pixmap: &mut Pixmap,
11899 params: &RadialShadingParams,
11900 vp_x: f32,
11901 vp_y: f32,
11902 scale_x: f32,
11903 scale_y: f32,
11904 clip_mask: Option<&Mask>,
11905 _no_aa: bool,
11906 mut cmyk_buf: Option<&mut [f32]>,
11907 icc: Option<&IccCache>,
11908) {
11909 let pw = pixmap.width();
11910 let ph = pixmap.height();
11911 if params.color_stops.is_empty() || pw == 0 || ph == 0 {
11912 return;
11913 }
11914
11915 let Some(inv_ctm) = params.ctm.invert() else {
11916 return;
11917 };
11918
11919 let (px_min, py_min, px_max, py_max) = if let Some(bbox) = ¶ms.bbox {
11920 let corners = [
11921 params.ctm.transform_point(bbox[0], bbox[1]),
11922 params.ctm.transform_point(bbox[2], bbox[1]),
11923 params.ctm.transform_point(bbox[0], bbox[3]),
11924 params.ctm.transform_point(bbox[2], bbox[3]),
11925 ];
11926 let x_min = corners
11927 .iter()
11928 .map(|c| c.0 as f32)
11929 .fold(f32::INFINITY, f32::min);
11930 let y_min = corners
11931 .iter()
11932 .map(|c| c.1 as f32)
11933 .fold(f32::INFINITY, f32::min);
11934 let x_max = corners
11935 .iter()
11936 .map(|c| c.0 as f32)
11937 .fold(f32::NEG_INFINITY, f32::max);
11938 let y_max = corners
11939 .iter()
11940 .map(|c| c.1 as f32)
11941 .fold(f32::NEG_INFINITY, f32::max);
11942 (
11943 ((x_min - vp_x) * scale_x).max(0.0) as u32,
11944 ((y_min - vp_y) * scale_y).max(0.0) as u32,
11945 (((x_max - vp_x) * scale_x).ceil() as u32).min(pw),
11946 (((y_max - vp_y) * scale_y).ceil() as u32).min(ph),
11947 )
11948 } else {
11949 (0, 0, pw, ph)
11950 };
11951
11952 let inv_sx = 1.0 / scale_x as f64;
11953 let inv_sy = 1.0 / scale_y as f64;
11954
11955 let rotated_bbox = if let Some(bbox) = ¶ms.bbox {
11957 if params.ctm.b.abs() > 1e-10 || params.ctm.c.abs() > 1e-10 {
11958 let (bx0, bx1) = (bbox[0].min(bbox[2]), bbox[0].max(bbox[2]));
11959 let (by0, by1) = (bbox[1].min(bbox[3]), bbox[1].max(bbox[3]));
11960 Some((bx0, by0, bx1, by1))
11961 } else {
11962 None
11963 }
11964 } else {
11965 None
11966 };
11967
11968 let data = pixmap.data_mut();
11969 let stride = pw as usize * 4;
11970
11971 for py in py_min..py_max {
11972 let dev_y = py as f64 * inv_sy + vp_y as f64;
11973 for px in px_min..px_max {
11974 let dev_x = px as f64 * inv_sx + vp_x as f64;
11975 let (ux, uy) = inv_ctm.transform_point(dev_x, dev_y);
11976
11977 if let Some((bx0, by0, bx1, by1)) = rotated_bbox {
11979 if ux < bx0 || ux > bx1 || uy < by0 || uy > by1 {
11980 continue;
11981 }
11982 }
11983
11984 let t = solve_radial_t(
11985 ux,
11986 uy,
11987 params.x0,
11988 params.y0,
11989 params.r0,
11990 params.x1,
11991 params.y1,
11992 params.r1,
11993 params.extend_start,
11994 params.extend_end,
11995 );
11996 if let Some(t) = t {
11997 let clamped = t.clamp(0.0, 1.0);
11998 let color = interpolate_color_stops(¶ms.color_stops, clamped);
11999
12000 let clipped = clip_mask
12001 .is_some_and(|mask| mask.data()[py as usize * pw as usize + px as usize] == 0);
12002
12003 if clipped {
12004 continue;
12005 }
12006
12007 let cmyk = interpolate_cmyk_from_stops(
12016 ¶ms.color_stops,
12017 ¶ms.color_space,
12018 clamped,
12019 &color,
12020 icc,
12021 );
12022 let ci = (py as usize * pw as usize + px as usize) * 4;
12023 let buffer_clean = if let Some(ref buf) = cmyk_buf {
12024 if ci + 3 < buf.len() {
12025 buf[ci] == 0.0
12026 && buf[ci + 1] == 0.0
12027 && buf[ci + 2] == 0.0
12028 && buf[ci + 3] == 0.0
12029 } else {
12030 false
12031 }
12032 } else {
12033 false
12034 };
12035 let offset_for_check = py as usize * stride + px as usize * 4;
12036 let pixmap_has_colour = data[offset_for_check + 3] > 0
12037 && (data[offset_for_check] < 250
12038 || data[offset_for_check + 1] < 250
12039 || data[offset_for_check + 2] < 250);
12040 let use_multiplicative = params.overprint
12041 && params.painted_channels != stet_graphics::device::CMYK_ALL
12042 && buffer_clean
12043 && pixmap_has_colour;
12044
12045 if let Some(ref mut buf) = cmyk_buf
12047 && ci + 3 < buf.len()
12048 {
12049 if params.overprint
12050 && params.painted_channels != stet_graphics::device::CMYK_ALL
12051 {
12052 if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
12053 buf[ci] = cmyk.0 as f32;
12054 }
12055 if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
12056 buf[ci + 1] = cmyk.1 as f32;
12057 }
12058 if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
12059 buf[ci + 2] = cmyk.2 as f32;
12060 }
12061 if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
12062 buf[ci + 3] = cmyk.3 as f32;
12063 }
12064 } else {
12065 buf[ci] = cmyk.0 as f32;
12066 buf[ci + 1] = cmyk.1 as f32;
12067 buf[ci + 2] = cmyk.2 as f32;
12068 buf[ci + 3] = cmyk.3 as f32;
12069 }
12070 }
12071
12072 let offset = py as usize * stride + px as usize * 4;
12073 if use_multiplicative {
12074 let bg_r = data[offset] as f64 / 255.0;
12079 let bg_g = data[offset + 1] as f64 / 255.0;
12080 let bg_b = data[offset + 2] as f64 / 255.0;
12081 let over_r = if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
12082 1.0 - cmyk.0
12083 } else {
12084 1.0
12085 };
12086 let over_g = if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
12087 1.0 - cmyk.1
12088 } else {
12089 1.0
12090 };
12091 let over_b = if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
12092 1.0 - cmyk.2
12093 } else {
12094 1.0
12095 };
12096 let k_fac = if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
12097 1.0 - cmyk.3
12098 } else {
12099 1.0
12100 };
12101 data[offset] = ((bg_r * over_r * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
12102 data[offset + 1] =
12103 ((bg_g * over_g * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
12104 data[offset + 2] =
12105 ((bg_b * over_b * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
12106 data[offset + 3] = 255;
12107 } else {
12108 data[offset] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
12109 data[offset + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
12110 data[offset + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
12111 data[offset + 3] = 255;
12112
12113 if params.overprint
12128 && params.painted_channels != stet_graphics::device::CMYK_ALL
12129 && matches!(
12130 params.color_space,
12131 ShadingColorSpace::DeviceCMYK
12132 | ShadingColorSpace::Separation { .. }
12133 | ShadingColorSpace::DeviceN { .. }
12134 )
12135 && let Some(ref mut buf) = cmyk_buf
12136 && ci + 3 < buf.len()
12137 && let Some(icc_cache) = icc
12138 {
12139 let c = buf[ci] as f64;
12140 let m = buf[ci + 1] as f64;
12141 let y = buf[ci + 2] as f64;
12142 let k = buf[ci + 3] as f64;
12143 if let Some((r, g, b)) = icc_cache.convert_cmyk_readonly(c, m, y, k) {
12144 data[offset] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
12145 data[offset + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
12146 data[offset + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
12147 }
12148 }
12149 }
12150 }
12151 }
12152 }
12153}
12154#[allow(clippy::too_many_arguments)]
12159fn solve_radial_t(
12160 px: f64,
12161 py: f64,
12162 x0: f64,
12163 y0: f64,
12164 r0: f64,
12165 x1: f64,
12166 y1: f64,
12167 r1: f64,
12168 extend_start: bool,
12169 extend_end: bool,
12170) -> Option<f64> {
12171 let cdx = x1 - x0;
12174 let cdy = y1 - y0;
12175 let dr = r1 - r0;
12176
12177 let a = cdx * cdx + cdy * cdy - dr * dr;
12178 let dpx = px - x0;
12179 let dpy = py - y0;
12180 let b = -2.0 * (dpx * cdx + dpy * cdy + r0 * dr);
12181 let c = dpx * dpx + dpy * dpy - r0 * r0;
12182
12183 let in_domain = |t: f64| -> bool {
12185 (0.0..=1.0).contains(&t) || (t < 0.0 && extend_start) || (t > 1.0 && extend_end)
12186 };
12187
12188 if a.abs() < 1e-10 {
12189 if b.abs() < 1e-10 {
12191 return None;
12192 }
12193 let t = -c / b;
12194 let radius = r0 + t * dr;
12195 if radius >= 0.0 && in_domain(t) {
12196 return Some(t);
12197 }
12198 return None;
12199 }
12200
12201 let discriminant = b * b - 4.0 * a * c;
12202 if discriminant < 0.0 {
12203 return None;
12204 }
12205 let sqrt_d = discriminant.sqrt();
12206 let t1 = (-b + sqrt_d) / (2.0 * a);
12207 let t2 = (-b - sqrt_d) / (2.0 * a);
12208
12209 let mut best: Option<f64> = None;
12211 for t in [t1, t2] {
12212 let radius = r0 + t * dr;
12213 if radius >= 0.0 && in_domain(t) {
12214 best = Some(match best {
12215 Some(prev) => prev.max(t),
12216 None => t,
12217 });
12218 }
12219 }
12220 best
12221}
12222
12223#[allow(clippy::too_many_arguments)]
12225fn render_mesh_shading(
12226 pixmap: &mut Pixmap,
12227 params: &MeshShadingParams,
12228 vp_x: f32,
12229 vp_y: f32,
12230 scale_x: f32,
12231 scale_y: f32,
12232 clip_mask: Option<&Mask>,
12233 mut cmyk_buf: Option<&mut [f32]>,
12234 icc: Option<&IccCache>,
12235) {
12236 let pw = pixmap.width() as usize;
12237 let ph = pixmap.height() as usize;
12238 if pw == 0 || ph == 0 {
12239 return;
12240 }
12241 let data = pixmap.data_mut();
12242 let stride = pw * 4;
12243
12244 let lut = params.color_lut.as_deref();
12245
12246 for tri in ¶ms.triangles {
12247 let (dx0, dy0) = params.ctm.transform_point(tri.v0.x, tri.v0.y);
12248 let (dx1, dy1) = params.ctm.transform_point(tri.v1.x, tri.v1.y);
12249 let (dx2, dy2) = params.ctm.transform_point(tri.v2.x, tri.v2.y);
12250
12251 let x0 = (dx0 as f32 - vp_x) * scale_x;
12252 let y0 = (dy0 as f32 - vp_y) * scale_y;
12253 let x1 = (dx1 as f32 - vp_x) * scale_x;
12254 let y1 = (dy1 as f32 - vp_y) * scale_y;
12255 let x2 = (dx2 as f32 - vp_x) * scale_x;
12256 let y2 = (dy2 as f32 - vp_y) * scale_y;
12257
12258 let min_x = (x0.min(x1).min(x2).floor().max(0.0)) as usize;
12259 let max_x = (x0.max(x1).max(x2).ceil() as usize).min(pw);
12260 let min_y = (y0.min(y1).min(y2).floor().max(0.0)) as usize;
12261 let max_y = (y0.max(y1).max(y2).ceil() as usize).min(ph);
12262
12263 if min_x >= max_x || min_y >= max_y {
12264 continue;
12265 }
12266
12267 let x0 = x0 as f64;
12268 let y0 = y0 as f64;
12269 let x1 = x1 as f64;
12270 let y1 = y1 as f64;
12271 let x2 = x2 as f64;
12272 let y2 = y2 as f64;
12273 let denom = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2);
12278 if denom.abs() < 1e-10 {
12279 continue;
12280 }
12281 let (x1, y1, x2, y2) = if denom < 0.0 {
12282 (x2, y2, x1, y1)
12283 } else {
12284 (x1, y1, x2, y2)
12285 };
12286 let (v1_ref, v2_ref) = if denom < 0.0 {
12287 (&tri.v2, &tri.v1)
12288 } else {
12289 (&tri.v1, &tri.v2)
12290 };
12291 let denom = denom.abs();
12292 let inv_denom = 1.0 / denom;
12293
12294 for py in min_y..max_y {
12295 for px in min_x..max_x {
12296 let pxf = px as f64 + 0.5;
12297 let pyf = py as f64 + 0.5;
12298
12299 let w0 = ((y1 - y2) * (pxf - x2) + (x2 - x1) * (pyf - y2)) * inv_denom;
12300 let w1 = ((y2 - y0) * (pxf - x2) + (x0 - x2) * (pyf - y2)) * inv_denom;
12301 let w2 = 1.0 - w0 - w1;
12302
12303 if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 {
12304 continue;
12305 }
12306
12307 let clipped = clip_mask.is_some_and(|mask| mask.data()[py * pw + px] == 0);
12308
12309 let w0c = w0.max(0.0);
12310 let w1c = w1.max(0.0);
12311 let w2c = w2.max(0.0);
12312 let wsum = w0c + w1c + w2c;
12313 let w0n = w0c / wsum;
12314 let w1n = w1c / wsum;
12315 let w2n = w2c / wsum;
12316
12317 let (r, g, b) = if let Some(lut) = lut {
12320 let raw = w0n * tri.v0.raw_components[0]
12322 + w1n * v1_ref.raw_components[0]
12323 + w2n * v2_ref.raw_components[0];
12324 let raw = raw.clamp(0.0, 1.0);
12325 let fi = raw * (lut.len() - 1) as f64;
12327 let i0 = (fi as usize).min(lut.len().saturating_sub(2));
12328 let frac = fi - i0 as f64;
12329 let c0 = &lut[i0];
12330 let c1 = &lut[i0 + 1];
12331 (
12332 c0.r + frac * (c1.r - c0.r),
12333 c0.g + frac * (c1.g - c0.g),
12334 c0.b + frac * (c1.b - c0.b),
12335 )
12336 } else {
12337 (
12338 w0n * tri.v0.color.r + w1n * v1_ref.color.r + w2n * v2_ref.color.r,
12339 w0n * tri.v0.color.g + w1n * v1_ref.color.g + w2n * v2_ref.color.g,
12340 w0n * tri.v0.color.b + w1n * v1_ref.color.b + w2n * v2_ref.color.b,
12341 )
12342 };
12343
12344 if let Some(ref mut buf) = cmyk_buf {
12346 let ci = (py * pw + px) * 4;
12347 if ci + 3 < buf.len() {
12348 let cmyk = interpolate_cmyk_from_vertices(
12349 &tri.v0,
12350 v1_ref,
12351 v2_ref,
12352 w0n,
12353 w1n,
12354 w2n,
12355 ¶ms.color_space,
12356 r,
12357 g,
12358 b,
12359 icc,
12360 );
12361 if params.overprint
12362 && params.painted_channels != stet_graphics::device::CMYK_ALL
12363 {
12364 if !clipped {
12365 if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
12366 buf[ci] = cmyk.0 as f32;
12367 }
12368 if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
12369 buf[ci + 1] = cmyk.1 as f32;
12370 }
12371 if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
12372 buf[ci + 2] = cmyk.2 as f32;
12373 }
12374 if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
12375 buf[ci + 3] = cmyk.3 as f32;
12376 }
12377 }
12378 } else {
12379 buf[ci] = cmyk.0 as f32;
12380 buf[ci + 1] = cmyk.1 as f32;
12381 buf[ci + 2] = cmyk.2 as f32;
12382 buf[ci + 3] = cmyk.3 as f32;
12383 }
12384 }
12385 }
12386
12387 if clipped {
12388 continue;
12389 }
12390
12391 let offset = py * stride + px * 4;
12392 data[offset] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
12393 data[offset + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
12394 data[offset + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
12395 data[offset + 3] = 255;
12396 }
12397 }
12398 }
12399}
12400
12401#[allow(clippy::too_many_arguments)]
12403fn render_patch_shading(
12404 pixmap: &mut Pixmap,
12405 params: &PatchShadingParams,
12406 vp_x: f32,
12407 vp_y: f32,
12408 scale_x: f32,
12409 scale_y: f32,
12410 clip_mask: Option<&Mask>,
12411 cmyk_buf: Option<&mut [f32]>,
12412 icc: Option<&IccCache>,
12413) {
12414 let mut triangles = Vec::new();
12415 let scale = scale_x.max(scale_y) as f64;
12416 for patch in ¶ms.patches {
12417 if patch.points.len() >= 12 {
12418 let mut x_min = f64::INFINITY;
12420 let mut y_min = f64::INFINITY;
12421 let mut x_max = f64::NEG_INFINITY;
12422 let mut y_max = f64::NEG_INFINITY;
12423 for &(px, py) in &patch.points {
12424 let (dx, dy) = params.ctm.transform_point(px, py);
12425 x_min = x_min.min(dx);
12426 y_min = y_min.min(dy);
12427 x_max = x_max.max(dx);
12428 y_max = y_max.max(dy);
12429 }
12430 let extent = (x_max - x_min).max(y_max - y_min).abs() * scale;
12431 let n = (extent / 2.0).ceil().clamp(8.0, 64.0) as usize;
12433 let icc_profile_hash = match ¶ms.color_space {
12435 stet_graphics::device::ShadingColorSpace::ICCBased { profile_hash, .. } => {
12436 Some(profile_hash)
12437 }
12438 _ => None,
12439 };
12440 subdivide_patch_to_triangles(patch, &mut triangles, n, icc_profile_hash, icc);
12441 }
12442 }
12443 if !triangles.is_empty() {
12444 let mesh_params = MeshShadingParams {
12445 triangles,
12446 ctm: params.ctm,
12447 bbox: params.bbox,
12448 color_space: params.color_space.clone(),
12449 overprint: params.overprint,
12450 overprint_mode: params.overprint_mode,
12451 painted_channels: params.painted_channels,
12452 color_lut: params.color_lut.clone(),
12453 alpha: params.alpha,
12454 blend_mode: params.blend_mode,
12455 alpha_is_shape: params.alpha_is_shape,
12456 };
12457 render_mesh_shading(
12458 pixmap,
12459 &mesh_params,
12460 vp_x,
12461 vp_y,
12462 scale_x,
12463 scale_y,
12464 clip_mask,
12465 cmyk_buf,
12466 icc,
12467 );
12468 }
12469}
12470fn subdivide_patch_to_triangles(
12475 patch: &stet_graphics::device::ShadingPatch,
12476 triangles: &mut Vec<stet_graphics::device::ShadingTriangle>,
12477 n: usize,
12478 icc_profile_hash: Option<&stet_graphics::icc::ProfileHash>,
12479 icc_cache: Option<&IccCache>,
12480) {
12481 let mut grid: Vec<(f64, f64, DeviceColor, Vec<f64>)> = Vec::with_capacity((n + 1) * (n + 1));
12485 let use_tensor = patch.points.len() >= 16;
12486 let has_raw = !patch.raw_colors[0].is_empty();
12487 let use_icc_interp = has_raw && icc_profile_hash.is_some() && icc_cache.is_some();
12489
12490 for row in 0..=n {
12491 let v = row as f64 / n as f64;
12492 for col in 0..=n {
12493 let u = col as f64 / n as f64;
12494 let (x, y) = if use_tensor {
12495 eval_tensor_patch(patch, u, v)
12496 } else {
12497 eval_coons_patch(patch, u, v)
12498 };
12499 let raw = if has_raw {
12500 bilinear_raw(&patch.raw_colors, u, v)
12501 } else {
12502 vec![]
12503 };
12504 let color = if use_icc_interp {
12510 if let Some((r, g, b)) = icc_cache
12511 .unwrap()
12512 .convert_color_readonly(icc_profile_hash.unwrap(), &raw)
12513 {
12514 DeviceColor::from_rgb(r, g, b)
12515 } else {
12516 bilinear_color(&patch.colors, u, v)
12517 }
12518 } else {
12519 bilinear_color(&patch.colors, u, v)
12520 };
12521 grid.push((x, y, color, raw));
12522 }
12523 }
12524
12525 let cols = n + 1;
12527 for row in 0..n {
12528 for col in 0..n {
12529 let i00 = row * cols + col;
12530 let i10 = i00 + 1;
12531 let i01 = i00 + cols;
12532 let i11 = i01 + 1;
12533
12534 let (x00, y00, c00, r00) = &grid[i00];
12535 let (x10, y10, c10, r10) = &grid[i10];
12536 let (x01, y01, c01, r01) = &grid[i01];
12537 let (x11, y11, c11, r11) = &grid[i11];
12538
12539 use stet_graphics::device::ShadingVertex;
12540 triangles.push(stet_graphics::device::ShadingTriangle {
12541 v0: ShadingVertex {
12542 x: *x00,
12543 y: *y00,
12544 color: c00.clone(),
12545 raw_components: r00.clone(),
12546 },
12547 v1: ShadingVertex {
12548 x: *x10,
12549 y: *y10,
12550 color: c10.clone(),
12551 raw_components: r10.clone(),
12552 },
12553 v2: ShadingVertex {
12554 x: *x01,
12555 y: *y01,
12556 color: c01.clone(),
12557 raw_components: r01.clone(),
12558 },
12559 });
12560 triangles.push(stet_graphics::device::ShadingTriangle {
12561 v0: ShadingVertex {
12562 x: *x10,
12563 y: *y10,
12564 color: c10.clone(),
12565 raw_components: r10.clone(),
12566 },
12567 v1: ShadingVertex {
12568 x: *x11,
12569 y: *y11,
12570 color: c11.clone(),
12571 raw_components: r11.clone(),
12572 },
12573 v2: ShadingVertex {
12574 x: *x01,
12575 y: *y01,
12576 color: c01.clone(),
12577 raw_components: r01.clone(),
12578 },
12579 });
12580 }
12581 }
12582}
12583
12584fn eval_coons_patch(patch: &stet_graphics::device::ShadingPatch, u: f64, v: f64) -> (f64, f64) {
12587 let pts = &patch.points;
12588 if pts.len() < 12 {
12589 return (0.0, 0.0);
12590 }
12591
12592 let c0 = eval_cubic_bezier(pts[0], pts[1], pts[2], pts[3], u);
12597 let c2 = eval_cubic_bezier(pts[6], pts[7], pts[8], pts[9], 1.0 - u);
12598 let d0 = eval_cubic_bezier(pts[0], pts[11], pts[10], pts[9], v);
12599 let d1 = eval_cubic_bezier(pts[3], pts[4], pts[5], pts[6], v);
12600
12601 let p00 = pts[0];
12603 let p10 = pts[3];
12604 let p01 = pts[9];
12605 let p11 = pts[6];
12606 let bx = (1.0 - u) * (1.0 - v) * p00.0
12607 + u * (1.0 - v) * p10.0
12608 + (1.0 - u) * v * p01.0
12609 + u * v * p11.0;
12610 let by = (1.0 - u) * (1.0 - v) * p00.1
12611 + u * (1.0 - v) * p10.1
12612 + (1.0 - u) * v * p01.1
12613 + u * v * p11.1;
12614
12615 let x = (1.0 - v) * c0.0 + v * c2.0 + (1.0 - u) * d0.0 + u * d1.0 - bx;
12617 let y = (1.0 - v) * c0.1 + v * c2.1 + (1.0 - u) * d0.1 + u * d1.1 - by;
12618
12619 (x, y)
12620}
12621
12622fn eval_tensor_patch(patch: &stet_graphics::device::ShadingPatch, u: f64, v: f64) -> (f64, f64) {
12636 let pts = &patch.points;
12637
12638 let grid: [[usize; 4]; 4] = [[0, 1, 2, 3], [11, 12, 13, 4], [10, 15, 14, 5], [9, 8, 7, 6]];
12642
12643 let su = 1.0 - u;
12645 let bu = [su * su * su, 3.0 * su * su * u, 3.0 * su * u * u, u * u * u];
12646 let sv = 1.0 - v;
12647 let bv = [sv * sv * sv, 3.0 * sv * sv * v, 3.0 * sv * v * v, v * v * v];
12648
12649 let mut x = 0.0;
12650 let mut y = 0.0;
12651 for j in 0..4 {
12652 for i in 0..4 {
12653 let w = bu[i] * bv[j];
12654 let p = pts[grid[j][i]];
12655 x += w * p.0;
12656 y += w * p.1;
12657 }
12658 }
12659 (x, y)
12660}
12661
12662fn eval_cubic_bezier(
12664 p0: (f64, f64),
12665 p1: (f64, f64),
12666 p2: (f64, f64),
12667 p3: (f64, f64),
12668 t: f64,
12669) -> (f64, f64) {
12670 let s = 1.0 - t;
12671 let s2 = s * s;
12672 let t2 = t * t;
12673 let b0 = s2 * s;
12674 let b1 = 3.0 * s2 * t;
12675 let b2 = 3.0 * s * t2;
12676 let b3 = t2 * t;
12677 (
12678 b0 * p0.0 + b1 * p1.0 + b2 * p2.0 + b3 * p3.0,
12679 b0 * p0.1 + b1 * p1.1 + b2 * p2.1 + b3 * p3.1,
12680 )
12681}
12682
12683fn bilinear_color(colors: &[DeviceColor; 4], u: f64, v: f64) -> DeviceColor {
12685 let r = (1.0 - u) * (1.0 - v) * colors[0].r
12686 + u * (1.0 - v) * colors[1].r
12687 + (1.0 - u) * v * colors[3].r
12688 + u * v * colors[2].r;
12689 let g = (1.0 - u) * (1.0 - v) * colors[0].g
12690 + u * (1.0 - v) * colors[1].g
12691 + (1.0 - u) * v * colors[3].g
12692 + u * v * colors[2].g;
12693 let b = (1.0 - u) * (1.0 - v) * colors[0].b
12694 + u * (1.0 - v) * colors[1].b
12695 + (1.0 - u) * v * colors[3].b
12696 + u * v * colors[2].b;
12697 DeviceColor::from_rgb(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0))
12698}
12699
12700fn bilinear_raw(raw_colors: &[Vec<f64>; 4], u: f64, v: f64) -> Vec<f64> {
12702 let n = raw_colors[0].len();
12703 let mut result = vec![0.0; n];
12704 for i in 0..n {
12705 result[i] = (1.0 - u) * (1.0 - v) * raw_colors[0][i]
12706 + u * (1.0 - v) * raw_colors[1][i]
12707 + (1.0 - u) * v * raw_colors[3][i]
12708 + u * v * raw_colors[2][i];
12709 }
12710 result
12711}
12712
12713fn build_gradient_lut(stops: &[stet_graphics::device::ColorStop], size: usize) -> Vec<[u8; 4]> {
12719 let size = size.max(2);
12720 let mut lut = vec![[0u8; 4]; size];
12721 if stops.is_empty() {
12722 return lut;
12723 }
12724 let mut si = 0usize; let last = (size - 1) as f64;
12726 for i in 0..size {
12727 let t = i as f64 / last;
12728 while si + 1 < stops.len() && stops[si + 1].position < t {
12730 si += 1;
12731 }
12732 let (r, g, b) = if si + 1 >= stops.len() {
12733 let c = &stops[stops.len() - 1].color;
12734 (c.r, c.g, c.b)
12735 } else if t <= stops[si].position {
12736 let c = &stops[si].color;
12737 (c.r, c.g, c.b)
12738 } else {
12739 let t0 = stops[si].position;
12740 let t1 = stops[si + 1].position;
12741 let frac = if (t1 - t0).abs() < 1e-10 {
12742 0.0
12743 } else {
12744 (t - t0) / (t1 - t0)
12745 };
12746 let c0 = &stops[si].color;
12747 let c1 = &stops[si + 1].color;
12748 (
12749 c0.r + frac * (c1.r - c0.r),
12750 c0.g + frac * (c1.g - c0.g),
12751 c0.b + frac * (c1.b - c0.b),
12752 )
12753 };
12754 lut[i] = [
12755 (r * 255.0).round().clamp(0.0, 255.0) as u8,
12756 (g * 255.0).round().clamp(0.0, 255.0) as u8,
12757 (b * 255.0).round().clamp(0.0, 255.0) as u8,
12758 255,
12759 ];
12760 }
12761 lut
12762}
12763
12764fn build_gradient_stops(
12766 stops: &[stet_graphics::device::ColorStop],
12767) -> Vec<stet_tiny_skia::GradientStop> {
12768 let mut result = Vec::with_capacity(stops.len());
12769 for stop in stops {
12770 let r = (stop.color.r * 255.0).round().clamp(0.0, 255.0) as u8;
12771 let g = (stop.color.g * 255.0).round().clamp(0.0, 255.0) as u8;
12772 let b = (stop.color.b * 255.0).round().clamp(0.0, 255.0) as u8;
12773 result.push(stet_tiny_skia::GradientStop::new(
12774 stop.position as f32,
12775 Color::from_rgba8(r, g, b, 255),
12776 ));
12777 }
12778 result
12779}
12780
12781fn interpolate_color_stops(
12783 stops: &[stet_graphics::device::ColorStop],
12784 position: f64,
12785) -> DeviceColor {
12786 if stops.is_empty() {
12787 return DeviceColor::from_gray(0.0);
12788 }
12789 if stops.len() == 1 || position <= stops[0].position {
12790 return stops[0].color.clone();
12791 }
12792 if position >= stops.last().unwrap().position {
12793 return stops.last().unwrap().color.clone();
12794 }
12795
12796 for i in 1..stops.len() {
12798 if position <= stops[i].position {
12799 let t0 = stops[i - 1].position;
12800 let t1 = stops[i].position;
12801 let frac = if (t1 - t0).abs() < 1e-10 {
12802 0.0
12803 } else {
12804 (position - t0) / (t1 - t0)
12805 };
12806 let c0 = &stops[i - 1].color;
12807 let c1 = &stops[i].color;
12808 return DeviceColor::from_rgb(
12809 (c0.r + frac * (c1.r - c0.r)).clamp(0.0, 1.0),
12810 (c0.g + frac * (c1.g - c0.g)).clamp(0.0, 1.0),
12811 (c0.b + frac * (c1.b - c0.b)).clamp(0.0, 1.0),
12812 );
12813 }
12814 }
12815
12816 stops.last().unwrap().color.clone()
12817}
12818
12819fn interpolate_cmyk_from_stops(
12831 stops: &[stet_graphics::device::ColorStop],
12832 cs: &ShadingColorSpace,
12833 t: f64,
12834 color: &DeviceColor,
12835 icc: Option<&IccCache>,
12836) -> (f64, f64, f64, f64) {
12837 let rgb_to_cmyk = |c: &DeviceColor| -> (f64, f64, f64, f64) {
12838 if let Some(cmyk) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(c.r, c.g, c.b)) {
12839 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
12840 } else {
12841 (
12842 (1.0 - c.r).clamp(0.0, 1.0),
12843 (1.0 - c.g).clamp(0.0, 1.0),
12844 (1.0 - c.b).clamp(0.0, 1.0),
12845 0.0,
12846 )
12847 }
12848 };
12849
12850 match cs {
12851 ShadingColorSpace::DeviceCMYK
12855 | ShadingColorSpace::Separation { .. }
12856 | ShadingColorSpace::DeviceN { .. } => {
12857 if stops.len() == 1 {
12859 let rc = &stops[0].raw_components;
12860 if rc.len() >= 4 {
12861 return (rc[0], rc[1], rc[2], rc[3]);
12862 }
12863 }
12864 let mut lo = &stops[0];
12866 let mut hi = stops.last().unwrap();
12867 for i in 0..stops.len() - 1 {
12868 if stops[i + 1].position >= t {
12869 lo = &stops[i];
12870 hi = &stops[i + 1];
12871 break;
12872 }
12873 }
12874 let span = hi.position - lo.position;
12875 let frac = if span > 1e-10 {
12876 (t - lo.position) / span
12877 } else {
12878 0.0
12879 };
12880 let frac = frac.clamp(0.0, 1.0);
12881 if lo.raw_components.len() >= 4 && hi.raw_components.len() >= 4 {
12882 (
12883 lo.raw_components[0] + frac * (hi.raw_components[0] - lo.raw_components[0]),
12884 lo.raw_components[1] + frac * (hi.raw_components[1] - lo.raw_components[1]),
12885 lo.raw_components[2] + frac * (hi.raw_components[2] - lo.raw_components[2]),
12886 lo.raw_components[3] + frac * (hi.raw_components[3] - lo.raw_components[3]),
12887 )
12888 } else {
12889 rgb_to_cmyk(color)
12890 }
12891 }
12892 _ => rgb_to_cmyk(color),
12893 }
12894}
12895
12896#[allow(clippy::too_many_arguments)]
12902fn interpolate_cmyk_from_vertices(
12903 v0: &ShadingVertex,
12904 v1: &ShadingVertex,
12905 v2: &ShadingVertex,
12906 w0: f64,
12907 w1: f64,
12908 w2: f64,
12909 cs: &ShadingColorSpace,
12910 r: f64,
12911 g: f64,
12912 b: f64,
12913 icc: Option<&IccCache>,
12914) -> (f64, f64, f64, f64) {
12915 let rgb_to_cmyk = |r: f64, g: f64, b: f64| -> (f64, f64, f64, f64) {
12916 if let Some(cmyk) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(r, g, b)) {
12917 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
12918 } else {
12919 (
12920 (1.0 - r).clamp(0.0, 1.0),
12921 (1.0 - g).clamp(0.0, 1.0),
12922 (1.0 - b).clamp(0.0, 1.0),
12923 0.0,
12924 )
12925 }
12926 };
12927
12928 match cs {
12929 ShadingColorSpace::DeviceCMYK
12930 | ShadingColorSpace::Separation { .. }
12931 | ShadingColorSpace::DeviceN { .. } => {
12932 if v0.raw_components.len() >= 4
12933 && v1.raw_components.len() >= 4
12934 && v2.raw_components.len() >= 4
12935 {
12936 (
12937 w0 * v0.raw_components[0]
12938 + w1 * v1.raw_components[0]
12939 + w2 * v2.raw_components[0],
12940 w0 * v0.raw_components[1]
12941 + w1 * v1.raw_components[1]
12942 + w2 * v2.raw_components[1],
12943 w0 * v0.raw_components[2]
12944 + w1 * v1.raw_components[2]
12945 + w2 * v2.raw_components[2],
12946 w0 * v0.raw_components[3]
12947 + w1 * v1.raw_components[3]
12948 + w2 * v2.raw_components[3],
12949 )
12950 } else {
12951 rgb_to_cmyk(r, g, b)
12952 }
12953 }
12954 _ => rgb_to_cmyk(r, g, b),
12955 }
12956}
12957
12958#[cfg(test)]
12959mod tests {
12960 use super::*;
12961 use stet_graphics::color::DashPattern;
12962 use stet_graphics::device::{BgUcrState, HalftoneState, TransferState};
12963
12964 #[test]
12965 fn test_create_device() {
12966 let dev = SkiaDevice::new(100, 100);
12967 assert_eq!(dev.page_size(), (100, 100));
12968 }
12969
12970 #[test]
12971 fn test_fill_rect() {
12972 let mut dev = SkiaDevice::new(100, 100);
12973 let mut path = PsPath::new();
12974 path.segments.push(PathSegment::MoveTo(10.0, 10.0));
12975 path.segments.push(PathSegment::LineTo(90.0, 10.0));
12976 path.segments.push(PathSegment::LineTo(90.0, 90.0));
12977 path.segments.push(PathSegment::LineTo(10.0, 90.0));
12978 path.segments.push(PathSegment::ClosePath);
12979
12980 let params = FillParams {
12981 color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
12982 fill_rule: FillRule::NonZeroWinding,
12983 ctm: Matrix::identity(),
12984 is_text_glyph: false,
12985 overprint: false,
12986 overprint_mode: 0,
12987 opm_paired: false,
12988 painted_channels: 0,
12989 is_device_cmyk: false,
12990 spot_color: None,
12991 icc_color: None,
12992 rendering_intent: 0,
12993 transfer: TransferState::default(),
12994 halftone: HalftoneState::default(),
12995 bg_ucr: BgUcrState::default(),
12996 alpha: 1.0,
12997 blend_mode: 0,
12998 alpha_is_shape: false,
12999 };
13000 dev.fill_path(&path, ¶ms);
13001
13002 let pixel = dev.pixmap().pixel(50, 50).unwrap();
13004 assert_eq!(pixel.red(), 255);
13005 assert_eq!(pixel.green(), 0);
13006 assert_eq!(pixel.blue(), 0);
13007 }
13008
13009 #[test]
13010 fn test_stroke_line() {
13011 let mut dev = SkiaDevice::new(100, 100);
13012 let mut path = PsPath::new();
13013 path.segments.push(PathSegment::MoveTo(10.0, 50.0));
13014 path.segments.push(PathSegment::LineTo(90.0, 50.0));
13015
13016 let params = StrokeParams {
13017 color: DeviceColor::from_rgb(0.0, 0.0, 1.0),
13018 line_width: 4.0,
13019 line_cap: LineCap::Butt,
13020 line_join: LineJoin::Miter,
13021 miter_limit: 10.0,
13022 dash_pattern: DashPattern::solid(),
13023 ctm: Matrix::identity(),
13024 stroke_adjust: false,
13025 is_text_glyph: false,
13026 overprint: false,
13027 overprint_mode: 0,
13028 opm_paired: false,
13029 painted_channels: 0,
13030 is_device_cmyk: false,
13031 spot_color: None,
13032 icc_color: None,
13033 rendering_intent: 0,
13034 transfer: TransferState::default(),
13035 halftone: HalftoneState::default(),
13036 bg_ucr: BgUcrState::default(),
13037 alpha: 1.0,
13038 blend_mode: 0,
13039 alpha_is_shape: false,
13040 };
13041 dev.stroke_path(&path, ¶ms);
13042
13043 let pixel = dev.pixmap().pixel(50, 50).unwrap();
13045 assert_eq!(pixel.blue(), 255);
13046 }
13047
13048 #[test]
13049 fn test_clip() {
13050 let mut dev = SkiaDevice::new(100, 100);
13051
13052 let mut clip_path = PsPath::new();
13054 clip_path.segments.push(PathSegment::MoveTo(0.0, 0.0));
13055 clip_path.segments.push(PathSegment::LineTo(50.0, 0.0));
13056 clip_path.segments.push(PathSegment::LineTo(50.0, 100.0));
13057 clip_path.segments.push(PathSegment::LineTo(0.0, 100.0));
13058 clip_path.segments.push(PathSegment::ClosePath);
13059
13060 let clip_params = ClipParams {
13061 fill_rule: FillRule::NonZeroWinding,
13062 ctm: Matrix::identity(),
13063 stroke_params: None,
13064 };
13065 dev.clip_path(&clip_path, &clip_params);
13066
13067 let mut fill_path = PsPath::new();
13069 fill_path.segments.push(PathSegment::MoveTo(0.0, 0.0));
13070 fill_path.segments.push(PathSegment::LineTo(100.0, 0.0));
13071 fill_path.segments.push(PathSegment::LineTo(100.0, 100.0));
13072 fill_path.segments.push(PathSegment::LineTo(0.0, 100.0));
13073 fill_path.segments.push(PathSegment::ClosePath);
13074
13075 let fill_params = FillParams {
13076 color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
13077 fill_rule: FillRule::NonZeroWinding,
13078 ctm: Matrix::identity(),
13079 is_text_glyph: false,
13080 overprint: false,
13081 overprint_mode: 0,
13082 opm_paired: false,
13083 painted_channels: 0,
13084 is_device_cmyk: false,
13085 spot_color: None,
13086 icc_color: None,
13087 rendering_intent: 0,
13088 transfer: TransferState::default(),
13089 halftone: HalftoneState::default(),
13090 bg_ucr: BgUcrState::default(),
13091 alpha: 1.0,
13092 blend_mode: 0,
13093 alpha_is_shape: false,
13094 };
13095 dev.fill_path(&fill_path, &fill_params);
13096
13097 let left_pixel = dev.pixmap().pixel(25, 50).unwrap();
13099 assert_eq!(left_pixel.red(), 255);
13100
13101 let right_pixel = dev.pixmap().pixel(75, 50).unwrap();
13103 assert_eq!(right_pixel.red(), 255);
13104 assert_eq!(right_pixel.green(), 255); }
13106
13107 #[test]
13108 fn test_erase_page() {
13109 let mut dev = SkiaDevice::new(100, 100);
13110 let mut path = PsPath::new();
13112 path.segments.push(PathSegment::MoveTo(0.0, 0.0));
13113 path.segments.push(PathSegment::LineTo(100.0, 0.0));
13114 path.segments.push(PathSegment::LineTo(100.0, 100.0));
13115 path.segments.push(PathSegment::LineTo(0.0, 100.0));
13116 path.segments.push(PathSegment::ClosePath);
13117 let params = FillParams {
13118 color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
13119 fill_rule: FillRule::NonZeroWinding,
13120 ctm: Matrix::identity(),
13121 is_text_glyph: false,
13122 overprint: false,
13123 overprint_mode: 0,
13124 opm_paired: false,
13125 painted_channels: 0,
13126 is_device_cmyk: false,
13127 spot_color: None,
13128 icc_color: None,
13129 rendering_intent: 0,
13130 transfer: TransferState::default(),
13131 halftone: HalftoneState::default(),
13132 bg_ucr: BgUcrState::default(),
13133 alpha: 1.0,
13134 blend_mode: 0,
13135 alpha_is_shape: false,
13136 };
13137 dev.fill_path(&path, ¶ms);
13138
13139 dev.erase_page();
13140
13141 let pixel = dev.pixmap().pixel(50, 50).unwrap();
13143 assert_eq!(pixel.red(), 255);
13144 assert_eq!(pixel.green(), 255);
13145 assert_eq!(pixel.blue(), 255);
13146 }
13147
13148 #[test]
13149 fn test_show_page() {
13150 let mut dev = SkiaDevice::new(10, 10);
13151 let path = std::env::temp_dir().join("stet_test_output.png");
13152 let path_str = path.to_string_lossy();
13153 let result = dev.show_page(&path_str);
13154 assert!(result.is_ok());
13155 assert!(path.exists());
13156 std::fs::remove_file(&path).ok();
13157 }
13158
13159 #[test]
13160 fn test_transform() {
13161 let mut dev = SkiaDevice::new(200, 200);
13162 let mut path = PsPath::new();
13164 path.segments.push(PathSegment::MoveTo(0.0, 0.0));
13165 path.segments.push(PathSegment::LineTo(10.0, 0.0));
13166 path.segments.push(PathSegment::LineTo(10.0, 10.0));
13167 path.segments.push(PathSegment::LineTo(0.0, 10.0));
13168 path.segments.push(PathSegment::ClosePath);
13169
13170 let params = FillParams {
13171 color: DeviceColor::from_rgb(0.0, 1.0, 0.0),
13172 fill_rule: FillRule::NonZeroWinding,
13173 ctm: Matrix::translate(100.0, 100.0),
13174 is_text_glyph: false,
13175 overprint: false,
13176 overprint_mode: 0,
13177 opm_paired: false,
13178 painted_channels: 0,
13179 is_device_cmyk: false,
13180 spot_color: None,
13181 icc_color: None,
13182 rendering_intent: 0,
13183 transfer: TransferState::default(),
13184 halftone: HalftoneState::default(),
13185 bg_ucr: BgUcrState::default(),
13186 alpha: 1.0,
13187 blend_mode: 0,
13188 alpha_is_shape: false,
13189 };
13190 dev.fill_path(&path, ¶ms);
13191
13192 let pixel = dev.pixmap().pixel(105, 105).unwrap();
13194 assert_eq!(pixel.green(), 255);
13195 assert_eq!(pixel.red(), 0);
13196 }
13197
13198 fn make_test_fill_at(x: f64, y: f64, w: f64, h: f64) -> DisplayElement {
13199 let mut path = PsPath::new();
13200 path.segments.push(PathSegment::MoveTo(x, y));
13201 path.segments.push(PathSegment::LineTo(x + w, y));
13202 path.segments.push(PathSegment::LineTo(x + w, y + h));
13203 path.segments.push(PathSegment::LineTo(x, y + h));
13204 path.segments.push(PathSegment::ClosePath);
13205 DisplayElement::Fill {
13206 path,
13207 params: FillParams {
13208 color: DeviceColor::from_rgb(0.0, 0.0, 0.0),
13209 fill_rule: FillRule::NonZeroWinding,
13210 ctm: Matrix::identity(),
13211 is_text_glyph: false,
13212 overprint: false,
13213 overprint_mode: 0,
13214 opm_paired: false,
13215 painted_channels: 0,
13216 is_device_cmyk: false,
13217 spot_color: None,
13218 icc_color: None,
13219 rendering_intent: 0,
13220 transfer: TransferState::default(),
13221 halftone: HalftoneState::default(),
13222 bg_ucr: BgUcrState::default(),
13223 alpha: 1.0,
13224 blend_mode: 0,
13225 alpha_is_shape: false,
13226 },
13227 }
13228 }
13229
13230 #[test]
13231 fn test_compute_paint_bounds_two_fills() {
13232 let mut list = DisplayList::new();
13233 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");
13237 assert!(
13238 (bounds.x_min - 10.0).abs() < 1e-9,
13239 "x_min was {}",
13240 bounds.x_min
13241 );
13242 assert!(
13243 (bounds.y_min - 20.0).abs() < 1e-9,
13244 "y_min was {}",
13245 bounds.y_min
13246 );
13247 assert!(
13248 (bounds.x_max - 150.0).abs() < 1e-9,
13249 "x_max was {}",
13250 bounds.x_max
13251 );
13252 assert!(
13253 (bounds.y_max - 75.0).abs() < 1e-9,
13254 "y_max was {}",
13255 bounds.y_max
13256 );
13257 }
13258
13259 #[test]
13260 fn test_compute_paint_bounds_empty_list() {
13261 let list = DisplayList::new();
13262 assert!(compute_paint_bounds(&list, 72.0).is_none());
13263 }
13264
13265 #[test]
13266 fn test_compute_paint_bounds_only_clip_returns_none() {
13267 let mut list = DisplayList::new();
13268 list.push(DisplayElement::InitClip);
13269 assert!(compute_paint_bounds(&list, 72.0).is_none());
13272 }
13273
13274 #[test]
13275 fn test_rasterize_mask_anchors_to_paint_bounds() {
13276 use stet_graphics::display_list::{SoftMaskParams, SoftMaskSubtype};
13277
13278 let mut mask = DisplayList::new();
13281 let mut path = PsPath::new();
13282 path.segments.push(PathSegment::MoveTo(200.0, 300.0));
13283 path.segments.push(PathSegment::LineTo(250.0, 300.0));
13284 path.segments.push(PathSegment::LineTo(250.0, 340.0));
13285 path.segments.push(PathSegment::LineTo(200.0, 340.0));
13286 path.segments.push(PathSegment::ClosePath);
13287 mask.push(DisplayElement::Fill {
13288 path,
13289 params: FillParams {
13290 color: DeviceColor::from_rgb(1.0, 1.0, 1.0),
13291 fill_rule: FillRule::NonZeroWinding,
13292 ctm: Matrix::identity(),
13293 is_text_glyph: false,
13294 overprint: false,
13295 overprint_mode: 0,
13296 opm_paired: false,
13297 painted_channels: 0,
13298 is_device_cmyk: false,
13299 spot_color: None,
13300 icc_color: None,
13301 rendering_intent: 0,
13302 transfer: TransferState::default(),
13303 halftone: HalftoneState::default(),
13304 bg_ucr: BgUcrState::default(),
13305 alpha: 1.0,
13306 blend_mode: 0,
13307 alpha_is_shape: false,
13308 },
13309 });
13310
13311 let params = SoftMaskParams {
13312 subtype: SoftMaskSubtype::Luminosity,
13313 bbox: [0.0, 0.0, 100.0, 100.0],
13316 backdrop_color: None, transfer_invert: false,
13318 has_nested_mask_scope: false,
13319 parent_clip_bbox: None,
13320 };
13321
13322 let raster = rasterize_mask(
13323 &mask,
13324 ¶ms,
13325 None,
13326 false,
13327 72.0,
13328 1.0,
13329 1.0,
13330 &LayerSet::new(),
13331 )
13332 .expect("expected raster");
13333
13334 assert_eq!(raster.origin_x, 199);
13337 assert_eq!(raster.origin_y, 299);
13338 assert_eq!(raster.width, 52);
13340 assert_eq!(raster.height, 42);
13341 assert_eq!(raster.scale_x, 1.0);
13342 assert_eq!(raster.scale_y, 1.0);
13343
13344 let mx = 225 - raster.origin_x;
13348 let my = 320 - raster.origin_y;
13349 assert!(mx >= 0 && (mx as u32) < raster.width);
13350 assert!(my >= 0 && (my as u32) < raster.height);
13351 let center_value = raster.data[(my as usize) * raster.width as usize + mx as usize];
13352 assert_eq!(
13353 center_value, 255,
13354 "center of painted mask should be opaque white (lum=255)"
13355 );
13356
13357 let mx_out = 300 - raster.origin_x;
13361 let in_bounds = mx_out >= 0 && (mx_out as u32) < raster.width;
13362 assert!(!in_bounds, "page x=300 should be outside the mask raster");
13363 assert_eq!(
13364 out_of_bounds_mask_value(¶ms),
13365 0,
13366 "black backdrop → out-of-bounds = 0"
13367 );
13368 }
13369
13370 #[test]
13371 fn test_band_local_to_mask_formula() {
13372 let raster_origin_x = 200i32;
13383 let raster_origin_y = 300i32;
13384
13385 let sample = |vp_x_dev: f32,
13387 vp_y_dev: f32,
13388 scale: f32,
13389 crop_x: i32,
13390 crop_y: i32,
13391 x: i32,
13392 y: i32|
13393 -> (i32, i32) {
13394 let vp_x_pixels = (vp_x_dev * scale).round() as i32;
13395 let vp_y_pixels = (vp_y_dev * scale).round() as i32;
13396 let page_x = vp_x_pixels + crop_x + x;
13397 let page_y = vp_y_pixels + crop_y + y;
13398 let mx = page_x - raster_origin_x;
13399 let my = page_y - raster_origin_y;
13400 (mx, my)
13401 };
13402
13403 let (mx, my) = sample(0.0, 0.0, 1.0, 220, 310, 0, 0);
13408 assert_eq!((mx, my), (20, 10), "top band: smask top-left");
13409
13410 let (mx, my) = sample(0.0, 0.0, 1.0, 220, 310, 5, 5);
13412 assert_eq!((mx, my), (25, 15), "top band: 5px into smask");
13413
13414 let (mx, my) = sample(0.0, 305.0, 1.0, 220, 5, 0, 0);
13422 assert_eq!((mx, my), (20, 10), "mid band: smask top-left");
13423
13424 let vp_x_pixels = (100.0_f32 * 2.0).round() as i32;
13436 let crop_x = ((220.0_f32 - 100.0) * 2.0).floor() as i32;
13437 let page_x_for_x_zero = vp_x_pixels + crop_x;
13438 assert_eq!(page_x_for_x_zero, 440, "viewport scale-2: page-x at x=0");
13439 }
13440
13441 fn x_path() -> PsPath {
13444 let mut p = PsPath::new();
13445 p.segments.push(PathSegment::MoveTo(10.0, 10.0));
13446 p.segments.push(PathSegment::LineTo(20.0, 20.0));
13447 p.segments.push(PathSegment::LineTo(30.0, 10.0));
13448 p.segments.push(PathSegment::LineTo(20.0, 0.0));
13449 p.segments.push(PathSegment::ClosePath);
13450 p
13451 }
13452
13453 fn x_path_perturbed() -> PsPath {
13454 let mut p = PsPath::new();
13457 p.segments.push(PathSegment::MoveTo(10.001, 10.0));
13458 p.segments.push(PathSegment::LineTo(20.0, 19.999));
13459 p.segments.push(PathSegment::LineTo(30.002, 10.001));
13460 p.segments.push(PathSegment::LineTo(19.999, 0.0));
13461 p.segments.push(PathSegment::ClosePath);
13462 p
13463 }
13464
13465 fn fill(path: PsPath, alpha: f64, blend: u8) -> DisplayElement {
13466 DisplayElement::Fill {
13467 path,
13468 params: FillParams {
13469 color: DeviceColor::from_rgb(0.0, 0.0, 0.0),
13470 fill_rule: FillRule::NonZeroWinding,
13471 ctm: Matrix::identity(),
13472 is_text_glyph: false,
13473 overprint: false,
13474 overprint_mode: 0,
13475 opm_paired: false,
13476 painted_channels: 0,
13477 is_device_cmyk: false,
13478 spot_color: None,
13479 icc_color: None,
13480 rendering_intent: 0,
13481 transfer: TransferState::default(),
13482 halftone: HalftoneState::default(),
13483 bg_ucr: BgUcrState::default(),
13484 alpha,
13485 blend_mode: blend,
13486 alpha_is_shape: false,
13487 },
13488 }
13489 }
13490
13491 fn rect_path(x0: f64, y0: f64, x1: f64, y1: f64) -> PsPath {
13492 let mut p = PsPath::new();
13493 p.segments.push(PathSegment::MoveTo(x0, y0));
13494 p.segments.push(PathSegment::LineTo(x1, y0));
13495 p.segments.push(PathSegment::LineTo(x1, y1));
13496 p.segments.push(PathSegment::LineTo(x0, y1));
13497 p.segments.push(PathSegment::ClosePath);
13498 p
13499 }
13500
13501 fn clip_elem(path: PsPath) -> DisplayElement {
13502 DisplayElement::Clip {
13503 path,
13504 params: ClipParams {
13505 fill_rule: FillRule::NonZeroWinding,
13506 ctm: Matrix::identity(),
13507 stroke_params: None,
13508 },
13509 }
13510 }
13511
13512 fn group_elem(
13513 inner: Vec<DisplayElement>,
13514 bbox: [f64; 4],
13515 isolated: bool,
13516 alpha: f64,
13517 blend: u8,
13518 ) -> DisplayElement {
13519 let mut dl = DisplayList::new();
13520 for e in inner {
13521 dl.push(e);
13522 }
13523 DisplayElement::Group {
13524 elements: dl,
13525 params: stet_graphics::display_list::GroupParams {
13526 bbox,
13527 isolated,
13528 knockout: false,
13529 blend_mode: blend,
13530 alpha,
13531 color_space: stet_graphics::display_list::GroupColorSpace::Inherited,
13532 },
13533 }
13534 }
13535
13536 fn dl(elements: Vec<DisplayElement>) -> DisplayList {
13537 let mut d = DisplayList::new();
13538 for e in elements {
13539 d.push(e);
13540 }
13541 d
13542 }
13543
13544 #[test]
13545 fn obscured_skip_fires_on_matching_fill_plus_iso_group() {
13546 let parent = fill(x_path(), 1.0, 0);
13549 let inner = vec![fill(x_path_perturbed(), 1.0, 0)];
13550 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13551 let d = dl(vec![
13552 parent,
13553 clip_elem(rect_path(0.0, -5.0, 40.0, 30.0)),
13554 grp,
13555 ]);
13556 assert_eq!(compute_obscured_fill_skips(&d), vec![0]);
13557 }
13558
13559 #[test]
13560 fn obscured_skip_does_not_fire_on_non_isolated_group() {
13561 let parent = fill(x_path(), 1.0, 0);
13562 let inner = vec![fill(x_path(), 1.0, 0)];
13563 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], false, 1.0, 0);
13564 let d = dl(vec![parent, grp]);
13565 assert!(compute_obscured_fill_skips(&d).is_empty());
13566 }
13567
13568 #[test]
13569 fn obscured_skip_does_not_fire_on_partial_alpha_group() {
13570 let parent = fill(x_path(), 1.0, 0);
13571 let inner = vec![fill(x_path(), 1.0, 0)];
13572 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 0.5, 0);
13573 let d = dl(vec![parent, grp]);
13574 assert!(compute_obscured_fill_skips(&d).is_empty());
13575 }
13576
13577 #[test]
13578 fn obscured_skip_does_not_fire_on_non_normal_blend() {
13579 let parent = fill(x_path(), 1.0, 0);
13580 let inner = vec![fill(x_path(), 1.0, 0)];
13581 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 10);
13584 let d = dl(vec![parent, grp]);
13585 assert!(compute_obscured_fill_skips(&d).is_empty());
13586 }
13587
13588 #[test]
13589 fn obscured_skip_does_not_fire_when_paths_differ() {
13590 let parent = fill(rect_path(0.0, 0.0, 5.0, 5.0), 1.0, 0);
13591 let inner = vec![fill(x_path(), 1.0, 0)];
13592 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13593 let d = dl(vec![parent, grp]);
13594 assert!(compute_obscured_fill_skips(&d).is_empty());
13595 }
13596
13597 #[test]
13598 fn obscured_skip_does_not_fire_when_group_bbox_too_small() {
13599 let big = rect_path(0.0, 0.0, 100.0, 100.0);
13603 let parent = fill(big.clone(), 1.0, 0);
13604 let inner = vec![fill(big, 1.0, 0)];
13605 let grp = group_elem(inner, [0.0, 0.0, 10.0, 10.0], true, 1.0, 0);
13607 let d = dl(vec![parent, grp]);
13608 assert!(compute_obscured_fill_skips(&d).is_empty());
13609 }
13610
13611 #[test]
13612 fn obscured_skip_does_not_fire_when_intervening_clip_too_small() {
13613 let parent = fill(x_path(), 1.0, 0);
13617 let narrow_clip = clip_elem(rect_path(12.0, 5.0, 18.0, 15.0));
13618 let inner = vec![fill(x_path(), 1.0, 0)];
13619 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13620 let d = dl(vec![parent, narrow_clip, grp]);
13621 assert!(compute_obscured_fill_skips(&d).is_empty());
13622 }
13623
13624 #[test]
13625 fn obscured_skip_does_not_fire_when_inner_clip_too_small() {
13626 let parent = fill(x_path(), 1.0, 0);
13628 let inner = vec![
13629 clip_elem(rect_path(12.0, 5.0, 18.0, 15.0)),
13630 fill(x_path(), 1.0, 0),
13631 ];
13632 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13633 let d = dl(vec![parent, grp]);
13634 assert!(compute_obscured_fill_skips(&d).is_empty());
13635 }
13636
13637 #[test]
13638 fn obscured_skip_fires_when_inner_clip_is_wider_than_parent_path() {
13639 let parent = fill(x_path(), 1.0, 0);
13642 let inner = vec![
13643 clip_elem(rect_path(-10.0, -10.0, 40.0, 30.0)),
13644 fill(x_path_perturbed(), 1.0, 0),
13645 ];
13646 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13647 let d = dl(vec![parent, grp]);
13648 assert_eq!(compute_obscured_fill_skips(&d), vec![0]);
13649 }
13650
13651 #[test]
13652 fn obscured_skip_does_not_fire_on_partial_alpha_parent() {
13653 let parent = fill(x_path(), 0.5, 0);
13656 let inner = vec![fill(x_path(), 1.0, 0)];
13657 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13658 let d = dl(vec![parent, grp]);
13659 assert!(compute_obscured_fill_skips(&d).is_empty());
13660 }
13661}