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