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(
1574 list: &DisplayList,
1575 system_cmyk_bytes: Option<&std::sync::Arc<Vec<u8>>>,
1576) -> IccCache {
1577 let mut cache = IccCache::new();
1578 let mut seen = HashSet::new();
1579
1580 if let Some(cmyk_bytes) = system_cmyk_bytes
1582 && let Some(hash) = cache.register_profile(cmyk_bytes)
1583 {
1584 seen.insert(hash);
1585 cache.set_default_cmyk_hash(hash);
1587 cache.prepare_reverse_cmyk();
1591 }
1592
1593 fn scan_elements(
1595 elements: &[DisplayElement],
1596 seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
1597 cache: &mut IccCache,
1598 ) {
1599 for element in elements {
1600 if let DisplayElement::Group { elements: sub, .. } = element {
1602 scan_elements(sub.elements(), seen, cache);
1603 }
1604 if let DisplayElement::SoftMasked { content, mask, .. } = element {
1605 scan_elements(content.elements(), seen, cache);
1606 scan_elements(mask.elements(), seen, cache);
1607 }
1608 if let DisplayElement::OcgGroup { elements: sub, .. } = element {
1609 scan_elements(sub.elements(), seen, cache);
1610 }
1611 let shading_cs = match element {
1613 DisplayElement::AxialShading { params } => Some(¶ms.color_space),
1614 DisplayElement::RadialShading { params } => Some(¶ms.color_space),
1615 DisplayElement::MeshShading { params } => Some(¶ms.color_space),
1616 DisplayElement::PatchShading { params } => Some(¶ms.color_space),
1617 _ => None,
1618 };
1619 if let Some(stet_graphics::device::ShadingColorSpace::ICCBased {
1620 n,
1621 profile_hash,
1622 profile_data,
1623 }) = shading_cs
1624 {
1625 if seen.insert(*profile_hash) {
1626 cache.register_profile_with_n(profile_data, Some(*n));
1627 }
1628 }
1629 if let DisplayElement::Image { params, .. } = element {
1631 match ¶ms.color_space {
1632 ImageColorSpace::ICCBased {
1633 n,
1634 profile_hash,
1635 profile_data,
1636 } if seen.insert(*profile_hash) => {
1637 cache.register_profile_with_n(profile_data, Some(*n));
1638 }
1639 ImageColorSpace::Indexed { base, .. }
1640 if matches!(base.as_ref(), ImageColorSpace::ICCBased { .. }) =>
1641 {
1642 if let ImageColorSpace::ICCBased {
1643 n,
1644 profile_hash,
1645 profile_data,
1646 } = base.as_ref()
1647 {
1648 if seen.insert(*profile_hash) {
1649 cache.register_profile_with_n(profile_data, Some(*n));
1650 }
1651 }
1652 }
1653 _ => {}
1654 }
1655 }
1656 }
1657 }
1658 scan_elements(list.elements(), &mut seen, &mut cache);
1659
1660 cache
1661}
1662
1663fn register_shading_icc_profiles(list: &DisplayList, cache: &mut IccCache) {
1668 fn register_image_iccs(
1669 cs: &ImageColorSpace,
1670 seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
1671 cache: &mut IccCache,
1672 ) {
1673 match cs {
1674 ImageColorSpace::ICCBased {
1675 n,
1676 profile_hash,
1677 profile_data,
1678 } => {
1679 if seen.insert(*profile_hash) {
1680 cache.register_profile_with_n(profile_data, Some(*n));
1681 }
1682 }
1683 ImageColorSpace::Indexed { base, .. } => register_image_iccs(base, seen, cache),
1684 ImageColorSpace::Separation { alt_space, .. }
1685 | ImageColorSpace::DeviceN { alt_space, .. } => {
1686 register_image_iccs(alt_space, seen, cache)
1687 }
1688 _ => {}
1689 }
1690 }
1691 fn scan(
1692 elements: &[DisplayElement],
1693 seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
1694 cache: &mut IccCache,
1695 ) {
1696 for element in elements {
1697 if let DisplayElement::Group { elements: sub, .. } = element {
1698 scan(sub.elements(), seen, cache);
1699 }
1700 if let DisplayElement::SoftMasked { content, mask, .. } = element {
1701 scan(content.elements(), seen, cache);
1702 scan(mask.elements(), seen, cache);
1703 }
1704 if let DisplayElement::OcgGroup { elements: sub, .. } = element {
1705 scan(sub.elements(), seen, cache);
1706 }
1707 let shading_cs = match element {
1708 DisplayElement::AxialShading { params } => Some(¶ms.color_space),
1709 DisplayElement::RadialShading { params } => Some(¶ms.color_space),
1710 DisplayElement::MeshShading { params } => Some(¶ms.color_space),
1711 DisplayElement::PatchShading { params } => Some(¶ms.color_space),
1712 _ => None,
1713 };
1714 if let Some(stet_graphics::device::ShadingColorSpace::ICCBased {
1715 n,
1716 profile_hash,
1717 profile_data,
1718 }) = shading_cs
1719 && seen.insert(*profile_hash)
1720 {
1721 cache.register_profile_with_n(profile_data, Some(*n));
1722 }
1723 if let DisplayElement::Image { params, .. } = element {
1724 register_image_iccs(¶ms.color_space, seen, cache);
1725 }
1726 }
1727 }
1728 let mut seen = HashSet::new();
1729 scan(list.elements(), &mut seen, cache);
1730}
1731
1732fn samples_to_rgba(
1736 data: &[u8],
1737 params: &ImageParams,
1738 icc: Option<&IccCache>,
1739 opm_zero_transparent: bool,
1740) -> Vec<u8> {
1741 let w = params.width as usize;
1742 let h = params.height as usize;
1743 let npixels = w * h;
1744 let bpc = params.bits_per_component;
1745 match ¶ms.color_space {
1746 ImageColorSpace::PreconvertedRGBA => {
1747 data.to_vec()
1749 }
1750 ImageColorSpace::DeviceGray => {
1751 let mut rgba = vec![255u8; npixels * 4];
1752 if bpc == 16 {
1753 for i in 0..npixels {
1754 let g = data.get(i * 2).copied().unwrap_or(0);
1755 let pi = i * 4;
1756 rgba[pi] = g;
1757 rgba[pi + 1] = g;
1758 rgba[pi + 2] = g;
1759 }
1760 } else {
1761 for i in 0..npixels {
1762 let g = data.get(i).copied().unwrap_or(0);
1763 let pi = i * 4;
1764 rgba[pi] = g;
1765 rgba[pi + 1] = g;
1766 rgba[pi + 2] = g;
1767 }
1768 }
1769 rgba
1770 }
1771 ImageColorSpace::DeviceRGB => {
1772 let mut rgba = vec![255u8; npixels * 4];
1773 if bpc == 16 {
1774 for i in 0..npixels {
1777 let si = i * 6;
1778 let pi = i * 4;
1779 rgba[pi] = data.get(si).copied().unwrap_or(0);
1780 rgba[pi + 1] = data.get(si + 2).copied().unwrap_or(0);
1781 rgba[pi + 2] = data.get(si + 4).copied().unwrap_or(0);
1782 }
1783 } else {
1784 for i in 0..npixels {
1785 let si = i * 3;
1786 let pi = i * 4;
1787 rgba[pi] = data.get(si).copied().unwrap_or(0);
1788 rgba[pi + 1] = data.get(si + 1).copied().unwrap_or(0);
1789 rgba[pi + 2] = data.get(si + 2).copied().unwrap_or(0);
1790 }
1791 }
1792 rgba
1793 }
1794 ImageColorSpace::DeviceCMYK => {
1795 if let Some(cache) = icc
1799 && let Some(cmyk_hash) = cache.default_cmyk_hash()
1800 {
1801 let avail_pixels = data.len() / 4;
1802 let icc_pixels = avail_pixels.min(npixels);
1803 if icc_pixels > 0
1804 && let Some(rgb) = cache.convert_image_8bit(cmyk_hash, data, icc_pixels)
1805 {
1806 let mut rgba = vec![255u8; npixels * 4];
1807 for i in 0..icc_pixels {
1808 rgba[i * 4] = rgb[i * 3];
1809 rgba[i * 4 + 1] = rgb[i * 3 + 1];
1810 rgba[i * 4 + 2] = rgb[i * 3 + 2];
1811 if opm_zero_transparent {
1813 let si = i * 4;
1814 if data[si] == 0
1815 && data[si + 1] == 0
1816 && data[si + 2] == 0
1817 && data[si + 3] == 0
1818 {
1819 rgba[i * 4 + 3] = 0;
1820 }
1821 }
1822 }
1823 return rgba;
1825 }
1826 }
1827 let mut rgba = vec![255u8; npixels * 4];
1829 for i in 0..npixels {
1830 let si = i * 4;
1831 let c = data.get(si).copied().unwrap_or(0) as f64 / 255.0;
1832 let m = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0;
1833 let y = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0;
1834 let k = data.get(si + 3).copied().unwrap_or(0) as f64 / 255.0;
1835 let r = (1.0 - c.min(1.0)) * (1.0 - k.min(1.0));
1836 let g = (1.0 - m.min(1.0)) * (1.0 - k.min(1.0));
1837 let b = (1.0 - y.min(1.0)) * (1.0 - k.min(1.0));
1838 let pi = i * 4;
1839 rgba[pi] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
1840 rgba[pi + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
1841 rgba[pi + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
1842 if opm_zero_transparent
1844 && data.get(si).copied().unwrap_or(0) == 0
1845 && data.get(si + 1).copied().unwrap_or(0) == 0
1846 && data.get(si + 2).copied().unwrap_or(0) == 0
1847 && data.get(si + 3).copied().unwrap_or(0) == 0
1848 {
1849 rgba[pi + 3] = 0;
1850 }
1851 }
1852 rgba
1853 }
1854 ImageColorSpace::ICCBased {
1855 n,
1856 profile_hash,
1857 profile_data,
1858 } => {
1859 if let Some(cache) = icc
1861 && cache.has_profile(profile_hash)
1862 && let Some(rgb) = cache.convert_image_8bit(profile_hash, data, npixels)
1863 {
1864 let mut rgba = vec![255u8; npixels * 4];
1865 for i in 0..npixels {
1866 rgba[i * 4] = rgb[i * 3];
1867 rgba[i * 4 + 1] = rgb[i * 3 + 1];
1868 rgba[i * 4 + 2] = rgb[i * 3 + 2];
1869 if opm_zero_transparent && *n == 4 {
1871 let si = i * *n as usize;
1872 if si + 3 < data.len()
1873 && data[si] == 0
1874 && data[si + 1] == 0
1875 && data[si + 2] == 0
1876 && data[si + 3] == 0
1877 {
1878 rgba[i * 4 + 3] = 0;
1879 }
1880 }
1881 }
1882 return rgba;
1883 }
1884 let _ = (profile_hash, profile_data);
1886 let fallback = match n {
1887 1 => ImageColorSpace::DeviceGray,
1888 4 => ImageColorSpace::DeviceCMYK,
1889 _ => ImageColorSpace::DeviceRGB,
1890 };
1891 let p = ImageParams {
1892 color_space: fallback,
1893 bits_per_component: 8,
1894 ..params.clone()
1895 };
1896 samples_to_rgba(data, &p, icc, opm_zero_transparent)
1897 }
1898 ImageColorSpace::Indexed {
1899 base,
1900 hival,
1901 lookup,
1902 } => {
1903 let base_ncomp = base.num_components() as usize;
1904 let mut expanded = Vec::with_capacity(npixels * base_ncomp);
1906 for i in 0..npixels {
1907 let idx = data.get(i).copied().unwrap_or(0) as usize;
1908 let idx = idx.min(*hival as usize);
1909 let offset = idx * base_ncomp;
1910 for c in 0..base_ncomp {
1911 expanded.push(lookup.get(offset + c).copied().unwrap_or(0));
1912 }
1913 }
1914 let p = ImageParams {
1915 color_space: *base.clone(),
1916 bits_per_component: 8,
1917 ..params.clone()
1918 };
1919 samples_to_rgba(&expanded, &p, icc, opm_zero_transparent)
1920 }
1921 ImageColorSpace::CIEBasedABC { params: cie_params } => {
1922 let mut rgba = vec![255u8; npixels * 4];
1923 for i in 0..npixels {
1924 let si = i * 3;
1925 let a = data.get(si).copied().unwrap_or(0) as f64 / 255.0;
1926 let b = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0;
1927 let c = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0;
1928 let color = DeviceColor::from_cie_abc(a, b, c, cie_params);
1929 let pi = i * 4;
1930 rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
1931 rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
1932 rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
1933 }
1934 rgba
1935 }
1936 ImageColorSpace::CIEBasedA { params: cie_params } => {
1937 let mut rgba = vec![255u8; npixels * 4];
1938 for i in 0..npixels {
1939 let val = data.get(i).copied().unwrap_or(0) as f64 / 255.0;
1940 let color = DeviceColor::from_cie_a(val, cie_params);
1941 let pi = i * 4;
1942 rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
1943 rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
1944 rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
1945 }
1946 rgba
1947 }
1948 ImageColorSpace::Lab { range, .. } => {
1949 let mut rgba = vec![255u8; npixels * 4];
1950 let a_span = range[1] - range[0];
1951 let b_span = range[3] - range[2];
1952 for i in 0..npixels {
1953 let si = i * 3;
1954 let l = data.get(si).copied().unwrap_or(0) as f64 / 255.0 * 100.0;
1955 let a = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0 * a_span + range[0];
1956 let b = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0 * b_span + range[2];
1957 let color = DeviceColor::from_lab(l, a, b, range);
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::Separation {
1966 alt_space,
1967 tint_table,
1968 ..
1969 } => {
1970 if matches!(alt_space.as_ref(), ImageColorSpace::DeviceCMYK)
1973 && let Some(rgba) = tint_separation_via_icc(data, npixels, tint_table, icc)
1974 {
1975 return rgba;
1976 }
1977 let mut rgba = vec![255u8; npixels * 4];
1978 let no = tint_table.num_outputs as usize;
1979 let mut alt_comps = vec![0.0f32; no];
1980 for i in 0..npixels {
1981 let tint = data.get(i).copied().unwrap_or(0) as f32 / 255.0;
1982 tint_table.lookup_1d(tint, &mut alt_comps);
1983 let (r, g, b) = alt_comps_to_rgb(&alt_comps, alt_space);
1984 let pi = i * 4;
1985 rgba[pi] = r;
1986 rgba[pi + 1] = g;
1987 rgba[pi + 2] = b;
1988 }
1989 rgba
1990 }
1991 ImageColorSpace::DeviceN {
1992 alt_space,
1993 tint_table,
1994 ..
1995 } => {
1996 let ni = tint_table.num_inputs as usize;
1997 let no = tint_table.num_outputs as usize;
1998 if matches!(alt_space.as_ref(), ImageColorSpace::DeviceCMYK)
2000 && let Some(rgba) = tint_devicen_via_icc(data, npixels, ni, tint_table, icc)
2001 {
2002 return rgba;
2003 }
2004 let mut rgba = vec![255u8; npixels * 4];
2005 let mut inputs = vec![0.0f32; ni];
2006 let mut alt_comps = vec![0.0f32; no];
2007 for i in 0..npixels {
2008 let si = i * ni;
2009 for (c, inp) in inputs.iter_mut().enumerate() {
2010 *inp = data.get(si + c).copied().unwrap_or(0) as f32 / 255.0;
2011 }
2012 tint_table.lookup_nd(&inputs, &mut alt_comps);
2013 let (r, g, b) = alt_comps_to_rgb(&alt_comps, alt_space);
2014 let pi = i * 4;
2015 rgba[pi] = r;
2016 rgba[pi + 1] = g;
2017 rgba[pi + 2] = b;
2018 }
2019 rgba
2020 }
2021 ImageColorSpace::Mask { color, polarity } => {
2022 let mut rgba = vec![0u8; npixels * 4];
2023 let r = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
2024 let g = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
2025 let b = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
2026 let bytes_per_row = (w).div_ceil(8);
2027 for row in 0..h {
2028 for col in 0..w {
2029 let byte_idx = row * bytes_per_row + col / 8;
2030 let bit_offset = 7 - (col % 8);
2031 let bit = if byte_idx < data.len() {
2032 (data[byte_idx] >> bit_offset) & 1
2033 } else {
2034 0
2035 };
2036 let paint = if *polarity { bit == 1 } else { bit == 0 };
2037 if paint {
2038 let pi = (row * w + col) * 4;
2039 rgba[pi] = r;
2040 rgba[pi + 1] = g;
2041 rgba[pi + 2] = b;
2042 rgba[pi + 3] = 255;
2043 }
2044 }
2045 }
2046 rgba
2047 }
2048 _ => vec![0u8; npixels * 4],
2049 }
2050}
2051
2052fn tint_separation_via_icc(
2055 data: &[u8],
2056 npixels: usize,
2057 tint_table: &TintLookupTable,
2058 icc: Option<&IccCache>,
2059) -> Option<Vec<u8>> {
2060 let cache = icc?;
2061 let cmyk_hash = cache.default_cmyk_hash()?;
2062 let mut cmyk_data = vec![0u8; npixels * 4];
2064 let mut alt_comps = [0.0f32; 4];
2065 for i in 0..npixels {
2066 let tint = data.get(i).copied().unwrap_or(0) as f32 / 255.0;
2067 tint_table.lookup_1d(tint, &mut alt_comps);
2068 let si = i * 4;
2069 cmyk_data[si] = (alt_comps[0].clamp(0.0, 1.0) * 255.0).round() as u8;
2070 cmyk_data[si + 1] = (alt_comps[1].clamp(0.0, 1.0) * 255.0).round() as u8;
2071 cmyk_data[si + 2] = (alt_comps[2].clamp(0.0, 1.0) * 255.0).round() as u8;
2072 cmyk_data[si + 3] = (alt_comps[3].clamp(0.0, 1.0) * 255.0).round() as u8;
2073 }
2074 let rgb = cache.convert_image_8bit(cmyk_hash, &cmyk_data, npixels)?;
2075 let mut rgba = vec![255u8; npixels * 4];
2076 for i in 0..npixels {
2077 rgba[i * 4] = rgb[i * 3];
2078 rgba[i * 4 + 1] = rgb[i * 3 + 1];
2079 rgba[i * 4 + 2] = rgb[i * 3 + 2];
2080 }
2081 Some(rgba)
2082}
2083
2084fn tint_devicen_via_icc(
2086 data: &[u8],
2087 npixels: usize,
2088 ni: usize,
2089 tint_table: &TintLookupTable,
2090 icc: Option<&IccCache>,
2091) -> Option<Vec<u8>> {
2092 let cache = icc?;
2093 let cmyk_hash = cache.default_cmyk_hash()?;
2094 let mut cmyk_data = vec![0u8; npixels * 4];
2095 let mut inputs = vec![0.0f32; ni];
2096 let mut alt_comps = [0.0f32; 4];
2097 for i in 0..npixels {
2098 let si = i * ni;
2099 for (c, inp) in inputs.iter_mut().enumerate() {
2100 *inp = data.get(si + c).copied().unwrap_or(0) as f32 / 255.0;
2101 }
2102 tint_table.lookup_nd(&inputs, &mut alt_comps);
2103 let di = i * 4;
2104 cmyk_data[di] = (alt_comps[0].clamp(0.0, 1.0) * 255.0).round() as u8;
2105 cmyk_data[di + 1] = (alt_comps[1].clamp(0.0, 1.0) * 255.0).round() as u8;
2106 cmyk_data[di + 2] = (alt_comps[2].clamp(0.0, 1.0) * 255.0).round() as u8;
2107 cmyk_data[di + 3] = (alt_comps[3].clamp(0.0, 1.0) * 255.0).round() as u8;
2108 }
2109 let rgb = cache.convert_image_8bit(cmyk_hash, &cmyk_data, npixels)?;
2110 let mut rgba = vec![255u8; npixels * 4];
2111 for i in 0..npixels {
2112 rgba[i * 4] = rgb[i * 3];
2113 rgba[i * 4 + 1] = rgb[i * 3 + 1];
2114 rgba[i * 4 + 2] = rgb[i * 3 + 2];
2115 }
2116 Some(rgba)
2117}
2118
2119fn alt_comps_to_rgb(comps: &[f32], alt_space: &ImageColorSpace) -> (u8, u8, u8) {
2121 match alt_space {
2122 ImageColorSpace::DeviceGray => {
2123 let g = (comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2124 (g, g, g)
2125 }
2126 ImageColorSpace::DeviceRGB => {
2127 let r = (comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2128 let g = (comps.get(1).copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2129 let b = (comps.get(2).copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2130 (r, g, b)
2131 }
2132 ImageColorSpace::DeviceCMYK => {
2133 let c = comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0);
2134 let m = comps.get(1).copied().unwrap_or(0.0).clamp(0.0, 1.0);
2135 let y = comps.get(2).copied().unwrap_or(0.0).clamp(0.0, 1.0);
2136 let k = comps.get(3).copied().unwrap_or(0.0).clamp(0.0, 1.0);
2137 let r = ((1.0 - (c + k).min(1.0)) * 255.0).round() as u8;
2138 let g = ((1.0 - (m + k).min(1.0)) * 255.0).round() as u8;
2139 let b = ((1.0 - (y + k).min(1.0)) * 255.0).round() as u8;
2140 (r, g, b)
2141 }
2142 _ => (0, 0, 0),
2143 }
2144}
2145
2146fn apply_mask_color_rgba(rgba: &mut [u8], sample_data: &[u8], params: &ImageParams) {
2148 let mask_color = match ¶ms.mask_color {
2149 Some(mc) => mc,
2150 None => return,
2151 };
2152 let ncomp = params.color_space.num_components() as usize;
2153 let npixels = params.width as usize * params.height as usize;
2154 let is_range = mask_color.len() == 2 * ncomp;
2155
2156 for i in 0..npixels {
2157 let si = i * ncomp;
2158 let matched = if is_range {
2159 (0..ncomp).all(|c| {
2160 let sample = sample_data.get(si + c).copied().unwrap_or(0);
2161 let min_val = mask_color.get(c * 2).copied().unwrap_or(0);
2162 let max_val = mask_color.get(c * 2 + 1).copied().unwrap_or(0);
2163 sample >= min_val && sample <= max_val
2164 })
2165 } else {
2166 (0..ncomp).all(|c| {
2167 let sample = sample_data.get(si + c).copied().unwrap_or(0);
2168 let target = mask_color.get(c).copied().unwrap_or(0);
2169 sample == target
2170 })
2171 };
2172 if matched {
2173 let pi = i * 4;
2174 if pi + 3 < rgba.len() {
2175 rgba[pi] = 0;
2176 rgba[pi + 1] = 0;
2177 rgba[pi + 2] = 0;
2178 rgba[pi + 3] = 0;
2179 }
2180 }
2181 }
2182}
2183
2184fn image_filter_quality(transform: Transform, interpolate: bool) -> stet_tiny_skia::FilterQuality {
2190 let eff_sx = (transform.sx * transform.sx + transform.ky * transform.ky).sqrt();
2191 let eff_sy = (transform.kx * transform.kx + transform.sy * transform.sy).sqrt();
2192 let min_scale = eff_sx.min(eff_sy);
2193 if (eff_sx - 1.0).abs() < 0.01 && (eff_sy - 1.0).abs() < 0.01 {
2195 stet_tiny_skia::FilterQuality::Nearest
2196 } else if !interpolate && min_scale >= 0.95 {
2197 stet_tiny_skia::FilterQuality::Nearest
2199 } else {
2200 stet_tiny_skia::FilterQuality::Bilinear
2201 }
2202}
2203
2204fn prescale_image(
2209 rgba_data: &[u8],
2210 w: u32,
2211 h: u32,
2212 transform: Transform,
2213 interpolate: bool,
2214) -> Option<(Vec<u8>, u32, u32, Transform)> {
2215 let scale_x = (transform.sx * transform.sx + transform.ky * transform.ky).sqrt();
2217 let scale_y = (transform.kx * transform.kx + transform.sy * transform.sy).sqrt();
2218 let min_scale = scale_x.min(scale_y);
2219
2220 if min_scale > 1.05 {
2224 if interpolate {
2225 let is_axis_aligned = transform.kx.abs() < 1e-4 && transform.ky.abs() < 1e-4;
2226 if is_axis_aligned && w >= 2 && h >= 2 {
2227 let dw = (w as f32 * transform.sx.abs()).round().max(1.0) as u32;
2228 let dh = (h as f32 * transform.sy.abs()).round().max(1.0) as u32;
2229 if dw > w || dh > h {
2230 let resampled = bicubic_resample(rgba_data, w, h, dw, dh);
2231 let new_sx = transform.sx * w as f32 / dw as f32;
2232 let new_sy = transform.sy * h as f32 / dh as f32;
2233 let adjusted = Transform::from_row(
2234 new_sx,
2235 transform.ky,
2236 transform.kx,
2237 new_sy,
2238 transform.tx,
2239 transform.ty,
2240 );
2241 return Some((resampled, dw, dh, adjusted));
2242 }
2243 }
2244 }
2245 return None;
2246 }
2247
2248 if min_scale >= 0.95 {
2250 return None;
2251 }
2252
2253 let is_axis_aligned = transform.kx.abs() < 1e-4 && transform.ky.abs() < 1e-4;
2256 if is_axis_aligned && w >= 2 && h >= 2 {
2257 let dw = (w as f32 * transform.sx.abs()).ceil().max(1.0) as u32;
2258 let dh = (h as f32 * transform.sy.abs()).ceil().max(1.0) as u32;
2259 if dw < w || dh < h {
2260 let resampled = box_resample(rgba_data, w, h, dw, dh);
2261 let new_sx = transform.sx * w as f32 / dw as f32;
2263 let new_sy = transform.sy * h as f32 / dh as f32;
2264 let adjusted = Transform::from_row(
2265 new_sx,
2266 transform.ky,
2267 transform.kx,
2268 new_sy,
2269 transform.tx,
2270 transform.ty,
2271 );
2272 return Some((resampled, dw, dh, adjusted));
2273 }
2274 }
2275
2276 let factor = (1.0 / min_scale) as u32;
2278 if factor < 2 || w < factor || h < factor {
2279 return None;
2280 }
2281 let nw = w / factor;
2282 let nh = h / factor;
2283 if nw == 0 || nh == 0 {
2284 return None;
2285 }
2286 let area = factor * factor;
2287 let half = area / 2;
2288 let stride = w as usize * 4;
2289 let mut out = vec![0u8; (nw * nh * 4) as usize];
2290 for dy in 0..nh {
2291 for dx in 0..nw {
2292 let (mut r, mut g, mut b, mut a) = (0u32, 0u32, 0u32, 0u32);
2293 let sy0 = (dy * factor) as usize;
2294 let sx0 = (dx * factor) as usize;
2295 for iy in 0..factor as usize {
2296 let row = (sy0 + iy) * stride + sx0 * 4;
2297 for ix in 0..factor as usize {
2298 let i = row + ix * 4;
2299 r += rgba_data[i] as u32;
2300 g += rgba_data[i + 1] as u32;
2301 b += rgba_data[i + 2] as u32;
2302 a += rgba_data[i + 3] as u32;
2303 }
2304 }
2305 let di = (dy * nw + dx) as usize * 4;
2306 out[di] = ((r + half) / area) as u8;
2307 out[di + 1] = ((g + half) / area) as u8;
2308 out[di + 2] = ((b + half) / area) as u8;
2309 out[di + 3] = ((a + half) / area) as u8;
2310 }
2311 }
2312 let f = factor as f32;
2313 let adjusted = Transform::from_row(
2314 transform.sx * f,
2315 transform.ky * f,
2316 transform.kx * f,
2317 transform.sy * f,
2318 transform.tx,
2319 transform.ty,
2320 );
2321 Some((out, nw, nh, adjusted))
2322}
2323
2324fn translate_clip_rect(rect: &ClipRect, y_start: u32, band_h: u32) -> ClipRect {
2326 ClipRect {
2327 x0: rect.x0,
2328 y0: rect.y0.saturating_sub(y_start).min(band_h),
2329 x1: rect.x1,
2330 y1: rect.y1.saturating_sub(y_start).min(band_h),
2331 }
2332}
2333
2334fn enforce_min_image_size(transform: Transform, img_w: u32, img_h: u32) -> Transform {
2341 let eff_w =
2343 ((transform.sx * img_w as f32).powi(2) + (transform.ky * img_w as f32).powi(2)).sqrt();
2344 let eff_h =
2345 ((transform.kx * img_h as f32).powi(2) + (transform.sy * img_h as f32).powi(2)).sqrt();
2346
2347 if eff_w >= 1.0 && eff_h >= 1.0 {
2348 return transform;
2349 }
2350
2351 let ratio = eff_w.max(eff_h) / eff_w.min(eff_h).max(0.001);
2355 if ratio < 3.0 {
2356 return transform;
2357 }
2358
2359 let mut t = transform;
2360 if eff_w < 1.0 && eff_w > 0.001 {
2361 let boost = 1.0 / eff_w;
2362 t.sx *= boost;
2363 t.ky *= boost;
2364 }
2365 if eff_h < 1.0 && eff_h > 0.001 {
2366 let boost = 1.0 / eff_h;
2367 t.kx *= boost;
2368 t.sy *= boost;
2369 }
2370 t
2371}
2372
2373fn hairline_min_width(ctm: &Matrix, dpi: f64) -> f64 {
2377 let (a, b, c, d) = (ctm.a, ctm.b, ctm.c, ctm.d);
2378 let sum_sq = a * a + b * b + c * c + d * d;
2379 let diff = ((a * a + b * b - c * c - d * d).powi(2) + 4.0 * (a * c + b * d).powi(2)).sqrt();
2380 let s_max = (0.5 * (sum_sq + diff)).max(0.0).sqrt();
2381 let min_px = if dpi <= 150.0 { 0.5 } else { 1.0 };
2382 if s_max > 1e-10 {
2383 min_px / s_max
2384 } else {
2385 min_px
2386 }
2387}
2388
2389fn is_k_only_src(color: &DeviceColor) -> bool {
2395 if let Some((c, m, y, _k)) = color.native_cmyk {
2396 c == 0.0 && m == 0.0 && y == 0.0
2397 } else {
2398 false
2399 }
2400}
2401
2402fn needs_gray_promotion(
2411 overprint: bool,
2412 painted_channels: u8,
2413 is_device_cmyk: bool,
2414 color: &DeviceColor,
2415) -> Option<f64> {
2416 if !overprint
2417 || painted_channels != 0
2418 || is_device_cmyk
2419 || color.native_cmyk.is_some()
2420 || color.process_cmyk.is_some()
2421 {
2422 return None;
2423 }
2424 let r = color.r;
2425 if (r - color.g).abs() > f64::EPSILON || (r - color.b).abs() > f64::EPSILON {
2426 return None;
2427 }
2428 Some(r.clamp(0.0, 1.0))
2429}
2430
2431fn maybe_promote_gray_fill<'a>(
2434 params: &'a FillParams,
2435 buf: &'a mut Option<FillParams>,
2436) -> &'a FillParams {
2437 if let Some(gray) = needs_gray_promotion(
2438 params.overprint,
2439 params.painted_channels,
2440 params.is_device_cmyk,
2441 ¶ms.color,
2442 ) {
2443 let mut promoted = params.clone();
2444 promoted.is_device_cmyk = true;
2445 promoted.painted_channels = stet_graphics::device::CMYK_K;
2446 promoted.color.native_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2447 promoted.color.process_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2448 *buf = Some(promoted);
2449 return buf.as_ref().unwrap();
2450 }
2451 params
2452}
2453
2454fn maybe_promote_gray_stroke<'a>(
2456 params: &'a StrokeParams,
2457 buf: &'a mut Option<StrokeParams>,
2458) -> &'a StrokeParams {
2459 if let Some(gray) = needs_gray_promotion(
2460 params.overprint,
2461 params.painted_channels,
2462 params.is_device_cmyk,
2463 ¶ms.color,
2464 ) {
2465 let mut promoted = params.clone();
2466 promoted.is_device_cmyk = true;
2467 promoted.painted_channels = stet_graphics::device::CMYK_K;
2468 promoted.color.native_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2469 promoted.color.process_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2470 *buf = Some(promoted);
2471 return buf.as_ref().unwrap();
2472 }
2473 params
2474}
2475
2476fn build_stroke(params: &StrokeParams, dpi: f64) -> Stroke {
2480 let min_lw = hairline_min_width(¶ms.ctm, dpi);
2481 let mut stroke = Stroke {
2482 width: (params.line_width as f32).max(min_lw as f32),
2483 line_cap: to_line_cap(params.line_cap),
2484 line_join: to_line_join(params.line_join),
2485 miter_limit: params.miter_limit as f32,
2486 ..Stroke::default()
2487 };
2488 if !params.dash_pattern.array.is_empty() {
2489 let mut dash_array: Vec<f32> = params
2490 .dash_pattern
2491 .array
2492 .iter()
2493 .map(|&v| v as f32)
2494 .collect();
2495 if dash_array.len() % 2 == 1 {
2498 let clone = dash_array.clone();
2499 dash_array.extend_from_slice(&clone);
2500 }
2501 if let Some(dash) = StrokeDash::new(dash_array, params.dash_pattern.offset as f32) {
2502 stroke.dash = Some(dash);
2503 }
2504 }
2505 stroke
2506}
2507
2508fn ctm_is_device_space(ctm: &Matrix) -> bool {
2525 (ctm.a.abs() - 1.0).abs() < 0.01
2526 && ctm.b.abs() < 0.01
2527 && ctm.c.abs() < 0.01
2528 && (ctm.d.abs() - 1.0).abs() < 0.01
2529}
2530
2531fn stroke_adjust_path_viewport(
2537 path: &PsPath,
2538 device_width: f64,
2539 scale_x: f64,
2540 scale_y: f64,
2541 vp_x: f64,
2542 vp_y: f64,
2543) -> PsPath {
2544 let use_half_pixel = device_width < 1.5 || (device_width.round() as i32) % 2 == 1;
2545
2546 let snap_x = |v: f64| -> f64 {
2548 let out = (v - vp_x) * scale_x;
2549 let snapped = if use_half_pixel {
2550 out.floor() + 0.5
2551 } else {
2552 out.round()
2553 };
2554 snapped / scale_x + vp_x
2555 };
2556 let snap_y = |v: f64| -> f64 {
2557 let out = (v - vp_y) * scale_y;
2558 let snapped = if use_half_pixel {
2559 out.floor() + 0.5
2560 } else {
2561 out.round()
2562 };
2563 snapped / scale_y + vp_y
2564 };
2565
2566 let mut result = PsPath::new();
2567 let mut prev_x = 0.0_f64;
2568 let mut prev_y = 0.0_f64;
2569
2570 for seg in &path.segments {
2571 match *seg {
2572 PathSegment::MoveTo(x, y) => {
2573 prev_x = x;
2574 prev_y = y;
2575 result.segments.push(PathSegment::MoveTo(x, y));
2576 }
2577 PathSegment::LineTo(x, y) => {
2578 let is_horizontal = (y - prev_y).abs() < 1e-6;
2579 let is_vertical = (x - prev_x).abs() < 1e-6;
2580
2581 if is_horizontal {
2582 let snapped_y = snap_y(y);
2583 if let Some(PathSegment::MoveTo(_, ly) | PathSegment::LineTo(_, ly)) =
2584 result.segments.last_mut()
2585 {
2586 *ly = snapped_y;
2587 }
2588 result.segments.push(PathSegment::LineTo(x, snapped_y));
2589 prev_x = x;
2590 prev_y = snapped_y;
2591 } else if is_vertical {
2592 let snapped_x = snap_x(x);
2593 if let Some(PathSegment::MoveTo(lx, _) | PathSegment::LineTo(lx, _)) =
2594 result.segments.last_mut()
2595 {
2596 *lx = snapped_x;
2597 }
2598 result.segments.push(PathSegment::LineTo(snapped_x, y));
2599 prev_x = snapped_x;
2600 prev_y = y;
2601 } else {
2602 result.segments.push(PathSegment::LineTo(x, y));
2603 prev_x = x;
2604 prev_y = y;
2605 }
2606 }
2607 PathSegment::CurveTo {
2608 x1,
2609 y1,
2610 x2,
2611 y2,
2612 x3,
2613 y3,
2614 } => {
2615 result.segments.push(PathSegment::CurveTo {
2616 x1,
2617 y1,
2618 x2,
2619 y2,
2620 x3,
2621 y3,
2622 });
2623 prev_x = x3;
2624 prev_y = y3;
2625 }
2626 PathSegment::ClosePath => {
2627 result.segments.push(PathSegment::ClosePath);
2628 }
2629 }
2630 }
2631 result
2632}
2633
2634fn render_element(
2640 pixmap: &mut Pixmap,
2641 band_state: &mut BandState,
2642 element: &DisplayElement,
2643 ctx: &RenderContext<'_>,
2644) {
2645 match element {
2646 DisplayElement::Fill { path, params } => {
2647 let mut promoted_fill: Option<FillParams> = None;
2653 let params = maybe_promote_gray_fill(params, &mut promoted_fill);
2654 let painted = params.painted_channels;
2675 let subset_channels = painted != 0 && painted != stet_graphics::device::CMYK_ALL;
2676 let opm1_cmyk = params.is_device_cmyk && params.overprint_mode == 1;
2677 let custom_spot =
2678 painted == 0 && !params.is_device_cmyk && params.color.native_cmyk.is_some();
2679 let is_k_only_cmyk =
2685 params.is_device_cmyk && params.overprint_mode == 0 && is_k_only_src(¶ms.color);
2686 let needs_overprint = params.overprint
2687 && band_state.cmyk_buffer.is_some()
2688 && params.blend_mode == 0
2689 && (subset_channels || opm1_cmyk || custom_spot || is_k_only_cmyk);
2690
2691 if needs_overprint {
2692 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2693 let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
2694 let spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2695 render_overprint_fill(
2696 pixmap,
2697 &mut cmyk_buf,
2698 &mut op_bg,
2699 &mut op_touched,
2700 &spot_mask,
2701 band_state,
2702 path,
2703 params,
2704 ctx.vp_x,
2705 ctx.vp_y,
2706 ctx.scale_x,
2707 ctx.scale_y,
2708 ctx.out_w,
2709 ctx.out_h,
2710 ctx.icc,
2711 ctx.no_aa,
2712 );
2713 band_state.cmyk_buffer = Some(cmyk_buf);
2714 band_state.restore_op_buffers(op_bg, op_touched);
2715 band_state.restore_spot_mask(spot_mask);
2716 } else {
2717 let Some(skia_path) = build_skia_path(path) else {
2718 return;
2719 };
2720 let mut temp_mask = None;
2721 let Some(mask_ref) = resolve_clip_mask(
2722 &band_state.clip_region,
2723 &mut temp_mask,
2724 ctx.out_w,
2725 ctx.out_h,
2726 ) else {
2727 return;
2728 };
2729 let paint =
2730 to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, ctx.no_aa);
2731 let transform = ctx.transform(¶ms.ctm);
2732
2733 if is_degenerate_fill(path) {
2739 let stroke = Stroke {
2740 width: 1.0,
2741 ..Stroke::default()
2742 };
2743 pixmap.stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
2744 } else {
2745 let fill_rule = to_fill_rule(¶ms.fill_rule);
2746 pixmap.fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
2747 }
2748
2749 if band_state.cmyk_buffer.is_some() {
2751 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2752 let mut spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2753 update_cmyk_buffer_for_fill(
2754 &mut cmyk_buf,
2755 &mut spot_mask,
2756 path,
2757 params,
2758 ctx.vp_x,
2759 ctx.vp_y,
2760 ctx.scale_x,
2761 ctx.scale_y,
2762 ctx.out_w,
2763 ctx.out_h,
2764 &band_state.clip_region,
2765 ctx.no_aa,
2766 ctx.icc,
2767 );
2768 band_state.cmyk_buffer = Some(cmyk_buf);
2769 band_state.restore_spot_mask(spot_mask);
2770 }
2771 }
2772 }
2773 DisplayElement::Stroke { path, params } => {
2774 let mut promoted_stroke: Option<StrokeParams> = None;
2775 let params = maybe_promote_gray_stroke(params, &mut promoted_stroke);
2776 let transform = ctx.transform(¶ms.ctm);
2777 let vp_ctm = Matrix {
2780 a: transform.sx as f64,
2781 b: transform.ky as f64,
2782 c: transform.kx as f64,
2783 d: transform.sy as f64,
2784 tx: 0.0,
2785 ty: 0.0,
2786 };
2787 let vp_params = StrokeParams {
2788 ctm: vp_ctm,
2789 ..params.clone()
2790 };
2791 let stroke = build_stroke(&vp_params, ctx.effective_dpi);
2792
2793 let adjusted;
2795 let draw_path = if params.stroke_adjust
2796 && stroke.width <= 2.0
2797 && ctm_is_device_space(¶ms.ctm)
2798 {
2799 adjusted = stroke_adjust_path_viewport(
2800 path,
2801 stroke.width as f64,
2802 ctx.scale_x as f64,
2803 ctx.scale_y as f64,
2804 ctx.vp_x as f64,
2805 ctx.vp_y as f64,
2806 );
2807 &adjusted
2808 } else {
2809 path
2810 };
2811
2812 let painted = params.painted_channels;
2820 let subset_channels = painted != 0 && painted != stet_graphics::device::CMYK_ALL;
2821 let opm1_cmyk = params.is_device_cmyk && params.overprint_mode == 1;
2822 let custom_spot =
2823 painted == 0 && !params.is_device_cmyk && params.color.native_cmyk.is_some();
2824 let is_k_only_cmyk =
2825 params.is_device_cmyk && params.overprint_mode == 0 && is_k_only_src(¶ms.color);
2826 let needs_overprint = params.overprint
2827 && band_state.cmyk_buffer.is_some()
2828 && params.blend_mode == 0
2829 && (subset_channels || opm1_cmyk || custom_spot || is_k_only_cmyk);
2830
2831 let Some(skia_path) = build_skia_path(draw_path) else {
2832 return;
2833 };
2834 let mut temp_mask = None;
2835 let Some(mask_ref) = resolve_clip_mask(
2836 &band_state.clip_region,
2837 &mut temp_mask,
2838 ctx.out_w,
2839 ctx.out_h,
2840 ) else {
2841 return;
2842 };
2843
2844 if needs_overprint {
2845 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2850 let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
2851 let spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2852 render_overprint_stroke(
2853 pixmap,
2854 &mut cmyk_buf,
2855 &mut op_bg,
2856 &mut op_touched,
2857 &spot_mask,
2858 band_state,
2859 &skia_path,
2860 &stroke,
2861 transform,
2862 params,
2863 ctx.out_w,
2864 ctx.out_h,
2865 ctx.icc,
2866 ctx.no_aa,
2867 );
2868 band_state.cmyk_buffer = Some(cmyk_buf);
2869 band_state.restore_op_buffers(op_bg, op_touched);
2870 band_state.restore_spot_mask(spot_mask);
2871 } else {
2872 let paint =
2873 to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, ctx.no_aa);
2874 pixmap.stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
2875
2876 if band_state.cmyk_buffer.is_some() {
2877 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2878 let mut spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2879 update_cmyk_buffer_for_stroke(
2880 &mut cmyk_buf,
2881 &mut spot_mask,
2882 draw_path,
2883 params,
2884 &stroke,
2885 transform,
2886 ctx.out_w,
2887 ctx.out_h,
2888 &band_state.clip_region,
2889 ctx.no_aa,
2890 ctx.icc,
2891 );
2892 band_state.cmyk_buffer = Some(cmyk_buf);
2893 band_state.restore_spot_mask(spot_mask);
2894 }
2895 }
2896 }
2897 DisplayElement::Clip { path, params } => {
2898 clip_path_unified(band_state, path, params, ctx);
2899 }
2900 DisplayElement::InitClip => {
2901 if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
2902 band_state.recycle_mask(mask);
2903 }
2904 band_state.clip_region = None;
2905 }
2906 DisplayElement::ErasePage => {
2907 pixmap.fill(Color::TRANSPARENT);
2908 if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
2909 band_state.recycle_mask(mask);
2910 }
2911 band_state.clip_region = None;
2912 }
2913 DisplayElement::Image {
2914 sample_data,
2915 params,
2916 } => {
2917 let iw = params.width;
2918 let ih = params.height;
2919 if iw == 0 || ih == 0 {
2920 return;
2921 }
2922
2923 let needs_overprint = params.overprint
2924 && band_state.cmyk_buffer.is_some()
2925 && image_supports_overprint(¶ms.color_space);
2926
2927 if needs_overprint {
2928 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2929 let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
2930 render_overprint_image(
2931 pixmap,
2932 &mut cmyk_buf,
2933 &mut op_bg,
2934 &mut op_touched,
2935 band_state,
2936 sample_data,
2937 params,
2938 ctx.vp_x,
2939 ctx.vp_y,
2940 ctx.scale_x,
2941 ctx.scale_y,
2942 ctx.out_w,
2943 ctx.out_h,
2944 ctx.icc,
2945 );
2946 band_state.cmyk_buffer = Some(cmyk_buf);
2947 band_state.restore_op_buffers(op_bg, op_touched);
2948 } else if let Some(pp) = ctx
2949 .preprocessed
2950 .and_then(|pp| pp.get(ctx.elem_idx))
2951 .and_then(|e| e.as_ref())
2952 {
2953 let Some(image_inv) = params.image_matrix.invert() else {
2956 return;
2957 };
2958 let combined = params.ctm.concat(&image_inv);
2959 let raw_transform = ctx.transform(&combined);
2960 let transform = Transform::from_row(
2961 pp.adj_sx,
2962 pp.adj_ky,
2963 pp.adj_kx,
2964 pp.adj_sy,
2965 raw_transform.tx,
2966 raw_transform.ty,
2967 );
2968
2969 let Some(img_pixmap) =
2970 stet_tiny_skia::PixmapRef::from_bytes(&pp.data, pp.width, pp.height)
2971 else {
2972 return;
2973 };
2974 #[allow(unused_assignments)]
2975 let mut temp_mask = None;
2976 let mask_ref = match &band_state.clip_region {
2977 None => None,
2978 Some(ClipRegion::Mask(m)) => Some(m as &Mask),
2979 Some(ClipRegion::Rect(rect)) => {
2980 if rect.is_empty() {
2981 return;
2982 } else if rect.is_full_page(ctx.out_w, ctx.out_h) {
2983 None
2984 } else {
2985 temp_mask = rect.make_mask(ctx.out_w, ctx.out_h);
2986 temp_mask.as_ref()
2987 }
2988 }
2989 };
2990 let img_paint = stet_tiny_skia::PixmapPaint {
2991 quality: pp.quality,
2992 opacity: params.alpha as f32,
2993 blend_mode: u8_to_blend_mode(params.blend_mode),
2994 };
2995 pixmap.draw_pixmap(0, 0, img_pixmap, &img_paint, transform, mask_ref);
2996
2997 if let Some(ref mut cmyk_buf) = band_state.cmyk_buffer {
3003 update_cmyk_buffer_for_image(
3004 cmyk_buf,
3005 sample_data,
3006 pixmap.data(),
3007 params,
3008 ctx.vp_x,
3009 ctx.vp_y,
3010 ctx.scale_x,
3011 ctx.scale_y,
3012 ctx.out_w,
3013 ctx.out_h,
3014 &band_state.clip_region,
3015 ctx.icc,
3016 );
3017 }
3018 } else {
3019 let owned_rgba;
3021 let rgba_data: &[u8] = if let Some(cached) =
3022 ctx.image_cache.and_then(|c| c.get(ctx.elem_idx))
3023 {
3024 cached
3025 } else {
3026 owned_rgba = {
3027 let mut rgba =
3028 samples_to_rgba(sample_data, params, ctx.icc, ctx.opm_zero_transparent);
3029 if params.mask_color.is_some() {
3030 apply_mask_color_rgba(&mut rgba, sample_data, params);
3031 }
3032 rgba
3033 };
3034 &owned_rgba
3035 };
3036 let expected = (iw * ih * 4) as usize;
3037 if rgba_data.len() < expected {
3038 return;
3039 }
3040 let Some(image_inv) = params.image_matrix.invert() else {
3041 return;
3042 };
3043 let combined = params.ctm.concat(&image_inv);
3044 let raw_transform = enforce_min_image_size(ctx.transform(&combined), iw, ih);
3045
3046 let prescaled =
3051 prescale_image(rgba_data, iw, ih, raw_transform, params.interpolate);
3052 let (img_data, img_w, img_h, transform) = match &prescaled {
3053 Some((data, w, h, t)) => (data.as_slice(), *w, *h, *t),
3054 None => (rgba_data, iw, ih, raw_transform),
3055 };
3056
3057 let Some(img_pixmap) =
3058 stet_tiny_skia::PixmapRef::from_bytes(img_data, img_w, img_h)
3059 else {
3060 return;
3061 };
3062 #[allow(unused_assignments)]
3063 let mut temp_mask = None;
3064 let mask_ref = match &band_state.clip_region {
3065 None => None,
3066 Some(ClipRegion::Mask(m)) => Some(m as &Mask),
3067 Some(ClipRegion::Rect(rect)) => {
3068 if rect.is_empty() {
3069 return;
3070 } else if rect.is_full_page(ctx.out_w, ctx.out_h) {
3071 None
3072 } else {
3073 temp_mask = rect.make_mask(ctx.out_w, ctx.out_h);
3074 temp_mask.as_ref()
3075 }
3076 }
3077 };
3078 let img_paint = stet_tiny_skia::PixmapPaint {
3079 quality: image_filter_quality(transform, params.interpolate),
3080 opacity: params.alpha as f32,
3081 blend_mode: u8_to_blend_mode(params.blend_mode),
3082 };
3083 pixmap.draw_pixmap(0, 0, img_pixmap, &img_paint, transform, mask_ref);
3084
3085 if let Some(ref mut cmyk_buf) = band_state.cmyk_buffer {
3089 update_cmyk_buffer_for_image(
3090 cmyk_buf,
3091 sample_data,
3092 pixmap.data(),
3093 params,
3094 ctx.vp_x,
3095 ctx.vp_y,
3096 ctx.scale_x,
3097 ctx.scale_y,
3098 ctx.out_w,
3099 ctx.out_h,
3100 &band_state.clip_region,
3101 ctx.icc,
3102 );
3103 }
3104 }
3105 }
3106 DisplayElement::AxialShading { params } => {
3107 let mut temp_mask = None;
3108 let Some(mask_ref) = resolve_clip_mask(
3109 &band_state.clip_region,
3110 &mut temp_mask,
3111 ctx.out_w,
3112 ctx.out_h,
3113 ) else {
3114 return;
3115 };
3116 render_axial_shading(
3117 pixmap,
3118 params,
3119 ctx.vp_x,
3120 ctx.vp_y,
3121 ctx.scale_x,
3122 ctx.scale_y,
3123 mask_ref,
3124 ctx.no_aa,
3125 band_state.cmyk_buffer.as_deref_mut(),
3126 ctx.icc,
3127 );
3128 }
3129 DisplayElement::RadialShading { params } => {
3130 let mut temp_mask = None;
3131 let Some(mask_ref) = resolve_clip_mask(
3132 &band_state.clip_region,
3133 &mut temp_mask,
3134 ctx.out_w,
3135 ctx.out_h,
3136 ) else {
3137 return;
3138 };
3139 render_radial_shading(
3140 pixmap,
3141 params,
3142 ctx.vp_x,
3143 ctx.vp_y,
3144 ctx.scale_x,
3145 ctx.scale_y,
3146 mask_ref,
3147 ctx.no_aa,
3148 band_state.cmyk_buffer.as_deref_mut(),
3149 ctx.icc,
3150 );
3151 }
3152 DisplayElement::MeshShading { params } => {
3153 let mut temp_mask = None;
3154 let Some(mask_ref) = resolve_clip_mask(
3155 &band_state.clip_region,
3156 &mut temp_mask,
3157 ctx.out_w,
3158 ctx.out_h,
3159 ) else {
3160 return;
3161 };
3162 render_mesh_shading(
3163 pixmap,
3164 params,
3165 ctx.vp_x,
3166 ctx.vp_y,
3167 ctx.scale_x,
3168 ctx.scale_y,
3169 mask_ref,
3170 band_state.cmyk_buffer.as_deref_mut(),
3171 ctx.icc,
3172 );
3173 }
3174 DisplayElement::PatchShading { 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_patch_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 band_state.cmyk_buffer.as_deref_mut(),
3193 ctx.icc,
3194 );
3195 }
3196 DisplayElement::PatternFill { params } => {
3197 render_pattern_fill(pixmap, band_state, params, ctx);
3198 }
3199 DisplayElement::Group { elements, params } => {
3200 render_group(pixmap, band_state, elements, params, ctx);
3201 }
3202 DisplayElement::SoftMasked {
3203 mask,
3204 content,
3205 params,
3206 mask_cache,
3207 } => {
3208 render_soft_masked(pixmap, band_state, mask, content, params, mask_cache, ctx);
3209 }
3210 DisplayElement::Text { .. } => {} DisplayElement::OcgGroup {
3212 elements,
3213 visibility,
3214 } => {
3215 let visible = ctx.layer_set.evaluate(visibility);
3221 for (idx, elem) in elements.elements().iter().enumerate() {
3222 if !visible
3223 && !matches!(elem, DisplayElement::Clip { .. } | DisplayElement::InitClip)
3224 {
3225 continue;
3226 }
3227 let elem_ctx = RenderContext {
3228 elem_idx: idx,
3229 ..*ctx
3230 };
3231 render_element(pixmap, band_state, elem, &elem_ctx);
3232 }
3233 }
3234 _ => {}
3235 }
3236}
3237
3238fn compute_group_crop(bbox: &[f64; 4], ctx: &RenderContext<'_>) -> Option<(i32, i32, u32, u32)> {
3243 let px_min = ((bbox[0] as f32 - ctx.vp_x) * ctx.scale_x).floor() as i32;
3245 let py_min = ((bbox[1] as f32 - ctx.vp_y) * ctx.scale_y).floor() as i32;
3246 let px_max = ((bbox[2] as f32 - ctx.vp_x) * ctx.scale_x).ceil() as i32;
3247 let py_max = ((bbox[3] as f32 - ctx.vp_y) * ctx.scale_y).ceil() as i32;
3248
3249 let x0 = px_min.max(0);
3251 let y0 = py_min.max(0);
3252 let x1 = px_max.min(ctx.out_w as i32);
3253 let y1 = py_max.min(ctx.out_h as i32);
3254
3255 if x0 >= x1 || y0 >= y1 {
3256 return None;
3257 }
3258
3259 let crop_w = (x1 - x0) as u32;
3260 let crop_h = (y1 - y0) as u32;
3261
3262 let crop_pixels = crop_w as u64 * crop_h as u64;
3264 let full_pixels = ctx.out_w as u64 * ctx.out_h as u64;
3265 if crop_pixels * 4 >= full_pixels * 3 {
3266 return None;
3267 }
3268
3269 Some((x0, y0, crop_w, crop_h))
3270}
3271
3272fn blend_cmyk_separable_channel(cb: f64, cs: f64, mode: u8) -> f64 {
3276 let cbi = 1.0 - cb;
3277 let csi = 1.0 - cs;
3278 let result_inv = match mode {
3279 1 => cbi * csi, 2 => cbi + csi - cbi * csi, 3 => {
3282 if cbi <= 0.5 {
3284 2.0 * cbi * csi
3285 } else {
3286 1.0 - 2.0 * (1.0 - cbi) * (1.0 - csi)
3287 }
3288 }
3289 4 => cbi.min(csi), 5 => cbi.max(csi), 6 => {
3292 if csi >= 1.0 {
3294 1.0
3295 } else {
3296 (cbi / (1.0 - csi)).min(1.0)
3297 }
3298 }
3299 7 => {
3300 if csi <= 0.0 {
3302 0.0
3303 } else {
3304 1.0 - ((1.0 - cbi) / csi).min(1.0)
3305 }
3306 }
3307 8 => {
3308 if csi <= 0.5 {
3310 2.0 * cbi * csi
3311 } else {
3312 1.0 - 2.0 * (1.0 - cbi) * (1.0 - csi)
3313 }
3314 }
3315 9 => {
3316 let d = if cbi <= 0.25 {
3318 ((16.0 * cbi - 12.0) * cbi + 4.0) * cbi
3319 } else {
3320 cbi.sqrt()
3321 };
3322 if csi <= 0.5 {
3323 cbi - (1.0 - 2.0 * csi) * cbi * (1.0 - cbi)
3324 } else {
3325 cbi + (2.0 * csi - 1.0) * (d - cbi)
3326 }
3327 }
3328 10 => (cbi - csi).abs(), 11 => cbi + csi - 2.0 * cbi * csi, _ => csi, };
3332 1.0 - result_inv.clamp(0.0, 1.0)
3333}
3334
3335fn blend_cmyk_nonseparable(cb: [f64; 4], cs: [f64; 4], mode: u8) -> [f64; 4] {
3341 fn lum(c: [f64; 3]) -> f64 {
3342 0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
3343 }
3344 fn clip_color(mut c: [f64; 3]) -> [f64; 3] {
3345 let l = lum(c);
3346 let n = c[0].min(c[1]).min(c[2]);
3347 let x = c[0].max(c[1]).max(c[2]);
3348 if n < 0.0 {
3349 for ci in c.iter_mut() {
3350 *ci = l + (*ci - l) * l / (l - n);
3351 }
3352 }
3353 if x > 1.0 {
3354 for ci in c.iter_mut() {
3355 *ci = l + (*ci - l) * (1.0 - l) / (x - l);
3356 }
3357 }
3358 c
3359 }
3360 fn set_lum(c: [f64; 3], l: f64) -> [f64; 3] {
3361 let d = l - lum(c);
3362 clip_color([c[0] + d, c[1] + d, c[2] + d])
3363 }
3364 fn sat(c: [f64; 3]) -> f64 {
3365 c[0].max(c[1]).max(c[2]) - c[0].min(c[1]).min(c[2])
3366 }
3367 fn set_sat(c: [f64; 3], s: f64) -> [f64; 3] {
3368 let mut idx = [0usize, 1, 2];
3370 idx.sort_by(|a, b| {
3371 c[*a]
3372 .partial_cmp(&c[*b])
3373 .unwrap_or(std::cmp::Ordering::Equal)
3374 });
3375 let (i_min, i_mid, i_max) = (idx[0], idx[1], idx[2]);
3376 let mut out = c;
3377 if c[i_max] > c[i_min] {
3378 out[i_mid] = (c[i_mid] - c[i_min]) * s / (c[i_max] - c[i_min]);
3379 out[i_max] = s;
3380 } else {
3381 out[i_mid] = 0.0;
3382 out[i_max] = 0.0;
3383 }
3384 out[i_min] = 0.0;
3385 out
3386 }
3387
3388 let cb_rgb = [1.0 - cb[0], 1.0 - cb[1], 1.0 - cb[2]];
3389 let cs_rgb = [1.0 - cs[0], 1.0 - cs[1], 1.0 - cs[2]];
3390 let result_rgb = match mode {
3391 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,
3396 };
3397 let result_k = if mode == 15 { cs[3] } else { cb[3] };
3401 [
3402 (1.0 - result_rgb[0]).clamp(0.0, 1.0),
3403 (1.0 - result_rgb[1]).clamp(0.0, 1.0),
3404 (1.0 - result_rgb[2]).clamp(0.0, 1.0),
3405 result_k,
3406 ]
3407}
3408
3409fn ps_path_bbox(path: &PsPath) -> Option<(f64, f64, f64, f64)> {
3414 let mut it = path.segments.iter().filter_map(|seg| match *seg {
3415 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => Some(vec![(x, y)]),
3416 PathSegment::CurveTo {
3417 x1,
3418 y1,
3419 x2,
3420 y2,
3421 x3,
3422 y3,
3423 } => Some(vec![(x1, y1), (x2, y2), (x3, y3)]),
3424 PathSegment::ClosePath => None,
3425 });
3426 let first = it.next()?.into_iter().next()?;
3427 let (mut x0, mut y0) = first;
3428 let (mut x1, mut y1) = first;
3429 for seg_points in std::iter::once(vec![first]).chain(it) {
3430 for (x, y) in seg_points {
3431 x0 = x0.min(x);
3432 y0 = y0.min(y);
3433 x1 = x1.max(x);
3434 y1 = y1.max(y);
3435 }
3436 }
3437 Some((x0, y0, x1, y1))
3438}
3439
3440fn bbox_contains(outer: (f64, f64, f64, f64), inner: (f64, f64, f64, f64), tolerance: f64) -> bool {
3443 inner.0 >= outer.0 - tolerance
3444 && inner.1 >= outer.1 - tolerance
3445 && inner.2 <= outer.2 + tolerance
3446 && inner.3 <= outer.3 + tolerance
3447}
3448
3449fn compute_obscured_fill_skips(elements: &DisplayList) -> Vec<usize> {
3475 let mut skips = Vec::new();
3476 let els = elements.elements();
3477 for i in 0..els.len() {
3478 let DisplayElement::Fill {
3479 path: parent_path,
3480 params: parent_params,
3481 } = &els[i]
3482 else {
3483 continue;
3484 };
3485 if (parent_params.alpha - 1.0).abs() > 1e-6 || parent_params.blend_mode != 0 {
3486 continue;
3487 }
3488 let Some(parent_bbox) = ps_path_bbox(parent_path) else {
3489 continue;
3490 };
3491 let mut j = i + 1;
3495 let mut clips_ok = true;
3496 while j < els.len() {
3497 match &els[j] {
3498 DisplayElement::InitClip => {}
3499 DisplayElement::Clip {
3500 path: clip_path, ..
3501 } => match ps_path_bbox(clip_path) {
3502 Some(cb) if bbox_contains(cb, parent_bbox, 0.5) => {}
3503 _ => {
3504 clips_ok = false;
3505 break;
3506 }
3507 },
3508 _ => break,
3509 }
3510 j += 1;
3511 }
3512 if !clips_ok {
3513 continue;
3514 }
3515 let Some(DisplayElement::Group {
3516 elements: group_elements,
3517 params: group_params,
3518 }) = els.get(j)
3519 else {
3520 continue;
3521 };
3522 if !group_params.isolated
3523 || (group_params.alpha - 1.0).abs() > 1e-6
3524 || group_params.blend_mode != 0
3525 {
3526 continue;
3527 }
3528 let group_bbox = (
3532 group_params.bbox[0],
3533 group_params.bbox[1],
3534 group_params.bbox[2],
3535 group_params.bbox[3],
3536 );
3537 if !bbox_contains(group_bbox, parent_bbox, 0.5) {
3538 continue;
3539 }
3540 let inner_els = group_elements.elements();
3543 let mut k = 0;
3544 let mut inner_clips_ok = true;
3545 while k < inner_els.len() {
3546 match &inner_els[k] {
3547 DisplayElement::InitClip => {}
3548 DisplayElement::Clip {
3549 path: clip_path, ..
3550 } => match ps_path_bbox(clip_path) {
3551 Some(cb) if bbox_contains(cb, parent_bbox, 0.5) => {}
3552 _ => {
3553 inner_clips_ok = false;
3554 break;
3555 }
3556 },
3557 _ => break,
3558 }
3559 k += 1;
3560 }
3561 if !inner_clips_ok {
3562 continue;
3563 }
3564 let Some(DisplayElement::Fill {
3565 path: group_path,
3566 params: group_fill_params,
3567 }) = inner_els.get(k)
3568 else {
3569 continue;
3570 };
3571 if (group_fill_params.alpha - 1.0).abs() > 1e-6 || group_fill_params.blend_mode != 0 {
3572 continue;
3573 }
3574 if paths_approximately_equal(parent_path, group_path, 0.5) {
3575 skips.push(i);
3576 }
3577 }
3578 skips
3579}
3580
3581fn paths_approximately_equal(a: &PsPath, b: &PsPath, tolerance: f64) -> bool {
3587 if a.segments.len() != b.segments.len() {
3588 return false;
3589 }
3590 for (sa, sb) in a.segments.iter().zip(b.segments.iter()) {
3591 let close_pair = |(x1, y1): (f64, f64), (x2, y2): (f64, f64)| -> bool {
3592 (x1 - x2).abs() <= tolerance && (y1 - y2).abs() <= tolerance
3593 };
3594 match (sa, sb) {
3595 (PathSegment::MoveTo(x1, y1), PathSegment::MoveTo(x2, y2)) => {
3596 if !close_pair((*x1, *y1), (*x2, *y2)) {
3597 return false;
3598 }
3599 }
3600 (PathSegment::LineTo(x1, y1), PathSegment::LineTo(x2, y2)) => {
3601 if !close_pair((*x1, *y1), (*x2, *y2)) {
3602 return false;
3603 }
3604 }
3605 (
3606 PathSegment::CurveTo {
3607 x1: ax1,
3608 y1: ay1,
3609 x2: ax2,
3610 y2: ay2,
3611 x3: ax3,
3612 y3: ay3,
3613 },
3614 PathSegment::CurveTo {
3615 x1: bx1,
3616 y1: by1,
3617 x2: bx2,
3618 y2: by2,
3619 x3: bx3,
3620 y3: by3,
3621 },
3622 ) => {
3623 if !close_pair((*ax1, *ay1), (*bx1, *by1))
3624 || !close_pair((*ax2, *ay2), (*bx2, *by2))
3625 || !close_pair((*ax3, *ay3), (*bx3, *by3))
3626 {
3627 return false;
3628 }
3629 }
3630 (PathSegment::ClosePath, PathSegment::ClosePath) => {}
3631 _ => return false,
3632 }
3633 }
3634 true
3635}
3636
3637fn render_group(
3641 pixmap: &mut Pixmap,
3642 band_state: &mut BandState,
3643 elements: &DisplayList,
3644 params: &stet_graphics::display_list::GroupParams,
3645 ctx: &RenderContext<'_>,
3646) {
3647 if params.knockout {
3648 render_knockout_group(pixmap, band_state, elements, params, ctx);
3649 return;
3650 }
3651
3652 let crop = compute_group_crop(¶ms.bbox, ctx);
3653
3654 let (eff_w, eff_h, crop_x, crop_y, eff_vp_x, eff_vp_y) = match crop {
3655 Some((cx, cy, cw, ch)) => (
3656 cw,
3657 ch,
3658 cx,
3659 cy,
3660 ctx.vp_x + cx as f32 / ctx.scale_x,
3661 ctx.vp_y + cy as f32 / ctx.scale_y,
3662 ),
3663 None => (ctx.out_w, ctx.out_h, 0, 0, ctx.vp_x, ctx.vp_y),
3664 };
3665
3666 let Some(mut offscreen) = Pixmap::new(eff_w, eff_h) else {
3667 return;
3668 };
3669
3670 use stet_graphics::display_list::GroupColorSpace;
3678
3679 let needs_group_cmyk = has_overprint_elements(elements)
3685 || band_state.cmyk_buffer.is_some()
3686 || params.color_space == GroupColorSpace::DeviceCMYK
3687 || has_cmyk_group(elements);
3688
3689 let force_cmyk_compose =
3696 std::env::var_os("STET_FORCE_CMYK_COMPOSITE_BACK").as_deref() == Some("1".as_ref());
3697 let plan_cmyk_compose = match ctx.knockout_painter_pass {
3708 KnockoutPainterPass::CoveragePass => false,
3709 KnockoutPainterPass::ColorPass => {
3710 !params.isolated
3711 && params.blend_mode != 0
3712 && needs_group_cmyk
3713 && band_state.cmyk_buffer.is_some()
3714 && group_content_is_native_cmyk(elements)
3715 }
3716 KnockoutPainterPass::None if force_cmyk_compose => {
3717 !params.isolated
3735 && matches!(params.blend_mode, 10..=15)
3736 && needs_group_cmyk
3737 && band_state.cmyk_buffer.is_some()
3738 && group_content_is_native_cmyk(elements)
3739 }
3740 KnockoutPainterPass::None => {
3741 let inversion_sensitive = !params.isolated
3746 && matches!(params.blend_mode, 10..=15)
3747 && group_only_native_cmyk_fills(elements);
3748 let cmyk_group_blend = !params.isolated
3763 && ctx.parent_group_isolated
3764 && params.blend_mode != 0
3765 && params.color_space == GroupColorSpace::DeviceCMYK
3766 && needs_group_cmyk
3767 && band_state.cmyk_buffer.is_some()
3768 && group_content_is_native_cmyk(elements);
3769 inversion_sensitive || cmyk_group_blend
3770 }
3771 };
3772 let needs_alpha_extraction = !params.isolated
3777 && params.blend_mode != 0
3778 && !plan_cmyk_compose
3779 && !ctx.alpha_extraction_pass;
3780 let needs_backdrop_preload =
3781 !params.isolated && (params.blend_mode == 0 || plan_cmyk_compose || needs_alpha_extraction);
3782 let backdrop = if needs_backdrop_preload {
3783 let data = if crop.is_some() {
3784 copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h)
3785 } else {
3786 pixmap.data().to_vec()
3787 };
3788 offscreen.data_mut().copy_from_slice(&data);
3789 Some(data)
3790 } else {
3791 None
3792 };
3793 let group_cmyk = if needs_group_cmyk {
3794 let buf_size = eff_w as usize * eff_h as usize * 4;
3795 let mut buf = vec![0.0f32; buf_size];
3796 if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
3797 let parent_stride = ctx.out_w as usize * 4;
3798 let group_stride = eff_w as usize * 4;
3799 for gy in 0..eff_h as usize {
3800 let py = crop_y as usize + gy;
3801 if py < ctx.out_h as usize {
3802 let p_start = py * parent_stride + crop_x as usize * 4;
3803 let g_start = gy * group_stride;
3804 let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
3805 buf[g_start..g_start + copy_len]
3806 .copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
3807 }
3808 }
3809 }
3810 Some(buf)
3811 } else {
3812 None
3813 };
3814
3815 let backdrop_cmyk: Option<Vec<f32>> = if !params.isolated {
3820 group_cmyk.clone()
3821 } else {
3822 None
3823 };
3824
3825 let mut group_band = BandState {
3826 clip_region: None,
3827 spare_mask: None,
3828 clip_mask_cache: HashMap::new(),
3829 clip_mask_seen: HashSet::new(),
3830 mask_pool: Vec::new(),
3831 cmyk_buffer: group_cmyk,
3832 op_bg_snapshot: None,
3833 op_touched: None,
3834 spot_mask: None,
3835 };
3836
3837 let group_ctx = RenderContext {
3838 vp_x: eff_vp_x,
3839 vp_y: eff_vp_y,
3840 scale_x: ctx.scale_x,
3841 scale_y: ctx.scale_y,
3842 out_w: eff_w,
3843 out_h: eff_h,
3844 effective_dpi: ctx.effective_dpi,
3845 icc: ctx.icc,
3846 image_cache: None, preprocessed: None,
3848 elem_idx: 0,
3849 no_aa: ctx.no_aa,
3850 opm_zero_transparent: ctx.opm_zero_transparent,
3851 knockout_painter_pass: ctx.knockout_painter_pass,
3852 parent_group_isolated: params.isolated,
3854 alpha_extraction_pass: ctx.alpha_extraction_pass,
3855 layer_set: ctx.layer_set,
3856 };
3857
3858 let skip_indices = compute_obscured_fill_skips(elements);
3859 for (idx, elem) in elements.elements().iter().enumerate() {
3860 if skip_indices.contains(&idx) {
3861 continue;
3862 }
3863 let elem_ctx = RenderContext {
3864 elem_idx: idx,
3865 ..group_ctx
3866 };
3867 render_element(&mut offscreen, &mut group_band, elem, &elem_ctx);
3868 }
3869
3870 let alpha_offscreen = if needs_alpha_extraction {
3874 let mut iso = Pixmap::new(eff_w, eff_h);
3875 if let Some(ref mut iso_pm) = iso {
3876 let mut iso_band = BandState {
3877 clip_region: None,
3878 spare_mask: None,
3879 clip_mask_cache: HashMap::new(),
3880 clip_mask_seen: HashSet::new(),
3881 mask_pool: Vec::new(),
3882 cmyk_buffer: None,
3883 op_bg_snapshot: None,
3884 op_touched: None,
3885 spot_mask: None,
3886 };
3887 let iso_ctx = RenderContext {
3888 parent_group_isolated: true,
3889 alpha_extraction_pass: true,
3890 ..group_ctx
3891 };
3892 for (idx, elem) in elements.elements().iter().enumerate() {
3893 let elem_ctx = RenderContext {
3894 elem_idx: idx,
3895 ..iso_ctx
3896 };
3897 render_element(iso_pm, &mut iso_band, elem, &elem_ctx);
3898 }
3899 }
3900 iso
3901 } else {
3902 None
3903 };
3904
3905 let mut temp_mask = None;
3906 let mask_ref = match resolve_clip_mask(
3907 &band_state.clip_region,
3908 &mut temp_mask,
3909 ctx.out_w,
3910 ctx.out_h,
3911 ) {
3912 None => return, Some(m) => m,
3914 };
3915
3916 let coverage_params;
3921 let effective_params: &stet_graphics::display_list::GroupParams =
3922 if ctx.knockout_painter_pass == KnockoutPainterPass::CoveragePass {
3923 coverage_params = stet_graphics::display_list::GroupParams {
3924 alpha: 1.0,
3925 blend_mode: 0,
3926 ..params.clone()
3927 };
3928 &coverage_params
3929 } else {
3930 params
3931 };
3932
3933 let mut cmyk_compose_done = false;
3934 if let Some(backdrop) = &backdrop {
3935 let inner_cmyk = group_band.cmyk_buffer.as_deref();
3953 let pre_cmyk = backdrop_cmyk.as_deref();
3954 if plan_cmyk_compose && let (Some(inner), Some(pre)) = (inner_cmyk, pre_cmyk) {
3955 composite_non_isolated_cmyk(
3956 pixmap,
3957 band_state.cmyk_buffer.as_deref_mut(),
3958 &offscreen,
3959 inner,
3960 pre,
3961 backdrop,
3962 effective_params,
3963 mask_ref,
3964 crop_x,
3965 crop_y,
3966 ctx.icc,
3967 );
3968 cmyk_compose_done = true;
3969 } else if let Some(ref alpha_os) = alpha_offscreen {
3970 composite_non_isolated_extracted(
3971 pixmap,
3972 &offscreen,
3973 alpha_os,
3974 backdrop,
3975 effective_params,
3976 mask_ref,
3977 crop_x,
3978 crop_y,
3979 );
3980 } else {
3981 composite_non_isolated_group_cropped(
3982 pixmap,
3983 &offscreen,
3984 backdrop,
3985 effective_params,
3986 mask_ref,
3987 crop_x,
3988 crop_y,
3989 );
3990 }
3991 } else {
3992 let paint = stet_tiny_skia::PixmapPaint {
3993 opacity: effective_params.alpha as f32,
3994 blend_mode: u8_to_blend_mode(effective_params.blend_mode),
3995 quality: stet_tiny_skia::FilterQuality::Nearest,
3996 };
3997 pixmap.draw_pixmap(
3998 crop_x,
3999 crop_y,
4000 offscreen.as_ref(),
4001 &paint,
4002 Transform::identity(),
4003 mask_ref,
4004 );
4005 }
4006
4007 if !cmyk_compose_done
4013 && let (Some(group_cmyk), Some(parent_cmyk)) =
4014 (&group_band.cmyk_buffer, &mut band_state.cmyk_buffer)
4015 {
4016 copy_cmyk_buffer_to_parent(
4017 parent_cmyk,
4018 group_cmyk,
4019 offscreen.data(),
4020 crop_x as usize,
4021 crop_y as usize,
4022 eff_w as usize,
4023 eff_h as usize,
4024 ctx.out_w as usize,
4025 ctx.out_h as usize,
4026 );
4027 }
4028}
4029
4030#[allow(clippy::too_many_arguments)]
4042fn composite_non_isolated_cmyk(
4043 target: &mut Pixmap,
4044 parent_cmyk: Option<&mut [f32]>,
4045 source: &Pixmap,
4046 source_cmyk: &[f32],
4047 backdrop_cmyk: &[f32],
4048 backdrop_pixels: &[u8],
4049 params: &stet_graphics::display_list::GroupParams,
4050 clip_mask: Option<&stet_tiny_skia::Mask>,
4051 crop_x: i32,
4052 crop_y: i32,
4053 icc: Option<&IccCache>,
4054) {
4055 let cw = source.width() as usize;
4056 let ch = source.height() as usize;
4057 let target_w = target.width() as usize;
4058 let target_h = target.height() as usize;
4059
4060 let opacity = params.alpha.clamp(0.0, 1.0);
4061 let blend_mode = params.blend_mode;
4062 let is_nonseparable = matches!(blend_mode, 12..=15);
4063
4064 let target_data = target.data_mut();
4065 let target_stride = target_w * 4;
4066 let group_stride = cw * 4;
4067
4068 let clip_data = clip_mask.map(|m| m.data());
4069
4070 for gy in 0..ch {
4071 let ty = crop_y + gy as i32;
4072 if ty < 0 || ty as usize >= target_h {
4073 continue;
4074 }
4075 let ty = ty as usize;
4076 let group_row = gy * group_stride;
4077 let target_row = ty * target_stride;
4078
4079 for gx in 0..cw {
4080 let tx = crop_x + gx as i32;
4081 if tx < 0 || tx as usize >= target_w {
4082 continue;
4083 }
4084 let tx = tx as usize;
4085 let gi = group_row + gx * 4;
4086 let ti = target_row + tx * 4;
4087
4088 let bc = backdrop_cmyk[gi] as f64;
4090 let bm = backdrop_cmyk[gi + 1] as f64;
4091 let by_ = backdrop_cmyk[gi + 2] as f64;
4092 let bk = backdrop_cmyk[gi + 3] as f64;
4093 let sc = source_cmyk[gi] as f64;
4094 let sm = source_cmyk[gi + 1] as f64;
4095 let sy_ = source_cmyk[gi + 2] as f64;
4096 let sk = source_cmyk[gi + 3] as f64;
4097 if (sc - bc).abs() < 1.0 / 255.0
4098 && (sm - bm).abs() < 1.0 / 255.0
4099 && (sy_ - by_).abs() < 1.0 / 255.0
4100 && (sk - bk).abs() < 1.0 / 255.0
4101 {
4102 continue;
4103 }
4104
4105 let cov = if let Some(cd) = clip_data {
4107 cd[ty * target_w + tx] as f64 / 255.0
4108 } else {
4109 1.0
4110 };
4111 if cov <= 0.0 {
4112 continue;
4113 }
4114
4115 let backdrop_alpha = backdrop_pixels[gi + 3];
4134 let backdrop_transparent = backdrop_alpha == 0;
4135
4136 let mix = cov * opacity;
4137 let dst_a = target_data[ti + 3] as f64 / 255.0;
4138
4139 if backdrop_transparent {
4140 let src_data = source.data();
4153 let src_a_pm = src_data[gi + 3] as f64 / 255.0;
4154 if src_a_pm <= 0.0 {
4155 continue;
4156 }
4157 let (full_r, full_g, full_b) = icc
4162 .and_then(|i| i.convert_cmyk_readonly(sc, sm, sy_, sk))
4163 .unwrap_or_else(|| cmyk_to_rgb_plrm(sc, sm, sy_, sk));
4164 let alpha_s = mix;
4165 let inv_sa = 1.0 - alpha_s;
4166 let dst_r_pm = target_data[ti] as f64 / 255.0;
4167 let dst_g_pm = target_data[ti + 1] as f64 / 255.0;
4168 let dst_b_pm = target_data[ti + 2] as f64 / 255.0;
4169 let out_r = full_r * alpha_s + dst_r_pm * inv_sa;
4170 let out_g = full_g * alpha_s + dst_g_pm * inv_sa;
4171 let out_b = full_b * alpha_s + dst_b_pm * inv_sa;
4172 let out_a = alpha_s + dst_a * inv_sa;
4173 target_data[ti] = (out_r * 255.0).round().clamp(0.0, 255.0) as u8;
4174 target_data[ti + 1] = (out_g * 255.0).round().clamp(0.0, 255.0) as u8;
4175 target_data[ti + 2] = (out_b * 255.0).round().clamp(0.0, 255.0) as u8;
4176 target_data[ti + 3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4177 continue;
4178 }
4179
4180 let (rc, rm, ry, rk) = if is_nonseparable {
4182 let r = blend_cmyk_nonseparable([bc, bm, by_, bk], [sc, sm, sy_, sk], blend_mode);
4183 (r[0], r[1], r[2], r[3])
4184 } else {
4185 (
4186 blend_cmyk_separable_channel(bc, sc, blend_mode),
4187 blend_cmyk_separable_channel(bm, sm, blend_mode),
4188 blend_cmyk_separable_channel(by_, sy_, blend_mode),
4189 blend_cmyk_separable_channel(bk, sk, blend_mode),
4190 )
4191 };
4192
4193 let (new_r, new_g, new_b) = icc
4194 .and_then(|i| i.convert_cmyk_readonly(rc, rm, ry, rk))
4195 .unwrap_or_else(|| cmyk_to_rgb_plrm(rc, rm, ry, rk));
4196
4197 let alpha_s = mix;
4210 let alpha_b = dst_a;
4211 let out_a = alpha_s + alpha_b * (1.0 - alpha_s);
4212 if out_a <= 0.0 {
4213 continue;
4214 }
4215 let (dst_r, dst_g, dst_b) = if alpha_b > 0.0 {
4216 let inv_a = 1.0 / alpha_b;
4217 (
4218 (target_data[ti] as f64 / 255.0) * inv_a,
4219 (target_data[ti + 1] as f64 / 255.0) * inv_a,
4220 (target_data[ti + 2] as f64 / 255.0) * inv_a,
4221 )
4222 } else {
4223 (0.0, 0.0, 0.0)
4224 };
4225 let coef_b = alpha_s * alpha_b;
4233 let coef_s = alpha_s * (1.0 - alpha_b);
4234 let coef_d = (1.0 - alpha_s) * alpha_b;
4235 let out_r = (coef_s * new_r + coef_b * new_r + coef_d * dst_r) / out_a;
4236 let out_g = (coef_s * new_g + coef_b * new_g + coef_d * dst_g) / out_a;
4237 let out_b = (coef_s * new_b + coef_b * new_b + coef_d * dst_b) / out_a;
4238
4239 target_data[ti] = (out_r * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4240 target_data[ti + 1] = (out_g * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4241 target_data[ti + 2] = (out_b * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4242 target_data[ti + 3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4243 }
4244 }
4245
4246 if let Some(parent_cmyk) = parent_cmyk {
4251 for gy in 0..ch {
4252 let ty = crop_y + gy as i32;
4253 if ty < 0 || ty as usize >= target_h {
4254 continue;
4255 }
4256 let ty = ty as usize;
4257 let group_row = gy * group_stride;
4258 let parent_row = ty * target_stride;
4259
4260 for gx in 0..cw {
4261 let tx = crop_x + gx as i32;
4262 if tx < 0 || tx as usize >= target_w {
4263 continue;
4264 }
4265 let tx = tx as usize;
4266 let gi = group_row + gx * 4;
4267 let pi = parent_row + tx * 4;
4268
4269 let bc = backdrop_cmyk[gi] as f64;
4270 let bm = backdrop_cmyk[gi + 1] as f64;
4271 let by_ = backdrop_cmyk[gi + 2] as f64;
4272 let bk = backdrop_cmyk[gi + 3] as f64;
4273 let sc = source_cmyk[gi] as f64;
4274 let sm = source_cmyk[gi + 1] as f64;
4275 let sy_ = source_cmyk[gi + 2] as f64;
4276 let sk = source_cmyk[gi + 3] as f64;
4277 if (sc - bc).abs() < 1.0 / 255.0
4278 && (sm - bm).abs() < 1.0 / 255.0
4279 && (sy_ - by_).abs() < 1.0 / 255.0
4280 && (sk - bk).abs() < 1.0 / 255.0
4281 {
4282 continue;
4283 }
4284
4285 let backdrop_transparent = backdrop_pixels[gi + 3] == 0;
4291 let (rc, rm, ry, rk) = if backdrop_transparent {
4292 (sc, sm, sy_, sk)
4293 } else if is_nonseparable {
4294 let r =
4295 blend_cmyk_nonseparable([bc, bm, by_, bk], [sc, sm, sy_, sk], blend_mode);
4296 (r[0], r[1], r[2], r[3])
4297 } else {
4298 (
4299 blend_cmyk_separable_channel(bc, sc, blend_mode),
4300 blend_cmyk_separable_channel(bm, sm, blend_mode),
4301 blend_cmyk_separable_channel(by_, sy_, blend_mode),
4302 blend_cmyk_separable_channel(bk, sk, blend_mode),
4303 )
4304 };
4305 parent_cmyk[pi] = rc as f32;
4306 parent_cmyk[pi + 1] = rm as f32;
4307 parent_cmyk[pi + 2] = ry as f32;
4308 parent_cmyk[pi + 3] = rk as f32;
4309 }
4310 }
4311 }
4312}
4313
4314fn render_knockout_group(
4319 pixmap: &mut Pixmap,
4320 band_state: &mut BandState,
4321 elements: &DisplayList,
4322 params: &stet_graphics::display_list::GroupParams,
4323 ctx: &RenderContext<'_>,
4324) {
4325 let crop = compute_group_crop(¶ms.bbox, ctx);
4326
4327 let (eff_w, eff_h, crop_x, crop_y, eff_vp_x, eff_vp_y) = match crop {
4328 Some((cx, cy, cw, ch)) => (
4329 cw,
4330 ch,
4331 cx,
4332 cy,
4333 ctx.vp_x + cx as f32 / ctx.scale_x,
4334 ctx.vp_y + cy as f32 / ctx.scale_y,
4335 ),
4336 None => (ctx.out_w, ctx.out_h, 0, 0, ctx.vp_x, ctx.vp_y),
4337 };
4338
4339 let Some(mut offscreen) = Pixmap::new(eff_w, eff_h) else {
4340 return;
4341 };
4342
4343 let initial_backdrop = if !params.isolated {
4344 if crop.is_some() {
4345 copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h)
4346 } else {
4347 pixmap.data().to_vec()
4348 }
4349 } else {
4350 vec![0u8; (eff_w * eff_h * 4) as usize]
4351 };
4352
4353 let Some(mut accumulated) = Pixmap::new(eff_w, eff_h) else {
4354 return;
4355 };
4356 accumulated.data_mut().copy_from_slice(&initial_backdrop);
4357
4358 let needs_cmyk = has_overprint_elements(elements) || band_state.cmyk_buffer.is_some();
4360 let initial_cmyk = if needs_cmyk {
4361 let buf_size = eff_w as usize * eff_h as usize * 4;
4362 let mut buf = vec![0.0f32; buf_size];
4363 if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
4364 let parent_stride = ctx.out_w as usize * 4;
4365 let group_stride = eff_w as usize * 4;
4366 for gy in 0..eff_h as usize {
4367 let py = crop_y as usize + gy;
4368 if py < ctx.out_h as usize {
4369 let p_start = py * parent_stride + crop_x as usize * 4;
4370 let g_start = gy * group_stride;
4371 let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
4372 buf[g_start..g_start + copy_len]
4373 .copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
4374 }
4375 }
4376 }
4377 Some(buf)
4378 } else {
4379 None
4380 };
4381
4382 let mut accumulated_cmyk = initial_cmyk.clone();
4383
4384 let group_ctx = RenderContext {
4389 vp_x: eff_vp_x,
4390 vp_y: eff_vp_y,
4391 scale_x: ctx.scale_x,
4392 scale_y: ctx.scale_y,
4393 out_w: eff_w,
4394 out_h: eff_h,
4395 effective_dpi: ctx.effective_dpi,
4396 icc: ctx.icc,
4397 image_cache: None,
4398 preprocessed: None,
4399 elem_idx: 0,
4400 no_aa: true,
4401 opm_zero_transparent: ctx.opm_zero_transparent,
4402 knockout_painter_pass: ctx.knockout_painter_pass,
4403 parent_group_isolated: true,
4407 alpha_extraction_pass: false,
4408 layer_set: ctx.layer_set,
4409 };
4410
4411 let mut ko_band = BandState {
4415 clip_region: None,
4416 spare_mask: None,
4417 clip_mask_cache: HashMap::new(),
4418 clip_mask_seen: HashSet::new(),
4419 mask_pool: Vec::new(),
4420 cmyk_buffer: None,
4421 op_bg_snapshot: None,
4422 op_touched: None,
4423 spot_mask: None,
4424 };
4425
4426 let mut coverage_offscreen: Option<Pixmap> = None;
4429
4430 for elem in elements.elements() {
4431 match elem {
4432 DisplayElement::Clip { .. } | DisplayElement::InitClip => {
4434 render_element(&mut offscreen, &mut ko_band, elem, &group_ctx);
4435 }
4436 DisplayElement::Group { .. } => {
4446 let pass1_ctx = RenderContext {
4453 knockout_painter_pass: KnockoutPainterPass::ColorPass,
4454 ..group_ctx
4455 };
4456 offscreen.data_mut().copy_from_slice(&initial_backdrop);
4457 ko_band.cmyk_buffer = initial_cmyk.clone();
4458 render_element(&mut offscreen, &mut ko_band, elem, &pass1_ctx);
4459 let pass1_cmyk = ko_band.cmyk_buffer.take();
4460
4461 let cov = match coverage_offscreen.as_mut() {
4466 Some(p) => {
4467 p.data_mut().fill(0);
4468 p
4469 }
4470 None => {
4471 let Some(p) = Pixmap::new(eff_w, eff_h) else {
4472 replace_changed_pixels(
4476 accumulated.data_mut(),
4477 offscreen.data(),
4478 &initial_backdrop,
4479 );
4480 if let (Some(p1), Some(acc)) = (&pass1_cmyk, &mut accumulated_cmyk) {
4481 replace_changed_cmyk(acc, p1, offscreen.data(), &initial_backdrop);
4482 }
4483 continue;
4484 };
4485 coverage_offscreen = Some(p);
4486 coverage_offscreen.as_mut().unwrap()
4487 }
4488 };
4489 ko_band.cmyk_buffer = None;
4490 let coverage_ctx = RenderContext {
4495 knockout_painter_pass: KnockoutPainterPass::CoveragePass,
4496 ..group_ctx
4497 };
4498 render_element(cov, &mut ko_band, elem, &coverage_ctx);
4499
4500 replace_with_coverage_mask(accumulated.data_mut(), offscreen.data(), cov.data());
4504
4505 if let (Some(p1_cmyk), Some(acc_cmyk)) = (&pass1_cmyk, &mut accumulated_cmyk) {
4506 replace_cmyk_with_coverage_mask(acc_cmyk, p1_cmyk, cov.data());
4507 }
4508 ko_band.cmyk_buffer = None;
4509 }
4510 _ => {
4514 offscreen.data_mut().copy_from_slice(&initial_backdrop);
4515
4516 ko_band.cmyk_buffer = initial_cmyk.clone();
4517
4518 render_element(&mut offscreen, &mut ko_band, elem, &group_ctx);
4519
4520 if let (Some(elem_cmyk), Some(acc_cmyk)) =
4521 (&ko_band.cmyk_buffer, &mut accumulated_cmyk)
4522 {
4523 replace_changed_cmyk(acc_cmyk, elem_cmyk, offscreen.data(), &initial_backdrop);
4524 }
4525 ko_band.cmyk_buffer = None;
4526
4527 replace_changed_pixels(accumulated.data_mut(), offscreen.data(), &initial_backdrop);
4528 }
4529 }
4530 }
4531
4532 let mut temp_mask = None;
4533 let mask_ref = resolve_clip_mask(
4534 &band_state.clip_region,
4535 &mut temp_mask,
4536 ctx.out_w,
4537 ctx.out_h,
4538 );
4539 let mask_ref = match mask_ref {
4540 None => return,
4541 Some(m) => m,
4542 };
4543
4544 composite_non_isolated_group_cropped(
4545 pixmap,
4546 &accumulated,
4547 &initial_backdrop,
4548 params,
4549 mask_ref,
4550 crop_x,
4551 crop_y,
4552 );
4553
4554 if let (Some(acc_cmyk), Some(parent_cmyk)) = (&accumulated_cmyk, &mut band_state.cmyk_buffer) {
4555 copy_cmyk_buffer_to_parent(
4556 parent_cmyk,
4557 acc_cmyk,
4558 accumulated.data(),
4559 crop_x as usize,
4560 crop_y as usize,
4561 eff_w as usize,
4562 eff_h as usize,
4563 ctx.out_w as usize,
4564 ctx.out_h as usize,
4565 );
4566 }
4567}
4568fn replace_with_coverage_mask(target: &mut [u8], source: &[u8], coverage: &[u8]) {
4576 for i in (0..target.len()).step_by(4) {
4577 let cov_a = coverage[i + 3];
4578 if cov_a == 0 {
4579 continue;
4580 }
4581 if cov_a == 255 {
4582 target[i..i + 4].copy_from_slice(&source[i..i + 4]);
4583 continue;
4584 }
4585 let a = cov_a as u32;
4586 let inv = 255 - a;
4587 for c in 0..4 {
4588 let s = source[i + c] as u32;
4589 let t = target[i + c] as u32;
4590 target[i + c] = ((s * a + t * inv + 127) / 255) as u8;
4591 }
4592 }
4593}
4594
4595fn replace_cmyk_with_coverage_mask(target: &mut [f32], source: &[f32], coverage: &[u8]) {
4599 let pixel_count = target.len() / 4;
4600 for i in 0..pixel_count {
4601 let pi = i * 4;
4602 let cov_a = coverage[pi + 3];
4603 if cov_a == 0 {
4604 continue;
4605 }
4606 if cov_a == 255 {
4607 target[pi..pi + 4].copy_from_slice(&source[pi..pi + 4]);
4608 continue;
4609 }
4610 let a = cov_a as f32 / 255.0;
4611 let inv = 1.0 - a;
4612 for c in 0..4 {
4613 target[pi + c] = source[pi + c] * a + target[pi + c] * inv;
4614 }
4615 }
4616}
4617
4618fn replace_changed_pixels(target: &mut [u8], source: &[u8], backdrop: &[u8]) {
4622 for i in (0..target.len()).step_by(4) {
4623 if source[i] != backdrop[i]
4624 || source[i + 1] != backdrop[i + 1]
4625 || source[i + 2] != backdrop[i + 2]
4626 || source[i + 3] != backdrop[i + 3]
4627 {
4628 target[i..i + 4].copy_from_slice(&source[i..i + 4]);
4629 }
4630 }
4631}
4632
4633#[allow(clippy::too_many_arguments)]
4637fn copy_cmyk_buffer_to_parent(
4638 parent_cmyk: &mut [f32],
4639 group_cmyk: &[f32],
4640 group_pixels: &[u8],
4641 crop_x: usize,
4642 crop_y: usize,
4643 group_w: usize,
4644 group_h: usize,
4645 parent_w: usize,
4646 parent_h: usize,
4647) {
4648 let parent_stride = parent_w * 4;
4649 let group_stride = group_w * 4;
4650 for gy in 0..group_h {
4651 let py = crop_y + gy;
4652 if py >= parent_h {
4653 break;
4654 }
4655 for gx in 0..group_w {
4656 let px = crop_x + gx;
4657 if px >= parent_w {
4658 break;
4659 }
4660 let g_pixel_idx = (gy * group_w + gx) * 4;
4665 let g_cmyk_idx = gy * group_stride + gx * 4;
4666 if group_pixels[g_pixel_idx + 3] > 0
4667 && (group_cmyk[g_cmyk_idx] != 0.0
4668 || group_cmyk[g_cmyk_idx + 1] != 0.0
4669 || group_cmyk[g_cmyk_idx + 2] != 0.0
4670 || group_cmyk[g_cmyk_idx + 3] != 0.0)
4671 {
4672 let p_cmyk_idx = py * parent_stride + px * 4;
4673 parent_cmyk[p_cmyk_idx..p_cmyk_idx + 4]
4674 .copy_from_slice(&group_cmyk[g_cmyk_idx..g_cmyk_idx + 4]);
4675 }
4676 }
4677 }
4678}
4679
4680fn replace_changed_cmyk(
4683 target_cmyk: &mut [f32],
4684 source_cmyk: &[f32],
4685 source_pixels: &[u8],
4686 backdrop_pixels: &[u8],
4687) {
4688 let pixel_count = target_cmyk.len() / 4;
4689 for i in 0..pixel_count {
4690 let pi = i * 4;
4691 if source_pixels[pi] != backdrop_pixels[pi]
4692 || source_pixels[pi + 1] != backdrop_pixels[pi + 1]
4693 || source_pixels[pi + 2] != backdrop_pixels[pi + 2]
4694 || source_pixels[pi + 3] != backdrop_pixels[pi + 3]
4695 {
4696 target_cmyk[pi..pi + 4].copy_from_slice(&source_cmyk[pi..pi + 4]);
4697 }
4698 }
4699}
4700
4701#[allow(clippy::too_many_arguments)]
4709fn render_soft_masked(
4710 pixmap: &mut Pixmap,
4711 band_state: &mut BandState,
4712 mask_list: &DisplayList,
4713 content_list: &DisplayList,
4714 params: &stet_graphics::display_list::SoftMaskParams,
4715 mask_cache: &Arc<Mutex<Option<Option<stet_graphics::display_list::MaskRaster>>>>,
4716 ctx: &RenderContext<'_>,
4717) {
4718 let use_inline_mask = ctx.vp_x != 0.0;
4735 let bbox = ¶ms.bbox;
4736 let smask_px_x0 = ((bbox[0] as f32 - ctx.vp_x) * ctx.scale_x).floor() as i32;
4737 let smask_px_y0 = ((bbox[1] as f32 - ctx.vp_y) * ctx.scale_y).floor() as i32;
4738 let smask_px_x1 = ((bbox[2] as f32 - ctx.vp_x) * ctx.scale_x).ceil() as i32;
4739 let smask_px_y1 = ((bbox[3] as f32 - ctx.vp_y) * ctx.scale_y).ceil() as i32;
4740
4741 let crop_x = smask_px_x0.max(0);
4743 let crop_y = smask_px_y0.max(0);
4744 let crop_x1 = smask_px_x1.min(ctx.out_w as i32);
4745 let crop_y1 = smask_px_y1.min(ctx.out_h as i32);
4746 if crop_x >= crop_x1 || crop_y >= crop_y1 {
4747 return;
4748 }
4749 let eff_w = (crop_x1 - crop_x) as u32;
4750 let eff_h = (crop_y1 - crop_y) as u32;
4751
4752 let eff_vp_x = ctx.vp_x + crop_x as f32 / ctx.scale_x;
4757 let eff_vp_y = ctx.vp_y + crop_y as f32 / ctx.scale_y;
4758
4759 let sub_ctx = RenderContext {
4760 vp_x: eff_vp_x,
4761 vp_y: eff_vp_y,
4762 scale_x: ctx.scale_x,
4763 scale_y: ctx.scale_y,
4764 out_w: eff_w,
4765 out_h: eff_h,
4766 effective_dpi: ctx.effective_dpi,
4767 icc: ctx.icc,
4768 image_cache: None,
4769 preprocessed: None,
4770 elem_idx: 0,
4771 no_aa: ctx.no_aa,
4772 opm_zero_transparent: ctx.opm_zero_transparent,
4773 knockout_painter_pass: ctx.knockout_painter_pass,
4774 parent_group_isolated: ctx.parent_group_isolated,
4775 alpha_extraction_pass: false,
4779 layer_set: ctx.layer_set,
4780 };
4781
4782 let mut mask_values_inline: Vec<u8> = Vec::new();
4786 if use_inline_mask {
4787 let Some(mut mask_pixmap) = Pixmap::new(eff_w, eff_h) else {
4788 return;
4789 };
4790 let mut mask_band = BandState {
4791 clip_region: None,
4792 spare_mask: None,
4793 clip_mask_cache: HashMap::new(),
4794 clip_mask_seen: HashSet::new(),
4795 mask_pool: Vec::new(),
4796 cmyk_buffer: None,
4797 op_bg_snapshot: None,
4798 op_touched: None,
4799 spot_mask: None,
4800 };
4801 for (idx, elem) in mask_list.elements().iter().enumerate() {
4802 let elem_ctx = RenderContext {
4803 elem_idx: idx,
4804 ..sub_ctx
4805 };
4806 render_element(&mut mask_pixmap, &mut mask_band, elem, &elem_ctx);
4807 }
4808 if params.has_nested_mask_scope
4809 && params.subtype == stet_graphics::display_list::SoftMaskSubtype::Luminosity
4810 {
4811 let bc = params.backdrop_color.as_ref();
4812 let bd_r = bc.map_or(0u8, |c| (c[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
4813 let bd_g = bc.map_or(0u8, |c| (c[1].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
4814 let bd_b = bc.map_or(0u8, |c| (c[2].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
4815 for chunk in mask_pixmap.data_mut().chunks_exact_mut(4) {
4816 let a = chunk[3] as u16;
4817 if a == 255 {
4818 continue;
4819 }
4820 let inv_a = 255 - a;
4821 chunk[0] = ((chunk[0] as u16 * 255 + bd_r as u16 * inv_a + 127) / 255) as u8;
4822 chunk[1] = ((chunk[1] as u16 * 255 + bd_g as u16 * inv_a + 127) / 255) as u8;
4823 chunk[2] = ((chunk[2] as u16 * 255 + bd_b as u16 * inv_a + 127) / 255) as u8;
4824 chunk[3] = 255;
4825 }
4826 }
4827 mask_values_inline = vec![0u8; (eff_w * eff_h) as usize];
4828 extract_soft_mask_values(mask_pixmap.data(), &mut mask_values_inline, params);
4829 }
4830
4831 let raster_owned: Option<stet_graphics::display_list::MaskRaster> = if use_inline_mask {
4833 None
4834 } else {
4835 let mut guard = mask_cache.lock().unwrap();
4836 let needs_build = match guard.as_ref() {
4837 None => true,
4838 Some(None) => false, Some(Some(r)) => {
4840 (r.scale_x - ctx.scale_x).abs() > 1e-4 || (r.scale_y - ctx.scale_y).abs() > 1e-4
4841 }
4842 };
4843 if needs_build {
4844 let built = rasterize_mask(
4845 mask_list,
4846 params,
4847 ctx.icc,
4848 ctx.no_aa,
4849 ctx.effective_dpi,
4850 ctx.scale_x,
4851 ctx.scale_y,
4852 ctx.layer_set,
4853 );
4854 *guard = Some(built);
4855 }
4856 guard.as_ref().and_then(|inner| inner.clone())
4857 };
4858
4859 let fallback_mask = out_of_bounds_mask_value(params) as i32;
4863
4864 let Some(mut content_pixmap) = Pixmap::new(eff_w, eff_h) else {
4868 return;
4869 };
4870 let backdrop = copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h);
4871 content_pixmap.data_mut().copy_from_slice(&backdrop);
4872
4873 let content_cmyk = if has_overprint_elements(content_list) || band_state.cmyk_buffer.is_some() {
4874 let buf_size = eff_w as usize * eff_h as usize * 4;
4875 let mut buf = vec![0.0f32; buf_size];
4876 if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
4877 let parent_stride = ctx.out_w as usize * 4;
4878 let group_stride = eff_w as usize * 4;
4879 for gy in 0..eff_h as usize {
4880 let py = crop_y as usize + gy;
4881 if py < ctx.out_h as usize {
4882 let p_start = py * parent_stride + crop_x as usize * 4;
4883 let g_start = gy * group_stride;
4884 let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
4885 buf[g_start..g_start + copy_len]
4886 .copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
4887 }
4888 }
4889 }
4890 Some(buf)
4891 } else {
4892 None
4893 };
4894 let backdrop_cmyk: Option<Vec<f32>> = content_cmyk.clone();
4901 let mut content_band = BandState {
4902 clip_region: None,
4903 spare_mask: None,
4904 clip_mask_cache: HashMap::new(),
4905 clip_mask_seen: HashSet::new(),
4906 mask_pool: Vec::new(),
4907 cmyk_buffer: content_cmyk,
4908 op_bg_snapshot: None,
4909 op_touched: None,
4910 spot_mask: None,
4911 };
4912 for (idx, elem) in content_list.elements().iter().enumerate() {
4913 let elem_ctx = RenderContext {
4914 elem_idx: idx,
4915 ..sub_ctx
4916 };
4917 render_element(&mut content_pixmap, &mut content_band, elem, &elem_ctx);
4918 }
4919
4920 let vp_x_pixels = (ctx.vp_x * ctx.scale_x).round() as i32;
4940 let vp_y_pixels = (ctx.vp_y * ctx.scale_y).round() as i32;
4941
4942 let mut temp_mask = None;
4943 let clip_ref = resolve_clip_mask(
4944 &band_state.clip_region,
4945 &mut temp_mask,
4946 ctx.out_w,
4947 ctx.out_h,
4948 );
4949 let clip_ref = match clip_ref {
4950 None => return,
4951 Some(m) => m,
4952 };
4953
4954 let use_cmyk_blend = ctx.icc.is_some()
4970 && backdrop_cmyk.is_some()
4971 && content_band.cmyk_buffer.is_some()
4972 && content_list_is_simple_native_cmyk(content_list);
4973
4974 let content_data = content_pixmap.data();
4975 let parent_data = pixmap.data_mut();
4976 let parent_stride = ctx.out_w as usize * 4;
4977 let content_stride = eff_w as usize * 4;
4978
4979 for y in 0..eff_h as usize {
4980 let py = crop_y as usize + y;
4981 if py >= ctx.out_h as usize {
4982 break;
4983 }
4984 let ci_row = y * content_stride;
4985 let pi_row = py * parent_stride;
4986 let page_y = vp_y_pixels + crop_y + y as i32;
4987
4988 for x in 0..eff_w as usize {
4989 let px = crop_x as usize + x;
4990 if px >= ctx.out_w as usize {
4991 break;
4992 }
4993
4994 if let Some(clip) = clip_ref {
4996 if clip.data()[py * ctx.out_w as usize + px] == 0 {
4997 continue;
4998 }
4999 }
5000
5001 let m = if use_inline_mask {
5004 mask_values_inline[y * eff_w as usize + x] as i32
5005 } else if let Some(ref raster) = raster_owned {
5006 let page_x = vp_x_pixels + crop_x + x as i32;
5007 let mx = page_x - raster.origin_x;
5008 let my = page_y - raster.origin_y;
5009 if mx >= 0 && (mx as u32) < raster.width && my >= 0 && (my as u32) < raster.height {
5010 raster.data[my as usize * raster.width as usize + mx as usize] as i32
5011 } else {
5012 fallback_mask
5013 }
5014 } else {
5015 fallback_mask
5016 };
5017 if m == 0 {
5018 continue;
5019 }
5020
5021 let ci = ci_row + x * 4;
5022 let pi = pi_row + px * 4;
5023
5024 let ci_cmyk = (y * eff_w as usize + x) * 4;
5034 let cmyk_path_ok = use_cmyk_blend && {
5035 let bc_cmyk = &backdrop_cmyk.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5036 let cc_cmyk = &content_band.cmyk_buffer.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5037 let icc_match = |cmyk: &[f32], rgb: &[u8]| -> bool {
5038 let (r, g, b) = ctx
5039 .icc
5040 .and_then(|i| {
5041 i.convert_cmyk_readonly(
5042 cmyk[0] as f64,
5043 cmyk[1] as f64,
5044 cmyk[2] as f64,
5045 cmyk[3] as f64,
5046 )
5047 })
5048 .unwrap_or_else(|| {
5049 cmyk_to_rgb_plrm(
5050 cmyk[0] as f64,
5051 cmyk[1] as f64,
5052 cmyk[2] as f64,
5053 cmyk[3] as f64,
5054 )
5055 });
5056 let r = (r * 255.0).round() as i32;
5057 let g = (g * 255.0).round() as i32;
5058 let b = (b * 255.0).round() as i32;
5059 (r - rgb[0] as i32).abs() <= 3
5060 && (g - rgb[1] as i32).abs() <= 3
5061 && (b - rgb[2] as i32).abs() <= 3
5062 };
5063 icc_match(bc_cmyk, &backdrop[ci..ci + 3])
5064 && icc_match(cc_cmyk, &content_data[ci..ci + 3])
5065 };
5066
5067 if cmyk_path_ok {
5068 let bc_cmyk = &backdrop_cmyk.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5070 let cc_cmyk = &content_band.cmyk_buffer.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5071 let mf = m as f64 / 255.0;
5072 let rc = bc_cmyk[0] as f64 + mf * (cc_cmyk[0] as f64 - bc_cmyk[0] as f64);
5073 let rm = bc_cmyk[1] as f64 + mf * (cc_cmyk[1] as f64 - bc_cmyk[1] as f64);
5074 let ry = bc_cmyk[2] as f64 + mf * (cc_cmyk[2] as f64 - bc_cmyk[2] as f64);
5075 let rk = bc_cmyk[3] as f64 + mf * (cc_cmyk[3] as f64 - bc_cmyk[3] as f64);
5076 let (fr, fg, fb) = ctx
5077 .icc
5078 .and_then(|i| i.convert_cmyk_readonly(rc, rm, ry, rk))
5079 .unwrap_or_else(|| cmyk_to_rgb_plrm(rc, rm, ry, rk));
5080 parent_data[pi] = (fr * 255.0).round().clamp(0.0, 255.0) as u8;
5081 parent_data[pi + 1] = (fg * 255.0).round().clamp(0.0, 255.0) as u8;
5082 parent_data[pi + 2] = (fb * 255.0).round().clamp(0.0, 255.0) as u8;
5083 let content_a = content_data[ci + 3] as i32;
5085 let backdrop_a = backdrop[ci + 3] as i32;
5086 let delta = content_a - backdrop_a;
5087 if delta != 0 {
5088 let masked_delta = if delta > 0 {
5089 (delta * m + 128) / 255
5090 } else {
5091 (delta * m - 128) / 255
5092 };
5093 let result = (parent_data[pi + 3] as i32 + masked_delta).clamp(0, 255);
5094 parent_data[pi + 3] = result as u8;
5095 }
5096 } else {
5105 for c in 0..4 {
5106 let content_val = content_data[ci + c] as i32;
5107 let backdrop_val = backdrop[ci + c] as i32;
5108 let delta = content_val - backdrop_val;
5109 if delta != 0 {
5110 let masked_delta = if delta > 0 {
5111 (delta * m + 128) / 255
5112 } else {
5113 (delta * m - 128) / 255
5114 };
5115 let result = (parent_data[pi + c] as i32 + masked_delta).clamp(0, 255);
5116 parent_data[pi + c] = result as u8;
5117 }
5118 }
5119 }
5120 }
5121 }
5122
5123 if !use_cmyk_blend {
5127 if let (Some(content_cmyk), Some(parent_cmyk)) =
5128 (&content_band.cmyk_buffer, &mut band_state.cmyk_buffer)
5129 {
5130 copy_cmyk_buffer_to_parent(
5131 parent_cmyk,
5132 content_cmyk,
5133 content_pixmap.data(),
5134 crop_x as usize,
5135 crop_y as usize,
5136 eff_w as usize,
5137 eff_h as usize,
5138 ctx.out_w as usize,
5139 ctx.out_h as usize,
5140 );
5141 }
5142 }
5143}
5144fn extract_soft_mask_values(
5146 rgba: &[u8],
5147 out: &mut [u8],
5148 params: &stet_graphics::display_list::SoftMaskParams,
5149) {
5150 use stet_graphics::display_list::SoftMaskSubtype;
5151 let pixel_count = out.len();
5152
5153 match params.subtype {
5154 SoftMaskSubtype::Alpha => {
5155 for i in 0..pixel_count {
5156 let a = rgba[i * 4 + 3]; out[i] = if params.transfer_invert { 255 - a } else { a };
5158 }
5159 }
5160 SoftMaskSubtype::Luminosity => {
5161 let backdrop_lum = if let Some(bc) = ¶ms.backdrop_color {
5163 (0.2126 * bc[0] + 0.7152 * bc[1] + 0.0722 * bc[2]).clamp(0.0, 1.0)
5164 } else {
5165 0.0 };
5167 let backdrop_byte = (backdrop_lum * 255.0 + 0.5) as u8;
5168
5169 #[allow(clippy::needless_range_loop)]
5170 for i in 0..pixel_count {
5171 let off = i * 4;
5172 let a = rgba[off + 3];
5173 let lum_byte = if a == 0 {
5174 backdrop_byte
5175 } else if a < 255 {
5176 let af = a as f64;
5179 let bd = backdrop_lum * 255.0;
5180 let r = rgba[off] as f64 + bd * (255.0 - af) / 255.0;
5181 let g = rgba[off + 1] as f64 + bd * (255.0 - af) / 255.0;
5182 let b = rgba[off + 2] as f64 + bd * (255.0 - af) / 255.0;
5183 let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
5184 (lum + 0.5).clamp(0.0, 255.0) as u8
5185 } else {
5186 let lum = 0.2126 * rgba[off] as f64
5188 + 0.7152 * rgba[off + 1] as f64
5189 + 0.0722 * rgba[off + 2] as f64;
5190 (lum + 0.5).clamp(0.0, 255.0) as u8
5191 };
5192 out[i] = if params.transfer_invert {
5194 255 - lum_byte
5195 } else {
5196 lum_byte
5197 };
5198 }
5199 }
5200 }
5201}
5202
5203fn out_of_bounds_mask_value(params: &stet_graphics::display_list::SoftMaskParams) -> u8 {
5211 use stet_graphics::display_list::SoftMaskSubtype;
5212 let raw = match params.subtype {
5213 SoftMaskSubtype::Alpha => 0u8,
5214 SoftMaskSubtype::Luminosity => {
5215 let lum = if let Some(bc) = ¶ms.backdrop_color {
5216 (0.2126 * bc[0] + 0.7152 * bc[1] + 0.0722 * bc[2]).clamp(0.0, 1.0)
5217 } else {
5218 0.0
5219 };
5220 (lum * 255.0 + 0.5) as u8
5221 }
5222 };
5223 if params.transfer_invert {
5224 255 - raw
5225 } else {
5226 raw
5227 }
5228}
5229
5230const MAX_MASK_RASTER_PIXELS: u64 = 64 * 1024 * 1024;
5237
5238fn rasterize_mask(
5250 mask_list: &DisplayList,
5251 params: &stet_graphics::display_list::SoftMaskParams,
5252 icc: Option<&IccCache>,
5253 no_aa: bool,
5254 effective_dpi: f64,
5255 scale_x: f32,
5256 scale_y: f32,
5257 layer_set: &LayerSet,
5258) -> Option<stet_graphics::display_list::MaskRaster> {
5259 let mut bounds = compute_paint_bounds(mask_list, effective_dpi)?;
5266 if let Some(cap) = params.parent_clip_bbox {
5267 let cap_bbox = BBox2D {
5268 x_min: cap[0],
5269 y_min: cap[1],
5270 x_max: cap[2],
5271 y_max: cap[3],
5272 };
5273 bounds = intersect_bbox(&bounds, &cap_bbox)?;
5274 }
5275
5276 let px_x_min = (bounds.x_min as f32 * scale_x).floor() as i32 - 1;
5279 let px_y_min = (bounds.y_min as f32 * scale_y).floor() as i32 - 1;
5280 let px_x_max = (bounds.x_max as f32 * scale_x).ceil() as i32 + 1;
5281 let px_y_max = (bounds.y_max as f32 * scale_y).ceil() as i32 + 1;
5282 if px_x_min >= px_x_max || px_y_min >= px_y_max {
5283 return None;
5284 }
5285 let raster_w = (px_x_max - px_x_min) as u32;
5286 let raster_h = (px_y_max - px_y_min) as u32;
5287 if raster_w == 0 || raster_h == 0 {
5288 return None;
5289 }
5290 if (raster_w as u64) * (raster_h as u64) > MAX_MASK_RASTER_PIXELS {
5291 return None;
5292 }
5293
5294 let mut mask_pixmap = Pixmap::new(raster_w, raster_h)?;
5296
5297 let sub_ctx = RenderContext {
5301 vp_x: px_x_min as f32 / scale_x,
5302 vp_y: px_y_min as f32 / scale_y,
5303 scale_x,
5304 scale_y,
5305 out_w: raster_w,
5306 out_h: raster_h,
5307 effective_dpi,
5308 icc,
5309 image_cache: None,
5310 preprocessed: None,
5311 elem_idx: 0,
5312 no_aa,
5313 opm_zero_transparent: false,
5314 knockout_painter_pass: KnockoutPainterPass::None,
5315 parent_group_isolated: false,
5316 alpha_extraction_pass: false,
5317 layer_set,
5318 };
5319
5320 let mut mask_band = BandState {
5322 clip_region: None,
5323 spare_mask: None,
5324 clip_mask_cache: HashMap::new(),
5325 clip_mask_seen: HashSet::new(),
5326 mask_pool: Vec::new(),
5327 cmyk_buffer: None,
5328 op_bg_snapshot: None,
5329 op_touched: None,
5330 spot_mask: None,
5331 };
5332
5333 for (idx, elem) in mask_list.elements().iter().enumerate() {
5335 let elem_ctx = RenderContext {
5336 elem_idx: idx,
5337 ..sub_ctx
5338 };
5339 render_element(&mut mask_pixmap, &mut mask_band, elem, &elem_ctx);
5340 }
5341
5342 if params.has_nested_mask_scope
5350 && params.subtype == stet_graphics::display_list::SoftMaskSubtype::Luminosity
5351 {
5352 let bc = params.backdrop_color.as_ref();
5353 let bd_r = bc.map_or(0u8, |c| (c[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
5354 let bd_g = bc.map_or(0u8, |c| (c[1].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
5355 let bd_b = bc.map_or(0u8, |c| (c[2].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
5356 for chunk in mask_pixmap.data_mut().chunks_exact_mut(4) {
5357 let a = chunk[3] as u16;
5358 if a == 255 {
5359 continue;
5360 }
5361 let inv_a = 255 - a;
5362 chunk[0] = ((chunk[0] as u16 * 255 + bd_r as u16 * inv_a + 127) / 255) as u8;
5363 chunk[1] = ((chunk[1] as u16 * 255 + bd_g as u16 * inv_a + 127) / 255) as u8;
5364 chunk[2] = ((chunk[2] as u16 * 255 + bd_b as u16 * inv_a + 127) / 255) as u8;
5365 chunk[3] = 255;
5366 }
5367 }
5368
5369 let pixel_count = (raster_w * raster_h) as usize;
5371 let mut data = vec![0u8; pixel_count];
5372 extract_soft_mask_values(mask_pixmap.data(), &mut data, params);
5373
5374 Some(stet_graphics::display_list::MaskRaster {
5375 data,
5376 width: raster_w,
5377 height: raster_h,
5378 origin_x: px_x_min,
5379 origin_y: px_y_min,
5380 scale_x,
5381 scale_y,
5382 })
5383}
5384
5385fn transform_element_ctm(elem: &DisplayElement, pm: &Matrix) -> DisplayElement {
5389 match elem {
5390 DisplayElement::Fill { path, params } => {
5391 let mut p = params.clone();
5392 p.ctm = pm.concat(&p.ctm);
5393 DisplayElement::Fill {
5394 path: path.clone(),
5395 params: p,
5396 }
5397 }
5398 DisplayElement::Stroke { path, params } => {
5399 let mut p = params.clone();
5400 p.ctm = pm.concat(&p.ctm);
5401 DisplayElement::Stroke {
5402 path: path.clone(),
5403 params: p,
5404 }
5405 }
5406 DisplayElement::Clip { path, params } => {
5407 let mut p = params.clone();
5408 p.ctm = pm.concat(&p.ctm);
5409 if let Some(ref mut sp) = p.stroke_params {
5410 sp.ctm = pm.concat(&sp.ctm);
5411 }
5412 DisplayElement::Clip {
5413 path: path.clone(),
5414 params: p,
5415 }
5416 }
5417 DisplayElement::Image {
5418 sample_data,
5419 params,
5420 } => {
5421 let mut p = params.clone();
5422 p.ctm = pm.concat(&p.ctm);
5423 DisplayElement::Image {
5424 sample_data: sample_data.clone(),
5425 params: p,
5426 }
5427 }
5428 DisplayElement::MeshShading { params } => {
5429 let mut p = params.clone();
5430 p.ctm = pm.concat(&p.ctm);
5431 DisplayElement::MeshShading { params: p }
5432 }
5433 DisplayElement::PatchShading { params } => {
5434 let mut p = params.clone();
5435 p.ctm = pm.concat(&p.ctm);
5436 DisplayElement::PatchShading { params: p }
5437 }
5438 DisplayElement::AxialShading { params } => {
5439 let mut p = params.clone();
5440 p.ctm = pm.concat(&p.ctm);
5441 DisplayElement::AxialShading { params: p }
5442 }
5443 DisplayElement::RadialShading { params } => {
5444 let mut p = params.clone();
5445 p.ctm = pm.concat(&p.ctm);
5446 DisplayElement::RadialShading { params: p }
5447 }
5448 DisplayElement::Group { elements, params } => {
5449 let mut t = DisplayList::new();
5450 for child in elements.elements() {
5451 t.push(transform_element_ctm(child, pm));
5452 }
5453 let mut p = params.clone();
5454 let corners = [
5455 pm.transform_point(p.bbox[0], p.bbox[1]),
5456 pm.transform_point(p.bbox[2], p.bbox[1]),
5457 pm.transform_point(p.bbox[0], p.bbox[3]),
5458 pm.transform_point(p.bbox[2], p.bbox[3]),
5459 ];
5460 p.bbox = [
5461 corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min),
5462 corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min),
5463 corners
5464 .iter()
5465 .map(|c| c.0)
5466 .fold(f64::NEG_INFINITY, f64::max),
5467 corners
5468 .iter()
5469 .map(|c| c.1)
5470 .fold(f64::NEG_INFINITY, f64::max),
5471 ];
5472 DisplayElement::Group {
5473 elements: t,
5474 params: p,
5475 }
5476 }
5477 DisplayElement::SoftMasked {
5478 mask,
5479 content,
5480 params,
5481 ..
5482 } => {
5483 let mut t_mask = DisplayList::new();
5484 for child in mask.elements() {
5485 t_mask.push(transform_element_ctm(child, pm));
5486 }
5487 let mut t_content = DisplayList::new();
5488 for child in content.elements() {
5489 t_content.push(transform_element_ctm(child, pm));
5490 }
5491 let mut p = params.clone();
5492 let corners = [
5493 pm.transform_point(p.bbox[0], p.bbox[1]),
5494 pm.transform_point(p.bbox[2], p.bbox[1]),
5495 pm.transform_point(p.bbox[0], p.bbox[3]),
5496 pm.transform_point(p.bbox[2], p.bbox[3]),
5497 ];
5498 p.bbox = [
5499 corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min),
5500 corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min),
5501 corners
5502 .iter()
5503 .map(|c| c.0)
5504 .fold(f64::NEG_INFINITY, f64::max),
5505 corners
5506 .iter()
5507 .map(|c| c.1)
5508 .fold(f64::NEG_INFINITY, f64::max),
5509 ];
5510 if let Some(pcb) = p.parent_clip_bbox {
5517 let pcb_corners = [
5518 pm.transform_point(pcb[0], pcb[1]),
5519 pm.transform_point(pcb[2], pcb[1]),
5520 pm.transform_point(pcb[0], pcb[3]),
5521 pm.transform_point(pcb[2], pcb[3]),
5522 ];
5523 p.parent_clip_bbox = Some([
5524 pcb_corners
5525 .iter()
5526 .map(|c| c.0)
5527 .fold(f64::INFINITY, f64::min),
5528 pcb_corners
5529 .iter()
5530 .map(|c| c.1)
5531 .fold(f64::INFINITY, f64::min),
5532 pcb_corners
5533 .iter()
5534 .map(|c| c.0)
5535 .fold(f64::NEG_INFINITY, f64::max),
5536 pcb_corners
5537 .iter()
5538 .map(|c| c.1)
5539 .fold(f64::NEG_INFINITY, f64::max),
5540 ]);
5541 }
5542 DisplayElement::SoftMasked {
5546 mask: t_mask,
5547 content: t_content,
5548 params: p,
5549 mask_cache: Arc::new(Mutex::new(None)),
5550 }
5551 }
5552 DisplayElement::PatternFill { params } => {
5553 let mut p = params.clone();
5554 p.pattern_matrix = pm.concat(&p.pattern_matrix);
5555 p.path = transform_path_by_matrix(&p.path, pm);
5557 if let Some(ref mut sp) = p.stroke_params {
5558 sp.ctm = pm.concat(&sp.ctm);
5559 }
5560 DisplayElement::PatternFill { params: p }
5561 }
5562 DisplayElement::OcgGroup {
5563 elements,
5564 visibility,
5565 } => {
5566 let mut t = DisplayList::new();
5567 for child in elements.elements() {
5568 t.push(transform_element_ctm(child, pm));
5569 }
5570 DisplayElement::OcgGroup {
5571 elements: t,
5572 visibility: visibility.clone(),
5573 }
5574 }
5575 other => other.clone(),
5576 }
5577}
5578
5579fn transform_path_by_matrix(path: &PsPath, m: &Matrix) -> PsPath {
5581 use stet_fonts::geometry::PathSegment;
5582 let mut out = PsPath::new();
5583 for seg in &path.segments {
5584 out.segments.push(match *seg {
5585 PathSegment::MoveTo(x, y) => {
5586 let (nx, ny) = m.transform_point(x, y);
5587 PathSegment::MoveTo(nx, ny)
5588 }
5589 PathSegment::LineTo(x, y) => {
5590 let (nx, ny) = m.transform_point(x, y);
5591 PathSegment::LineTo(nx, ny)
5592 }
5593 PathSegment::CurveTo {
5594 x1,
5595 y1,
5596 x2,
5597 y2,
5598 x3,
5599 y3,
5600 } => {
5601 let (nx1, ny1) = m.transform_point(x1, y1);
5602 let (nx2, ny2) = m.transform_point(x2, y2);
5603 let (nx3, ny3) = m.transform_point(x3, y3);
5604 PathSegment::CurveTo {
5605 x1: nx1,
5606 y1: ny1,
5607 x2: nx2,
5608 y2: ny2,
5609 x3: nx3,
5610 y3: ny3,
5611 }
5612 }
5613 PathSegment::ClosePath => PathSegment::ClosePath,
5614 });
5615 }
5616 out
5617}
5618
5619fn bilinear_prescale(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
5626 let mut dst = vec![0u8; (dw * dh * 4) as usize];
5627 for dy in 0..dh {
5628 let sy_f = (dy as f64 + 0.5) * sh as f64 / dh as f64 - 0.5;
5629 let sy0 = sy_f.floor().max(0.0) as u32;
5630 let sy1 = (sy0 + 1).min(sh - 1);
5631 let fy = (sy_f - sy0 as f64) as f32;
5632 let ify = 1.0 - fy;
5633 for dx in 0..dw {
5634 let sx_f = (dx as f64 + 0.5) * sw as f64 / dw as f64 - 0.5;
5635 let sx0 = sx_f.floor().max(0.0) as u32;
5636 let sx1 = (sx0 + 1).min(sw - 1);
5637 let fx = (sx_f - sx0 as f64) as f32;
5638 let ifx = 1.0 - fx;
5639
5640 let i00 = (sy0 * sw + sx0) as usize * 4;
5641 let i10 = (sy0 * sw + sx1) as usize * 4;
5642 let i01 = (sy1 * sw + sx0) as usize * 4;
5643 let i11 = (sy1 * sw + sx1) as usize * 4;
5644 let di = (dy * dw + dx) as usize * 4;
5645 for c in 0..4 {
5646 dst[di + c] = (src[i00 + c] as f32 * ifx * ify
5647 + src[i10 + c] as f32 * fx * ify
5648 + src[i01 + c] as f32 * ifx * fy
5649 + src[i11 + c] as f32 * fx * fy)
5650 .round() as u8;
5651 }
5652 }
5653 }
5654 dst
5655}
5656
5657fn render_pattern_fill(
5658 pixmap: &mut Pixmap,
5659 band_state: &mut BandState,
5660 params: &stet_graphics::device::PatternFillParams,
5661 ctx: &RenderContext<'_>,
5662) {
5663 let mut temp_mask = None;
5664 let Some(mask_ref) = resolve_clip_mask(
5665 &band_state.clip_region,
5666 &mut temp_mask,
5667 ctx.out_w,
5668 ctx.out_h,
5669 ) else {
5670 return;
5671 };
5672
5673 let pm = ¶ms.pattern_matrix;
5674
5675 let (step_ux, step_uy) = pm.transform_delta(params.xstep, 0.0);
5677 let (step_vx, step_vy) = pm.transform_delta(0.0, params.ystep);
5678
5679 let step_u_len = (step_ux * step_ux + step_uy * step_uy).sqrt();
5680 let step_v_len = (step_vx * step_vx + step_vy * step_vy).sqrt();
5681 if step_u_len < 0.01 || step_v_len < 0.01 {
5682 return;
5683 }
5684
5685 let origin_x = pm.tx;
5686 let origin_y = pm.ty;
5687
5688 let dev_vp_x = ctx.vp_x as f64;
5690 let dev_vp_y = ctx.vp_y as f64;
5691 let dev_vp_w = ctx.out_w as f64 / ctx.scale_x as f64;
5692 let dev_vp_h = ctx.out_h as f64 / ctx.scale_y as f64;
5693
5694 let (mut min_x, mut min_y, mut max_x, mut max_y) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
5695 for seg in ¶ms.path.segments {
5696 let (x, y) = match seg {
5697 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => (*x, *y),
5698 PathSegment::CurveTo { x3, y3, .. } => (*x3, *y3),
5699 PathSegment::ClosePath => continue,
5700 };
5701 min_x = min_x.min(x);
5702 min_y = min_y.min(y);
5703 max_x = max_x.max(x);
5704 max_y = max_y.max(y);
5705 }
5706
5707 if let Some(ref sp) = params.stroke_params {
5711 let ctm = &sp.ctm;
5713 let corners = [
5714 ctm.transform_point(min_x, min_y),
5715 ctm.transform_point(max_x, min_y),
5716 ctm.transform_point(min_x, max_y),
5717 ctm.transform_point(max_x, max_y),
5718 ];
5719 min_x = f64::MAX;
5720 min_y = f64::MAX;
5721 max_x = f64::MIN;
5722 max_y = f64::MIN;
5723 for (cx, cy) in &corners {
5724 min_x = min_x.min(*cx);
5725 min_y = min_y.min(*cy);
5726 max_x = max_x.max(*cx);
5727 max_y = max_y.max(*cy);
5728 }
5729 let half_w = sp.line_width
5731 * 0.5
5732 * (ctm.a * ctm.a + ctm.b * ctm.b)
5733 .sqrt()
5734 .max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt());
5735 min_x -= half_w;
5736 min_y -= half_w;
5737 max_x += half_w;
5738 max_y += half_w;
5739 }
5740
5741 min_x = min_x.max(dev_vp_x);
5743 min_y = min_y.max(dev_vp_y);
5744 max_x = max_x.min(dev_vp_x + dev_vp_w);
5745 max_y = max_y.min(dev_vp_y + dev_vp_h);
5746 if min_x >= max_x || min_y >= max_y {
5747 return;
5748 }
5749
5750 let det = step_ux * step_vy - step_uy * step_vx;
5751 if det.abs() < 1e-10 {
5752 return;
5753 }
5754 let inv_det = 1.0 / det;
5755
5756 let mut tu_min = f64::MAX;
5757 let mut tu_max = f64::MIN;
5758 let mut tv_min = f64::MAX;
5759 let mut tv_max = f64::MIN;
5760 for &(cx, cy) in &[
5761 (min_x, min_y),
5762 (max_x, min_y),
5763 (min_x, max_y),
5764 (max_x, max_y),
5765 ] {
5766 let dx = cx - origin_x;
5767 let dy = cy - origin_y;
5768 let tu = (dx * step_vy - dy * step_vx) * inv_det;
5769 let tv = (-dx * step_uy + dy * step_ux) * inv_det;
5770 tu_min = tu_min.min(tu);
5771 tu_max = tu_max.max(tu);
5772 tv_min = tv_min.min(tv);
5773 tv_max = tv_max.max(tv);
5774 }
5775
5776 let tile_x_start = tu_min.floor() as i32 - 1;
5777 let tile_x_end = tu_max.ceil() as i32 + 1;
5778 let tile_y_start = tv_min.floor() as i32 - 1;
5779 let tile_y_end = tv_max.ceil() as i32 + 1;
5780
5781 let tile_count = (tile_x_end - tile_x_start) as i64 * (tile_y_end - tile_y_start) as i64;
5782 if tile_count > 10000 {
5783 return;
5784 }
5785
5786 let Some(mut tile_buf) = Pixmap::new(ctx.out_w, ctx.out_h) else {
5787 return;
5788 };
5789
5790 let sx_f = ctx.scale_x as f64;
5791 let sy_f = ctx.scale_y as f64;
5792
5793 if params.device_space_tile {
5794 for tv in tile_y_start..tile_y_end {
5800 for tu in tile_x_start..tile_x_end {
5801 let offset_x = tu as f64 * step_ux + tv as f64 * step_vx;
5802 let offset_y = tu as f64 * step_uy + tv as f64 * step_vy;
5803
5804 let tile_ctx = RenderContext {
5805 vp_x: ctx.vp_x - offset_x as f32,
5806 vp_y: ctx.vp_y - offset_y as f32,
5807 scale_x: ctx.scale_x,
5808 scale_y: ctx.scale_y,
5809 out_w: ctx.out_w,
5810 out_h: ctx.out_h,
5811 effective_dpi: ctx.effective_dpi,
5812 icc: ctx.icc,
5813 image_cache: None,
5814 preprocessed: None,
5815 elem_idx: 0,
5816 no_aa: ctx.no_aa,
5817 opm_zero_transparent: params.overprint_mode == 1,
5818 knockout_painter_pass: ctx.knockout_painter_pass,
5819 parent_group_isolated: ctx.parent_group_isolated,
5820 alpha_extraction_pass: ctx.alpha_extraction_pass,
5821 layer_set: ctx.layer_set,
5822 };
5823
5824 let mut tile_band = BandState {
5825 clip_region: None,
5826 spare_mask: None,
5827 clip_mask_cache: HashMap::new(),
5828 clip_mask_seen: HashSet::new(),
5829 mask_pool: Vec::new(),
5830 cmyk_buffer: None,
5831 op_bg_snapshot: None,
5832 op_touched: None,
5833 spot_mask: None,
5834 };
5835
5836 for (idx, elem) in params.tile.elements().iter().enumerate() {
5837 let elem_ctx = RenderContext {
5838 elem_idx: idx,
5839 ..tile_ctx
5840 };
5841 render_element(&mut tile_buf, &mut tile_band, elem, &elem_ctx);
5842 }
5843 }
5844 }
5845 } else if params.tile.elements().iter().any(|e| {
5846 !matches!(
5847 e,
5848 DisplayElement::Fill { .. }
5849 | DisplayElement::Stroke { .. }
5850 | DisplayElement::Image { .. }
5851 | DisplayElement::Clip { .. }
5852 | DisplayElement::InitClip
5853 )
5854 }) {
5855 let bbox = ¶ms.bbox;
5859 let corners_dev = [
5860 pm.transform_point(bbox[0], bbox[1]),
5861 pm.transform_point(bbox[2], bbox[1]),
5862 pm.transform_point(bbox[0], bbox[3]),
5863 pm.transform_point(bbox[2], bbox[3]),
5864 ];
5865 let (mut td_x0, mut td_y0) = (f64::MAX, f64::MAX);
5866 let (mut td_x1, mut td_y1) = (f64::MIN, f64::MIN);
5867 for (x, y) in &corners_dev {
5868 td_x0 = td_x0.min(*x);
5869 td_y0 = td_y0.min(*y);
5870 td_x1 = td_x1.max(*x);
5871 td_y1 = td_y1.max(*y);
5872 }
5873 let tile_pw = ((td_x1 - td_x0) * sx_f).ceil().max(1.0) as u32;
5874 let tile_ph = ((td_y1 - td_y0) * sy_f).ceil().max(1.0) as u32;
5875 let tile_pw = tile_pw.min(8192);
5876 let tile_ph = tile_ph.min(8192);
5877
5878 if let Some(mut one_tile) = Pixmap::new(tile_pw, tile_ph) {
5879 let tile_render_ctx = RenderContext {
5880 vp_x: td_x0 as f32,
5881 vp_y: td_y0 as f32,
5882 scale_x: ctx.scale_x,
5883 scale_y: ctx.scale_y,
5884 out_w: tile_pw,
5885 out_h: tile_ph,
5886 effective_dpi: ctx.effective_dpi,
5887 icc: ctx.icc,
5888 image_cache: None,
5889 preprocessed: None,
5890 elem_idx: 0,
5891 no_aa: ctx.no_aa,
5892 opm_zero_transparent: params.overprint_mode == 1,
5893 knockout_painter_pass: ctx.knockout_painter_pass,
5894 parent_group_isolated: ctx.parent_group_isolated,
5895 alpha_extraction_pass: ctx.alpha_extraction_pass,
5896 layer_set: ctx.layer_set,
5897 };
5898 let mut tile_bs = BandState {
5899 clip_region: None,
5900 spare_mask: None,
5901 clip_mask_cache: HashMap::new(),
5902 clip_mask_seen: HashSet::new(),
5903 mask_pool: Vec::new(),
5904 cmyk_buffer: None,
5905 op_bg_snapshot: None,
5906 op_touched: None,
5907 spot_mask: None,
5908 };
5909 for (idx, elem) in params.tile.elements().iter().enumerate() {
5910 let transformed = transform_element_ctm(elem, pm);
5911 let elem_ctx = RenderContext {
5912 elem_idx: idx,
5913 ..tile_render_ctx
5914 };
5915 render_element(&mut one_tile, &mut tile_bs, &transformed, &elem_ctx);
5916 }
5917 for tv in tile_y_start..tile_y_end {
5919 for tu in tile_x_start..tile_x_end {
5920 let offset_x = tu as f64 * step_ux + tv as f64 * step_vx;
5921 let offset_y = tu as f64 * step_uy + tv as f64 * step_vy;
5922 let px = ((td_x0 + offset_x - dev_vp_x) * sx_f) as i32;
5923 let py = ((td_y0 + offset_y - dev_vp_y) * sy_f) as i32;
5924 let paint = stet_tiny_skia::PixmapPaint {
5925 opacity: 1.0,
5926 blend_mode: BlendMode::SourceOver,
5927 quality: stet_tiny_skia::FilterQuality::Nearest,
5928 };
5929 tile_buf.draw_pixmap(
5930 px,
5931 py,
5932 one_tile.as_ref(),
5933 &paint,
5934 Transform::identity(),
5935 None,
5936 );
5937 }
5938 }
5939 }
5940 } else {
5941 struct PreprocessedImage {
5949 rgba: Vec<u8>,
5950 width: u32,
5951 height: u32,
5952 img_transform: Transform,
5955 }
5956 let tile_elements = params.tile.elements();
5957 let mut preprocessed: Vec<Option<PreprocessedImage>> =
5958 Vec::with_capacity(tile_elements.len());
5959 let tt_sx = (pm.a * sx_f) as f32;
5961 let tt_sy = (pm.d * sy_f) as f32;
5962 let tt_kx = (pm.c * sx_f) as f32;
5963 let tt_ky = (pm.b * sy_f) as f32;
5964 for elem in tile_elements {
5965 if let DisplayElement::Image {
5966 sample_data,
5967 params: ip,
5968 } = elem
5969 {
5970 let iw = ip.width;
5971 let ih = ip.height;
5972 if iw > 0 && ih > 0 {
5973 let mut rgba =
5974 samples_to_rgba(sample_data, ip, ctx.icc, ctx.opm_zero_transparent);
5975 if ip.mask_color.is_some() {
5976 apply_mask_color_rgba(&mut rgba, sample_data, ip);
5977 }
5978 let expected = (iw * ih * 4) as usize;
5979 if rgba.len() >= expected {
5980 if let Some(inv) = ip.image_matrix.invert() {
5981 let combined_mat = ip.ctm.concat(&inv);
5982 let t = to_transform(&combined_mat);
5983 let test = t.post_concat(Transform::from_row(
5986 tt_sx, tt_ky, tt_kx, tt_sy, 0.0, 0.0,
5987 ));
5988 let eff_sx = (test.sx * test.sx + test.ky * test.ky).sqrt();
5989 let eff_sy = (test.kx * test.kx + test.sy * test.sy).sqrt();
5990 if eff_sx < 0.99 || eff_sy < 0.99 {
5991 let tw = (iw as f32 * eff_sx).floor().max(1.0) as u32;
5995 let th = (ih as f32 * eff_sy).floor().max(1.0) as u32;
5996 let scaled = bilinear_prescale(&rgba, iw, ih, tw, th);
5997 let adj = Transform::from_scale(
6000 iw as f32 / tw as f32,
6001 ih as f32 / th as f32,
6002 );
6003 preprocessed.push(Some(PreprocessedImage {
6004 rgba: scaled,
6005 width: tw,
6006 height: th,
6007 img_transform: t.pre_concat(adj),
6008 }));
6009 } else {
6010 preprocessed.push(Some(PreprocessedImage {
6011 rgba,
6012 width: iw,
6013 height: ih,
6014 img_transform: t,
6015 }));
6016 }
6017 } else {
6018 preprocessed.push(None);
6019 }
6020 } else {
6021 preprocessed.push(None);
6022 }
6023 } else {
6024 preprocessed.push(None);
6025 }
6026 }
6029 }
6030
6031 for tv in tile_y_start..tile_y_end {
6032 for tu in tile_x_start..tile_x_end {
6033 let pat_offset_x = tu as f64 * params.xstep;
6034 let pat_offset_y = tv as f64 * params.ystep;
6035
6036 let tile_transform = Transform::from_row(
6037 tt_sx,
6038 tt_ky,
6039 tt_kx,
6040 tt_sy,
6041 ((pm.a * pat_offset_x + pm.c * pat_offset_y + pm.tx - dev_vp_x) * sx_f) as f32,
6042 ((pm.b * pat_offset_x + pm.d * pat_offset_y + pm.ty - dev_vp_y) * sy_f) as f32,
6043 );
6044
6045 let bbox_clip = {
6047 let bb = ¶ms.bbox;
6048 let mut bp = stet_tiny_skia::PathBuilder::new();
6049 bp.move_to(bb[0] as f32, bb[1] as f32);
6050 bp.line_to(bb[2] as f32, bb[1] as f32);
6051 bp.line_to(bb[2] as f32, bb[3] as f32);
6052 bp.line_to(bb[0] as f32, bb[3] as f32);
6053 bp.close();
6054 bp.finish().and_then(|sp| {
6055 let mut m = Mask::new(ctx.out_w, ctx.out_h)?;
6056 m.fill_path(
6057 &sp,
6058 stet_tiny_skia::FillRule::Winding,
6059 false,
6060 tile_transform,
6061 );
6062 Some(m)
6063 })
6064 };
6065 let mut tile_clip: Option<Mask> = bbox_clip;
6066 let mut img_idx = 0usize;
6067 for elem in tile_elements {
6068 let clip_ref = tile_clip.as_ref();
6069 match elem {
6070 DisplayElement::Clip { path, params: cp } => {
6071 if let Some(sp) = build_skia_path(path) {
6072 let t = to_transform(&cp.ctm);
6073 let combined = t.post_concat(tile_transform);
6074 let mut mask = Mask::new(ctx.out_w, ctx.out_h).expect("mask");
6075 mask.fill_path(&sp, to_fill_rule(&cp.fill_rule), false, combined);
6076 if let Some(prev) = tile_clip.take() {
6077 intersect_masks(&mut mask, &prev);
6078 }
6079 tile_clip = Some(mask);
6080 }
6081 }
6082 DisplayElement::InitClip => {
6083 tile_clip = None;
6084 }
6085 DisplayElement::Fill { path, params: fp } => {
6086 if let Some(sp) = build_skia_path(path) {
6087 let mut paint = if params.paint_type == 1 {
6088 to_paint(&fp.color)
6089 } else {
6090 to_paint(
6091 params
6092 .underlying_color
6093 .as_ref()
6094 .unwrap_or(&DeviceColor::black()),
6095 )
6096 };
6097 paint.anti_alias = false;
6098 let t = to_transform(&fp.ctm);
6099 let combined = t.post_concat(tile_transform);
6100 let fr = to_fill_rule(&fp.fill_rule);
6101 tile_buf.fill_path(&sp, &paint, fr, combined, clip_ref);
6102 }
6103 }
6104 DisplayElement::Stroke { path, params: sp } => {
6105 if let Some(skp) = build_skia_path(path) {
6106 let effective_ctm = pm.concat(&sp.ctm);
6110 let mut sp_adj = sp.clone();
6111 sp_adj.ctm = effective_ctm;
6112 let stroke = build_stroke(&sp_adj, ctx.effective_dpi);
6113 let paint = if params.paint_type == 1 {
6114 to_paint(&sp.color)
6115 } else {
6116 to_paint(
6117 params
6118 .underlying_color
6119 .as_ref()
6120 .unwrap_or(&DeviceColor::black()),
6121 )
6122 };
6123 let t = to_transform(&sp.ctm);
6124 let combined = t.post_concat(tile_transform);
6125 tile_buf.stroke_path(&skp, &paint, &stroke, combined, clip_ref);
6126 }
6127 }
6128 DisplayElement::Image { .. } => {
6129 if let Some(ref pi) = preprocessed[img_idx] {
6130 let combined = pi.img_transform.post_concat(tile_transform);
6131 if let Some(img_ref) = stet_tiny_skia::PixmapRef::from_bytes(
6132 &pi.rgba, pi.width, pi.height,
6133 ) {
6134 let paint = stet_tiny_skia::PixmapPaint {
6135 opacity: 1.0,
6136 blend_mode: BlendMode::SourceOver,
6137 quality: stet_tiny_skia::FilterQuality::Nearest,
6138 };
6139 tile_buf.draw_pixmap(0, 0, img_ref, &paint, combined, clip_ref);
6140 }
6141 }
6142 img_idx += 1;
6143 }
6144 _ => {}
6145 }
6146 }
6147 }
6148 }
6149 }
6150
6151 let Some(fill_skia_path) = build_skia_path(¶ms.path) else {
6153 return;
6154 };
6155 let fill_rule = to_fill_rule(¶ms.fill_rule);
6156 let mut fill_mask = Mask::new(ctx.out_w, ctx.out_h).expect("mask");
6157 let path_transform = viewport_transform(
6158 Transform::identity(),
6159 ctx.vp_x,
6160 ctx.vp_y,
6161 ctx.scale_x,
6162 ctx.scale_y,
6163 );
6164 if let Some(ref sp) = params.stroke_params {
6165 let stroke = build_stroke(sp, ctx.effective_dpi);
6169 let ctm_transform = to_transform(&sp.ctm);
6170 let combined = ctm_transform.post_concat(path_transform);
6171 let res_scale = stet_tiny_skia::PathStroker::compute_resolution_scale(&combined);
6172 let dashed;
6173 let stroke_path = if let Some(ref dash) = stroke.dash {
6174 dashed = fill_skia_path.dash(dash, res_scale);
6175 match dashed.as_ref() {
6176 Some(p) => p,
6177 None => &fill_skia_path,
6178 }
6179 } else {
6180 &fill_skia_path
6181 };
6182 if let Some(outline) = stroke_path.stroke(&stroke, res_scale) {
6183 fill_mask.fill_path(
6184 &outline,
6185 stet_tiny_skia::FillRule::Winding,
6186 !ctx.no_aa,
6187 combined,
6188 );
6189 }
6190 } else {
6191 fill_mask.fill_path(&fill_skia_path, fill_rule, !ctx.no_aa, path_transform);
6192 }
6193
6194 if let Some(clip_mask) = mask_ref {
6195 intersect_masks(&mut fill_mask, clip_mask);
6196 }
6197
6198 let img_paint = stet_tiny_skia::PixmapPaint::default();
6199 pixmap.draw_pixmap(
6200 0,
6201 0,
6202 tile_buf.as_ref(),
6203 &img_paint,
6204 Transform::identity(),
6205 Some(&fill_mask),
6206 );
6207}
6208
6209fn clip_path_unified(
6214 band_state: &mut BandState,
6215 path: &PsPath,
6216 params: &ClipParams,
6217 ctx: &RenderContext<'_>,
6218) {
6219 let is_unit_scale = ctx.scale_x == 1.0 && ctx.scale_y == 1.0;
6220
6221 if is_unit_scale {
6223 let y_start = ctx.vp_y as u32;
6224 let x_start = ctx.vp_x as u32;
6225
6226 if x_start == 0
6231 && params.stroke_params.is_none()
6232 && params.ctm.a == 1.0
6233 && params.ctm.d == 1.0
6234 && params.ctm.tx == 0.0
6235 && params.ctm.ty == 0.0
6236 && let Some(bbox) = path_y_bbox(path)
6237 && (bbox.y_max <= y_start as f64 || bbox.y_min >= (y_start + ctx.out_h) as f64)
6238 {
6239 if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
6240 band_state.recycle_mask(mask);
6241 }
6242 band_state.clip_region = Some(ClipRegion::Rect(ClipRect {
6243 x0: 0,
6244 y0: 0,
6245 x1: 0,
6246 y1: 0,
6247 }));
6248 return;
6249 }
6250
6251 let ctm_is_identity = params.ctm.a == 1.0
6255 && params.ctm.b == 0.0
6256 && params.ctm.c == 0.0
6257 && params.ctm.d == 1.0
6258 && params.ctm.tx == 0.0
6259 && params.ctm.ty == 0.0;
6260 if x_start == 0
6261 && ctm_is_identity
6262 && params.stroke_params.is_none()
6263 && let Some(dev_rect) = detect_rect(path, ctx.out_w, u32::MAX)
6264 {
6265 let new_rect = translate_clip_rect(&dev_rect, y_start, ctx.out_h);
6266 match band_state.clip_region.take() {
6267 None => {
6268 band_state.clip_region = Some(ClipRegion::Rect(new_rect));
6269 }
6270 Some(ClipRegion::Rect(existing)) => {
6271 band_state.clip_region = Some(ClipRegion::Rect(existing.intersect(&new_rect)));
6272 }
6273 Some(ClipRegion::Mask(mut mask)) => {
6274 intersect_mask_with_rect(&mut mask, &new_rect, ctx.out_w, ctx.out_h);
6275 band_state.clip_region = Some(ClipRegion::Mask(mask));
6276 }
6277 }
6278 return;
6279 }
6280 }
6281
6282 let fill_rule = to_fill_rule(¶ms.fill_rule);
6284 let path_hash = hash_clip_path(path, ¶ms.fill_rule);
6285 let prev_region = band_state.clip_region.take();
6286
6287 let mut mask = band_state.take_mask(ctx.out_w, ctx.out_h);
6288
6289 let path_mask = if let Some(cached) = band_state.clip_mask_cache.get(&path_hash) {
6290 mask.data_mut().copy_from_slice(cached.data());
6291 mask
6292 } else {
6293 let Some(skia_path) = build_skia_path(path) else {
6294 band_state.recycle_mask(mask);
6295 band_state.clip_region = prev_region;
6296 return;
6297 };
6298 mask.data_mut().fill(0);
6299 if let Some(ref sp) = params.stroke_params {
6300 let stroke = build_stroke(sp, ctx.effective_dpi);
6303 let transform = ctx.transform(&sp.ctm);
6304 let res_scale = stet_tiny_skia::PathStroker::compute_resolution_scale(&transform);
6305 let dashed;
6306 let stroke_path = if let Some(ref dash) = stroke.dash {
6307 dashed = skia_path.dash(dash, res_scale);
6308 match dashed.as_ref() {
6309 Some(p) => p,
6310 None => &skia_path,
6311 }
6312 } else {
6313 &skia_path
6314 };
6315 if let Some(outline) = stroke_path.stroke(&stroke, res_scale) {
6316 mask.fill_path(
6317 &outline,
6318 stet_tiny_skia::FillRule::Winding,
6319 false,
6320 transform,
6321 );
6322 }
6323 } else {
6324 let transform = ctx.transform(¶ms.ctm);
6325 mask.fill_path(&skia_path, fill_rule, false, transform);
6326 }
6327 if !band_state.clip_mask_seen.insert(path_hash) {
6328 band_state.clip_mask_cache.insert(path_hash, mask.clone());
6329 }
6330 mask
6331 };
6332
6333 match prev_region {
6334 None => {
6335 band_state.clip_region = Some(ClipRegion::Mask(path_mask));
6336 }
6337 Some(ClipRegion::Rect(rect)) => {
6338 if rect.is_empty() {
6339 band_state.recycle_mask(path_mask);
6340 band_state.clip_region = Some(ClipRegion::Rect(rect));
6343 } else {
6344 let mut mask = path_mask;
6345 intersect_mask_with_rect(&mut mask, &rect, ctx.out_w, ctx.out_h);
6346 band_state.clip_region = Some(ClipRegion::Mask(mask));
6347 }
6348 }
6349 Some(ClipRegion::Mask(mut existing)) => {
6350 intersect_masks(&mut existing, &path_mask);
6351 band_state.recycle_mask(path_mask);
6352 band_state.clip_region = Some(ClipRegion::Mask(existing));
6353 }
6354 }
6355}
6356impl OutputDevice for SkiaDevice {
6357 fn fill_path(&mut self, path: &PsPath, params: &FillParams) {
6358 self.ensure_full_pixmap();
6359 let Some(skia_path) = build_skia_path(path) else {
6360 return;
6361 };
6362 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6363 let mut temp_mask = None;
6364 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6365 return; };
6367
6368 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, self.no_aa);
6369 let transform = to_transform(¶ms.ctm);
6370 let fill_rule = to_fill_rule(¶ms.fill_rule);
6371
6372 self.pixmap
6373 .fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
6374 }
6375
6376 fn stroke_path(&mut self, path: &PsPath, params: &StrokeParams) {
6377 self.ensure_full_pixmap();
6378 let stroke = build_stroke(params, self.dpi);
6379 let adjusted;
6380 let draw_path =
6381 if params.stroke_adjust && stroke.width <= 2.0 && ctm_is_device_space(¶ms.ctm) {
6382 adjusted =
6383 stroke_adjust_path_viewport(path, stroke.width as f64, 1.0, 1.0, 0.0, 0.0);
6384 &adjusted
6385 } else {
6386 path
6387 };
6388 let Some(skia_path) = build_skia_path(draw_path) else {
6389 return;
6390 };
6391 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, self.no_aa);
6392 let transform = to_transform(¶ms.ctm);
6393
6394 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6395 let mut temp_mask = None;
6396 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6397 return; };
6399
6400 self.pixmap
6401 .stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
6402 }
6403
6404 fn clip_path(&mut self, path: &PsPath, params: &ClipParams) {
6405 self.ensure_full_pixmap();
6406 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6407
6408 if let Some(new_rect) = detect_rect(path, w, h) {
6410 match self.clip_region.take() {
6411 None => {
6412 self.clip_region = Some(ClipRegion::Rect(new_rect));
6413 }
6414 Some(ClipRegion::Rect(existing)) => {
6415 self.clip_region = Some(ClipRegion::Rect(existing.intersect(&new_rect)));
6417 }
6418 Some(ClipRegion::Mask(mut mask)) => {
6419 intersect_mask_with_rect(&mut mask, &new_rect, w, h);
6421 self.clip_region = Some(ClipRegion::Mask(mask));
6422 }
6423 }
6424 return;
6425 }
6426
6427 let fill_rule = to_fill_rule(¶ms.fill_rule);
6429 let path_hash = hash_clip_path(path, ¶ms.fill_rule);
6430 let prev_region = self.clip_region.take();
6431
6432 macro_rules! take_spare {
6434 ($self:expr, $w:expr, $h:expr) => {
6435 $self
6436 .spare_mask
6437 .take()
6438 .unwrap_or_else(|| Mask::new($w, $h).expect("Failed to create mask"))
6439 };
6440 }
6441
6442 let path_mask = if let Some(cached) = self.clip_mask_cache.get(&path_hash) {
6444 let mut mask = take_spare!(self, w, h);
6446 mask.data_mut().copy_from_slice(cached.data());
6447 mask
6448 } else {
6449 let Some(skia_path) = build_skia_path(path) else {
6450 self.clip_region = prev_region;
6451 return;
6452 };
6453 let transform = to_transform(¶ms.ctm);
6454 let mut mask = take_spare!(self, w, h);
6455 mask.data_mut().fill(0); mask.fill_path(&skia_path, fill_rule, false, transform);
6457 if !self.clip_mask_seen.insert(path_hash) {
6459 self.clip_mask_cache.insert(path_hash, mask.clone());
6461 }
6462 mask
6463 };
6464
6465 match prev_region {
6466 None => {
6467 self.clip_region = Some(ClipRegion::Mask(path_mask));
6468 }
6469 Some(ClipRegion::Rect(rect)) => {
6470 if rect.is_empty() {
6471 self.spare_mask = Some(path_mask); } else {
6473 let mut mask = path_mask;
6474 intersect_mask_with_rect(&mut mask, &rect, w, h);
6475 self.clip_region = Some(ClipRegion::Mask(mask));
6476 }
6477 }
6478 Some(ClipRegion::Mask(mut existing)) => {
6479 intersect_masks(&mut existing, &path_mask);
6480 self.spare_mask = Some(path_mask); self.clip_region = Some(ClipRegion::Mask(existing));
6482 }
6483 }
6484 }
6485
6486 fn init_clip(&mut self) {
6487 if let Some(ClipRegion::Mask(mask)) = self.clip_region.take() {
6488 self.spare_mask = Some(mask);
6489 }
6490 self.clip_region = None;
6491 }
6492
6493 fn erase_page(&mut self) {
6494 self.pixmap.fill(Color::WHITE);
6497 if let Some(ClipRegion::Mask(mask)) = self.clip_region.take() {
6498 self.spare_mask = Some(mask);
6499 }
6500 self.clip_region = None;
6501 }
6502
6503 fn show_page(&mut self, output_path: &str) -> Result<(), String> {
6504 let w = self.pixmap.width();
6505 let h = self.pixmap.height();
6506 composite_onto_white(self.pixmap.data_mut());
6508 let mut sink = self.sink_factory.create_sink(output_path)?;
6509 sink.begin_page(w, h)?;
6510 sink.write_rows(self.pixmap.data(), h)?;
6511 sink.end_page()
6512 }
6513
6514 fn draw_image(&mut self, sample_data: &[u8], params: &ImageParams) {
6515 self.ensure_full_pixmap();
6516 let w = params.width;
6517 let h = params.height;
6518 if w == 0 || h == 0 {
6519 return;
6520 }
6521 let mut rgba_data =
6522 samples_to_rgba(sample_data, params, self.render_icc_cache.as_ref(), false);
6523 if params.mask_color.is_some() {
6524 apply_mask_color_rgba(&mut rgba_data, sample_data, params);
6525 }
6526 let expected = (w * h * 4) as usize;
6527 if rgba_data.len() < expected {
6528 return;
6529 }
6530
6531 let Some(image_inv) = params.image_matrix.invert() else {
6532 return;
6533 };
6534 let combined = params.ctm.concat(&image_inv);
6535 let raw_transform = enforce_min_image_size(to_transform(&combined), w, h);
6536
6537 let prescaled = prescale_image(&rgba_data, w, h, raw_transform, params.interpolate);
6538 let (img_data, img_w, img_h, transform) = match &prescaled {
6539 Some((data, pw, ph, t)) => (data.as_slice(), *pw, *ph, *t),
6540 None => (rgba_data.as_slice(), w, h, raw_transform),
6541 };
6542
6543 let Some(img_pixmap) = stet_tiny_skia::PixmapRef::from_bytes(img_data, img_w, img_h) else {
6544 return;
6545 };
6546
6547 let (pw, ph) = (self.pixmap.width(), self.pixmap.height());
6548 let mut temp_mask = None;
6549 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, pw, ph) else {
6550 return;
6551 };
6552
6553 let paint = stet_tiny_skia::PixmapPaint {
6554 quality: image_filter_quality(transform, params.interpolate),
6555 opacity: params.alpha as f32,
6556 blend_mode: u8_to_blend_mode(params.blend_mode),
6557 };
6558 self.pixmap
6559 .draw_pixmap(0, 0, img_pixmap, &paint, transform, mask_ref);
6560 }
6561
6562 fn paint_axial_shading(&mut self, params: &AxialShadingParams) {
6563 self.ensure_full_pixmap();
6564 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6565 let mut temp_mask = None;
6566 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6567 return;
6568 };
6569 render_axial_shading(
6570 &mut self.pixmap,
6571 params,
6572 0.0,
6573 0.0,
6574 1.0,
6575 1.0,
6576 mask_ref,
6577 self.no_aa,
6578 None,
6579 None,
6580 );
6581 }
6582
6583 fn paint_radial_shading(&mut self, params: &RadialShadingParams) {
6584 self.ensure_full_pixmap();
6585 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6586 let mut temp_mask = None;
6587 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6588 return;
6589 };
6590 render_radial_shading(
6591 &mut self.pixmap,
6592 params,
6593 0.0,
6594 0.0,
6595 1.0,
6596 1.0,
6597 mask_ref,
6598 self.no_aa,
6599 None,
6600 None,
6601 );
6602 }
6603
6604 fn paint_mesh_shading(&mut self, params: &MeshShadingParams) {
6605 self.ensure_full_pixmap();
6606 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6607 let mut temp_mask = None;
6608 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6609 return;
6610 };
6611 render_mesh_shading(
6612 &mut self.pixmap,
6613 params,
6614 0.0,
6615 0.0,
6616 1.0,
6617 1.0,
6618 mask_ref,
6619 None,
6620 None,
6621 );
6622 }
6623
6624 fn paint_patch_shading(&mut self, params: &PatchShadingParams) {
6625 self.ensure_full_pixmap();
6626 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6627 let mut temp_mask = None;
6628 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6629 return;
6630 };
6631 render_patch_shading(
6632 &mut self.pixmap,
6633 params,
6634 0.0,
6635 0.0,
6636 1.0,
6637 1.0,
6638 mask_ref,
6639 None,
6640 None,
6641 );
6642 }
6643
6644 fn paint_pattern_fill(&mut self, params: &stet_graphics::device::PatternFillParams) {
6645 self.ensure_full_pixmap();
6646 let w = self.pixmap.width();
6647 let h = self.pixmap.height();
6648 let mut band_state = BandState {
6649 clip_region: self.clip_region.take(),
6650 spare_mask: self.spare_mask.take(),
6651 clip_mask_cache: HashMap::new(),
6652 clip_mask_seen: HashSet::new(),
6653 mask_pool: Vec::new(),
6654 cmyk_buffer: None,
6655 op_bg_snapshot: None,
6656 op_touched: None,
6657 spot_mask: None,
6658 };
6659 {
6660 let ctx = RenderContext {
6661 vp_x: 0.0,
6662 vp_y: 0.0,
6663 scale_x: 1.0,
6664 scale_y: 1.0,
6665 out_w: w,
6666 out_h: h,
6667 effective_dpi: self.dpi,
6668 icc: None,
6669 image_cache: None,
6670 preprocessed: None,
6671 elem_idx: 0,
6672 no_aa: self.no_aa,
6673 opm_zero_transparent: false,
6674 knockout_painter_pass: KnockoutPainterPass::None,
6675 parent_group_isolated: false,
6676 alpha_extraction_pass: false,
6677 layer_set: &self.layer_set,
6678 };
6679 render_pattern_fill(&mut self.pixmap, &mut band_state, params, &ctx);
6680 }
6681 self.clip_region = band_state.clip_region.take();
6682 if let Some(mask) = band_state.spare_mask.take() {
6683 self.spare_mask = Some(mask);
6684 }
6685 }
6686
6687 fn page_size(&self) -> (u32, u32) {
6688 (self.page_w, self.page_h)
6689 }
6690
6691 fn replay_and_show(&mut self, list: DisplayList, output_path: &str) -> Result<(), String> {
6692 self.join_pending()?;
6694
6695 let (page_w, page_h) = self.page_size();
6696
6697 if self.use_viewport_path {
6702 let icc_cache = build_icc_cache_for_list(&list, self.system_cmyk_bytes.as_ref());
6703 let rgba = render_to_rgba_viewport(
6704 &list,
6705 page_w,
6706 page_h,
6707 self.dpi,
6708 Some(&icc_cache),
6709 self.no_aa,
6710 );
6711 let mut sink = self.sink_factory.create_sink(output_path)?;
6712 sink.begin_page(page_w, page_h)?;
6713 sink.write_rows(&rgba, page_h)?;
6714 sink.end_page()?;
6715 return Ok(());
6716 }
6717
6718 let band_h = select_band_height(page_w, page_h);
6719
6720 let icc_cache = build_icc_cache_for_list(&list, self.system_cmyk_bytes.as_ref());
6722
6723 if band_h >= page_h {
6727 self.ensure_full_pixmap();
6728 let ctx = RenderContext {
6729 vp_x: 0.0,
6730 vp_y: 0.0,
6731 scale_x: 1.0,
6732 scale_y: 1.0,
6733 out_w: page_w,
6734 out_h: page_h,
6735 effective_dpi: self.dpi,
6736 icc: Some(&icc_cache),
6737 image_cache: None,
6738 preprocessed: None,
6739 elem_idx: 0,
6740 no_aa: self.no_aa,
6741 opm_zero_transparent: false,
6742 knockout_painter_pass: KnockoutPainterPass::None,
6743 parent_group_isolated: false,
6744 alpha_extraction_pass: false,
6745 layer_set: &self.layer_set,
6746 };
6747 let mut band_state = BandState {
6748 clip_region: None,
6749 spare_mask: None,
6750 clip_mask_cache: HashMap::new(),
6751 clip_mask_seen: HashSet::new(),
6752 mask_pool: Vec::new(),
6753 cmyk_buffer: None,
6754 op_bg_snapshot: None,
6755 op_touched: None,
6756 spot_mask: None,
6757 };
6758 for (idx, elem) in list.elements().iter().enumerate() {
6759 let elem_ctx = RenderContext {
6760 elem_idx: idx,
6761 ..ctx
6762 };
6763 render_element(&mut self.pixmap, &mut band_state, elem, &elem_ctx);
6764 }
6765 return self.show_page(output_path);
6766 }
6767
6768 if self.pixmap.width() > 1 {
6772 self.pixmap = Pixmap::new(1, 1).expect("Failed to create placeholder pixmap");
6773 }
6774
6775 let mut sink = self.sink_factory.create_sink(output_path)?;
6777 let dpi = self.dpi;
6778 let layer_set = self.layer_set.clone();
6779
6780 #[cfg(feature = "parallel")]
6781 {
6782 let no_aa = self.no_aa;
6786 let (tx, rx) = std::sync::mpsc::sync_channel(1);
6787 rayon::spawn(move || {
6788 let result = render_banded_to_sink(
6789 page_w, page_h, band_h, dpi, &list, &mut *sink, &icc_cache, no_aa, &layer_set,
6790 );
6791 let _ = tx.send(result);
6792 });
6793 self.pending_render = Some(rx);
6794 }
6795 #[cfg(not(feature = "parallel"))]
6796 {
6797 render_banded_to_sink(
6798 page_w, page_h, band_h, dpi, &list, &mut *sink, &icc_cache, self.no_aa, &layer_set,
6799 )?;
6800 }
6801
6802 Ok(())
6803 }
6804
6805 fn finish(&mut self) -> Result<(), String> {
6806 self.join_pending()
6807 }
6808}
6809
6810impl Drop for SkiaDevice {
6811 fn drop(&mut self) {
6812 if let Some(rx) = self.pending_render.take() {
6814 let _ = rx.recv();
6815 }
6816 }
6817}
6818
6819impl SkiaDevice {
6820 fn join_pending(&mut self) -> Result<(), String> {
6822 if let Some(rx) = self.pending_render.take() {
6823 match rx.recv() {
6824 Ok(result) => result?,
6825 Err(_) => return Err("Background render task failed".to_string()),
6826 }
6827 }
6828 Ok(())
6829 }
6830}
6831
6832fn has_cmyk_group(list: &DisplayList) -> bool {
6837 use stet_graphics::display_list::GroupColorSpace;
6838 for elem in list.elements() {
6839 match elem {
6840 DisplayElement::Group { elements, params } => {
6841 if params.color_space == GroupColorSpace::DeviceCMYK {
6842 return true;
6843 }
6844 if has_cmyk_group(elements) {
6845 return true;
6846 }
6847 }
6848 DisplayElement::SoftMasked { content, mask, .. } => {
6849 if has_cmyk_group(content) || has_cmyk_group(mask) {
6850 return true;
6851 }
6852 }
6853 DisplayElement::OcgGroup { elements, .. } => {
6854 if has_cmyk_group(elements) {
6855 return true;
6856 }
6857 }
6858 _ => {}
6859 }
6860 }
6861 false
6862}
6863
6864fn group_only_native_cmyk_fills(elements: &DisplayList) -> bool {
6871 let mut found_paint = false;
6872 for elem in elements.elements() {
6873 match elem {
6874 DisplayElement::InitClip => continue,
6875 DisplayElement::Clip { .. } => continue,
6876 DisplayElement::Fill { params, .. } => {
6877 if params.color.native_cmyk.is_none() {
6878 return false;
6879 }
6880 found_paint = true;
6881 }
6882 DisplayElement::Stroke { params, .. } => {
6883 if params.color.native_cmyk.is_none() {
6889 return false;
6890 }
6891 found_paint = true;
6892 }
6893 _ => return false,
6894 }
6895 }
6896 found_paint
6897}
6898
6899fn group_content_is_native_cmyk(elements: &DisplayList) -> bool {
6920 let mut found_paint = false;
6921 for elem in elements.elements() {
6922 match elem {
6923 DisplayElement::InitClip => continue,
6924 DisplayElement::Clip { .. } => continue,
6925 DisplayElement::Text { .. } => continue,
6926 DisplayElement::ErasePage => continue,
6927 DisplayElement::Fill { params, .. } => {
6928 if params.color.native_cmyk.is_none() {
6929 return false;
6930 }
6931 found_paint = true;
6932 }
6933 DisplayElement::Stroke { params, .. } => {
6934 if params.color.native_cmyk.is_none() {
6935 return false;
6936 }
6937 found_paint = true;
6938 }
6939 DisplayElement::Image { params, .. } => {
6940 if !is_cmyk_color_space(¶ms.color_space) {
6941 return false;
6942 }
6943 found_paint = true;
6944 }
6945 DisplayElement::AxialShading { .. }
6946 | DisplayElement::RadialShading { .. }
6947 | DisplayElement::MeshShading { .. }
6948 | DisplayElement::PatchShading { .. } => {
6949 return false;
6952 }
6953 DisplayElement::PatternFill { .. } => {
6954 return false;
6958 }
6959 DisplayElement::Group { elements: sub, .. } => {
6960 if !group_content_is_native_cmyk(sub) {
6961 return false;
6962 }
6963 found_paint = true;
6964 }
6965 DisplayElement::SoftMasked { .. } => {
6966 return false;
6977 }
6978 DisplayElement::OcgGroup { elements: sub, .. } => {
6979 if !group_content_is_native_cmyk(sub) {
6980 return false;
6981 }
6982 found_paint = true;
6983 }
6984 _ => return false,
6985 }
6986 }
6987 found_paint
6988}
6989
6990fn content_list_is_simple_native_cmyk(list: &DisplayList) -> bool {
6997 let mut found_paint = false;
6998 for elem in list.elements() {
6999 match elem {
7000 DisplayElement::InitClip
7001 | DisplayElement::Clip { .. }
7002 | DisplayElement::Text { .. }
7003 | DisplayElement::ErasePage => continue,
7004 DisplayElement::Fill { params, .. } => {
7005 if params.color.native_cmyk.is_none() {
7006 return false;
7007 }
7008 if params.blend_mode != 0 || params.alpha != 1.0 {
7009 return false;
7010 }
7011 found_paint = true;
7012 }
7013 DisplayElement::Stroke { params, .. } => {
7014 if params.color.native_cmyk.is_none() {
7015 return false;
7016 }
7017 if params.blend_mode != 0 || params.alpha != 1.0 {
7018 return false;
7019 }
7020 found_paint = true;
7021 }
7022 DisplayElement::Group { params, elements } => {
7030 if params.blend_mode != 0 || params.alpha != 1.0 {
7031 return false;
7032 }
7033 if !content_list_is_simple_native_cmyk(elements) {
7034 return false;
7035 }
7036 if elements.elements().iter().any(|e| {
7040 matches!(
7041 e,
7042 DisplayElement::Fill { .. } | DisplayElement::Stroke { .. }
7043 )
7044 }) {
7045 found_paint = true;
7046 }
7047 }
7048 _ => return false,
7049 }
7050 }
7051 found_paint
7052}
7053
7054fn has_overprint_elements(list: &DisplayList) -> bool {
7056 for elem in list.elements() {
7057 match elem {
7058 DisplayElement::Fill { params, .. } => {
7059 if params.overprint {
7060 return true;
7061 }
7062 }
7063 DisplayElement::Stroke { params, .. } => {
7064 if params.overprint {
7065 return true;
7066 }
7067 }
7068 DisplayElement::Image { params, .. } => {
7069 if params.overprint {
7070 return true;
7071 }
7072 }
7073 DisplayElement::AxialShading { params } => {
7074 if params.overprint {
7075 return true;
7076 }
7077 }
7078 DisplayElement::RadialShading { params } => {
7079 if params.overprint {
7080 return true;
7081 }
7082 }
7083 DisplayElement::MeshShading { params } => {
7084 if params.overprint {
7085 return true;
7086 }
7087 }
7088 DisplayElement::PatchShading { params } => {
7089 if params.overprint {
7090 return true;
7091 }
7092 }
7093 DisplayElement::Group { elements, .. } => {
7094 if has_overprint_elements(elements) {
7095 return true;
7096 }
7097 }
7098 DisplayElement::SoftMasked { content, mask, .. } => {
7099 if has_overprint_elements(content) || has_overprint_elements(mask) {
7100 return true;
7101 }
7102 }
7103 DisplayElement::OcgGroup { elements, .. } => {
7104 if has_overprint_elements(elements) {
7105 return true;
7106 }
7107 }
7108 _ => {}
7109 }
7110 }
7111 false
7112}
7113
7114#[allow(clippy::too_many_arguments)]
7117fn render_overprint_fill(
7118 pixmap: &mut Pixmap,
7119 cmyk_buf: &mut [f32],
7120 op_bg: &mut [u8],
7121 op_touched: &mut [u8],
7122 spot_mask: &[u8],
7123 band_state: &mut BandState,
7124 path: &PsPath,
7125 params: &FillParams,
7126 vp_x: f32,
7127 vp_y: f32,
7128 scale_x: f32,
7129 scale_y: f32,
7130 out_w: u32,
7131 out_h: u32,
7132 icc: Option<&IccCache>,
7133 no_aa: bool,
7134) {
7135 let Some(skia_path) = build_skia_path(path) else {
7136 return;
7137 };
7138 let fill_rule = to_fill_rule(¶ms.fill_rule);
7139
7140 let mut coverage_mask = match Mask::new(out_w, out_h) {
7141 Some(m) => m,
7142 None => return,
7143 };
7144 let transform = viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
7145 coverage_mask.fill_path(&skia_path, fill_rule, !no_aa, transform);
7146
7147 let (bbox_x0, bbox_y0, bbox_x1, bbox_y1) =
7149 path_device_bbox(&skia_path, transform, out_w, out_h);
7150
7151 let clip_coverage: Option<&[u8]> = match &band_state.clip_region {
7153 None => None,
7154 Some(ClipRegion::Rect(r)) => {
7155 let data = coverage_mask.data_mut();
7157 let stride = out_w as usize;
7158 for y in bbox_y0..bbox_y1 {
7159 let row_start = y * stride;
7160 for x in bbox_x0..bbox_x1 {
7161 let yu = y as u32;
7162 let xu = x as u32;
7163 if yu < r.y0 || yu >= r.y1 || xu < r.x0 || xu >= r.x1 {
7164 data[row_start + x] = 0;
7165 }
7166 }
7167 }
7168 None
7169 }
7170 Some(ClipRegion::Mask(clip_mask)) => Some(clip_mask.data()),
7171 };
7172
7173 let (src_c, src_m, src_y, src_k) = params.color.native_cmyk.unwrap_or_else(|| {
7174 let r = params.color.r;
7175 let g = params.color.g;
7176 let b = params.color.b;
7177 (1.0 - r, 1.0 - g, 1.0 - b, 0.0)
7178 });
7179
7180 let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
7186
7187 let mut channels = params.painted_channels;
7188 if channels == 0 {
7191 channels = stet_graphics::device::CMYK_ALL;
7192 }
7193 if params.overprint_mode == 1
7195 && channels == stet_graphics::device::CMYK_ALL
7196 && params.is_device_cmyk
7197 {
7198 channels = 0;
7199 if src_c != 0.0 {
7200 channels |= stet_graphics::device::CMYK_C;
7201 }
7202 if src_m != 0.0 {
7203 channels |= stet_graphics::device::CMYK_M;
7204 }
7205 if src_y != 0.0 {
7206 channels |= stet_graphics::device::CMYK_Y;
7207 }
7208 if src_k != 0.0 {
7209 channels |= stet_graphics::device::CMYK_K;
7210 }
7211 if channels == 0 && !params.opm_paired {
7222 channels = stet_graphics::device::CMYK_ALL;
7223 }
7224 }
7225
7226 let is_k_only_cmyk = params.is_device_cmyk
7234 && params.overprint_mode == 0
7235 && src_c == 0.0
7236 && src_m == 0.0
7237 && src_y == 0.0;
7238 if channels == stet_graphics::device::CMYK_ALL && !is_custom_spot && !is_k_only_cmyk {
7239 let cov_data = coverage_mask.data();
7240 let stride = out_w as usize;
7241 for y in bbox_y0..bbox_y1 {
7242 for x in bbox_x0..bbox_x1 {
7243 let mi = y * stride + x;
7244 let mut cov = cov_data[mi] as f32 / 255.0;
7245 if let Some(clip) = clip_coverage {
7246 cov *= clip[mi] as f32 / 255.0;
7247 }
7248 if cov > 0.0 {
7249 let ci = mi * 4;
7250 cmyk_buf[ci] = src_c as f32;
7251 cmyk_buf[ci + 1] = src_m as f32;
7252 cmyk_buf[ci + 2] = src_y as f32;
7253 cmyk_buf[ci + 3] = src_k as f32;
7254 }
7255 }
7256 }
7257 let mut temp_mask = None;
7258 let Some(mask_ref) =
7259 resolve_clip_mask(&band_state.clip_region, &mut temp_mask, out_w, out_h)
7260 else {
7261 return;
7262 };
7263 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, no_aa);
7264 pixmap.fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
7265 return;
7266 }
7267
7268 let cov_data = coverage_mask.data();
7269 let stride = out_w as usize;
7270 let px_data = pixmap.data_mut();
7271 let px_stride = out_w as usize * 4;
7272
7273 for y in bbox_y0..bbox_y1 {
7274 for x in bbox_x0..bbox_x1 {
7275 let mi = y * stride + x;
7276 let mut cov = cov_data[mi] as f32 / 255.0;
7277 if let Some(clip) = clip_coverage {
7278 cov *= clip[mi] as f32 / 255.0;
7279 }
7280 if cov <= 0.0 {
7281 continue;
7282 }
7283
7284 let ci = mi * 4;
7285 let pi = y * px_stride + x * 4;
7286 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
7293 op_bg[pi] = px_data[pi];
7294 op_bg[pi + 1] = px_data[pi + 1];
7295 op_bg[pi + 2] = px_data[pi + 2];
7296 op_bg[pi + 3] = px_data[pi + 3];
7297 op_touched[mi] = 1;
7298 }
7299 let cur_c = cmyk_buf[ci] as f64;
7300 let cur_m = cmyk_buf[ci + 1] as f64;
7301 let cur_y = cmyk_buf[ci + 2] as f64;
7302 let cur_k = cmyk_buf[ci + 3] as f64;
7303 let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
7315 let pixmap_has_colour = px_data[pi + 3] > 0
7316 && (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
7317 let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
7325
7326 let is_promoted_gray = params.painted_channels == stet_graphics::device::CMYK_K
7342 && channels == stet_graphics::device::CMYK_K
7343 && params.is_device_cmyk
7344 && src_c == 0.0
7345 && src_m == 0.0
7346 && src_y == 0.0;
7347 let effective_channels = if is_promoted_gray && spot_mask[mi] == 0 {
7348 stet_graphics::device::CMYK_ALL
7349 } else {
7350 channels
7351 };
7352
7353 let new_c = if effective_channels & stet_graphics::device::CMYK_C != 0 {
7354 src_c
7355 } else {
7356 cur_c
7357 };
7358 let new_m = if effective_channels & stet_graphics::device::CMYK_M != 0 {
7359 src_m
7360 } else {
7361 cur_m
7362 };
7363 let new_y = if effective_channels & stet_graphics::device::CMYK_Y != 0 {
7364 src_y
7365 } else {
7366 cur_y
7367 };
7368 let new_k = if effective_channels & stet_graphics::device::CMYK_K != 0 {
7369 src_k
7370 } else {
7371 cur_k
7372 };
7373
7374 if !is_custom_spot {
7378 cmyk_buf[ci] = new_c as f32;
7379 cmyk_buf[ci + 1] = new_m as f32;
7380 cmyk_buf[ci + 2] = new_y as f32;
7381 cmyk_buf[ci + 3] = new_k as f32;
7382 }
7383
7384 let delta = (new_c - cur_c)
7398 .abs()
7399 .max((new_m - cur_m).abs())
7400 .max((new_y - cur_y).abs())
7401 .max((new_k - cur_k).abs());
7402 if delta < 1e-4 && spot_mask[mi] != 0 && pixmap_has_colour && !is_custom_spot {
7403 continue;
7404 }
7405
7406 let (r, g, b) =
7407 if is_promoted_gray && effective_channels == stet_graphics::device::CMYK_ALL {
7408 (params.color.r, params.color.g, params.color.b)
7423 } else if use_multiplicative {
7424 let bg_r = px_data[pi] as f64 / 255.0;
7430 let bg_g = px_data[pi + 1] as f64 / 255.0;
7431 let bg_b = px_data[pi + 2] as f64 / 255.0;
7432 let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
7433 1.0 - src_c
7434 } else {
7435 1.0
7436 };
7437 let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
7438 1.0 - src_m
7439 } else {
7440 1.0
7441 };
7442 let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
7443 1.0 - src_y
7444 } else {
7445 1.0
7446 };
7447 let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
7448 1.0 - src_k
7449 } else {
7450 1.0
7451 };
7452 (
7453 (bg_r * over_r * k_fac).clamp(0.0, 1.0),
7454 (bg_g * over_g * k_fac).clamp(0.0, 1.0),
7455 (bg_b * over_b * k_fac).clamp(0.0, 1.0),
7456 )
7457 } else if let Some(icc_cache) = icc {
7458 icc_cache
7459 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
7460 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
7461 } else {
7462 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
7463 };
7464
7465 let a = (cov * params.alpha as f32).min(1.0);
7466 let (bk_r, bk_g, bk_b, bk_a) = if op_touched[mi] != 0 {
7479 let new_r = (r as f32 * 255.0).clamp(0.0, 255.0);
7480 let new_g = (g as f32 * 255.0).clamp(0.0, 255.0);
7481 let new_b = (b as f32 * 255.0).clamp(0.0, 255.0);
7482 let dr = (op_bg[pi] as f32 - new_r).abs();
7483 let dg = (op_bg[pi + 1] as f32 - new_g).abs();
7484 let db = (op_bg[pi + 2] as f32 - new_b).abs();
7485 if dr.max(dg).max(db) <= 4.0 {
7486 (op_bg[pi], op_bg[pi + 1], op_bg[pi + 2], op_bg[pi + 3])
7487 } else {
7488 (
7489 px_data[pi],
7490 px_data[pi + 1],
7491 px_data[pi + 2],
7492 px_data[pi + 3],
7493 )
7494 }
7495 } else {
7496 (
7497 px_data[pi],
7498 px_data[pi + 1],
7499 px_data[pi + 2],
7500 px_data[pi + 3],
7501 )
7502 };
7503 let dst_a = bk_a as f32 / 255.0;
7504 let one_minus_a = 1.0 - a;
7505 let out_a = a + dst_a * one_minus_a;
7506 if out_a > 0.0 {
7507 px_data[pi] = ((r as f32 * a + (bk_r as f32 / 255.0) * one_minus_a) * 255.0)
7512 .clamp(0.0, 255.0)
7513 .round() as u8;
7514 px_data[pi + 1] = ((g as f32 * a + (bk_g as f32 / 255.0) * one_minus_a) * 255.0)
7515 .clamp(0.0, 255.0)
7516 .round() as u8;
7517 px_data[pi + 2] = ((b as f32 * a + (bk_b as f32 / 255.0) * one_minus_a) * 255.0)
7518 .clamp(0.0, 255.0)
7519 .round() as u8;
7520 px_data[pi + 3] = (out_a * 255.0).round() as u8;
7521 }
7522 }
7523 }
7524}
7525fn cmyk_to_rgb_plrm(c: f64, m: f64, y: f64, k: f64) -> (f64, f64, f64) {
7527 (
7528 1.0 - (c + k).min(1.0),
7529 1.0 - (m + k).min(1.0),
7530 1.0 - (y + k).min(1.0),
7531 )
7532}
7533
7534#[allow(clippy::too_many_arguments)]
7536fn path_device_bbox(
7539 skia_path: &stet_tiny_skia::Path,
7540 transform: Transform,
7541 w: u32,
7542 h: u32,
7543) -> (usize, usize, usize, usize) {
7544 let b = skia_path.bounds();
7545 let mut corners = [
7546 stet_tiny_skia::Point {
7547 x: b.left(),
7548 y: b.top(),
7549 },
7550 stet_tiny_skia::Point {
7551 x: b.right(),
7552 y: b.top(),
7553 },
7554 stet_tiny_skia::Point {
7555 x: b.right(),
7556 y: b.bottom(),
7557 },
7558 stet_tiny_skia::Point {
7559 x: b.left(),
7560 y: b.bottom(),
7561 },
7562 ];
7563 transform.map_points(&mut corners);
7564 let min_x = corners.iter().map(|p| p.x).fold(f32::INFINITY, f32::min);
7565 let min_y = corners.iter().map(|p| p.y).fold(f32::INFINITY, f32::min);
7566 let max_x = corners
7567 .iter()
7568 .map(|p| p.x)
7569 .fold(f32::NEG_INFINITY, f32::max);
7570 let max_y = corners
7571 .iter()
7572 .map(|p| p.y)
7573 .fold(f32::NEG_INFINITY, f32::max);
7574 let x0 = (min_x.floor() as i32 - 1).max(0) as usize;
7576 let y0 = (min_y.floor() as i32 - 1).max(0) as usize;
7577 let x1 = (max_x.ceil() as i32 + 1).clamp(0, w as i32) as usize;
7578 let y1 = (max_y.ceil() as i32 + 1).clamp(0, h as i32) as usize;
7579 (x0, y0, x1, y1)
7580}
7581
7582fn update_cmyk_buffer_for_fill(
7583 cmyk_buf: &mut [f32],
7584 spot_mask: &mut [u8],
7585 path: &PsPath,
7586 params: &FillParams,
7587 vp_x: f32,
7588 vp_y: f32,
7589 scale_x: f32,
7590 scale_y: f32,
7591 out_w: u32,
7592 out_h: u32,
7593 clip_region: &Option<ClipRegion>,
7594 no_aa: bool,
7595 icc: Option<&IccCache>,
7596) {
7597 let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
7603
7604 let has_spot_contrib = (is_custom_spot && params.color.native_cmyk.is_some())
7618 || matches!(
7619 (params.color.native_cmyk, params.color.process_cmyk),
7620 (Some(nat), Some(proc_))
7621 if (nat.0 - proc_.0).abs() > 1e-6
7622 || (nat.1 - proc_.1).abs() > 1e-6
7623 || (nat.2 - proc_.2).abs() > 1e-6
7624 || (nat.3 - proc_.3).abs() > 1e-6
7625 );
7626
7627 let (src_c, src_m, src_y, src_k) = if is_custom_spot {
7635 (0.0, 0.0, 0.0, 0.0)
7636 } else if let Some(c) = params.color.process_cmyk {
7637 c
7638 } else if let Some(c) = params.color.native_cmyk {
7639 c
7640 } else if let Some(cmyk) = icc.and_then(|i| {
7641 i.convert_rgb_to_cmyk_readonly(params.color.r, params.color.g, params.color.b)
7642 }) {
7643 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
7644 } else {
7645 (
7646 (1.0 - params.color.r).clamp(0.0, 1.0),
7647 (1.0 - params.color.g).clamp(0.0, 1.0),
7648 (1.0 - params.color.b).clamp(0.0, 1.0),
7649 0.0,
7650 )
7651 };
7652 let Some(skia_path) = build_skia_path(path) else {
7653 return;
7654 };
7655
7656 let mut coverage_mask = match Mask::new(out_w, out_h) {
7657 Some(m) => m,
7658 None => return,
7659 };
7660 let transform = viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
7661 let fill_rule = to_fill_rule(¶ms.fill_rule);
7662 coverage_mask.fill_path(&skia_path, fill_rule, !no_aa, transform);
7663
7664 let cov_data = coverage_mask.data();
7665 let clip_data: Option<&[u8]> = match clip_region {
7666 Some(ClipRegion::Mask(m)) => Some(m.data()),
7667 _ => None,
7668 };
7669
7670 let (mut bx0, mut by0, mut bx1, mut by1) =
7672 path_device_bbox(&skia_path, transform, out_w, out_h);
7673 if let Some(ClipRegion::Rect(r)) = clip_region {
7674 bx0 = bx0.max(r.x0 as usize);
7675 by0 = by0.max(r.y0 as usize);
7676 bx1 = bx1.min(r.x1 as usize);
7677 by1 = by1.min(r.y1 as usize);
7678 }
7679
7680 let stride = out_w as usize;
7681 for y in by0..by1 {
7682 for x in bx0..bx1 {
7683 let mi = y * stride + x;
7684 let mut cov = cov_data[mi] as f32 / 255.0;
7685 if let Some(clip) = clip_data {
7686 cov *= clip[mi] as f32 / 255.0;
7687 }
7688 if cov > 0.0 {
7689 let ci = mi * 4;
7690 cmyk_buf[ci] = src_c as f32;
7691 cmyk_buf[ci + 1] = src_m as f32;
7692 cmyk_buf[ci + 2] = src_y as f32;
7693 cmyk_buf[ci + 3] = src_k as f32;
7694 if has_spot_contrib {
7695 spot_mask[mi] = 1;
7696 }
7697 }
7698 }
7699 }
7700}
7701
7702#[allow(clippy::too_many_arguments)]
7707fn render_overprint_stroke(
7708 pixmap: &mut Pixmap,
7709 cmyk_buf: &mut [f32],
7710 op_bg: &mut [u8],
7711 op_touched: &mut [u8],
7712 spot_mask: &[u8],
7713 band_state: &mut BandState,
7714 skia_path: &stet_tiny_skia::Path,
7715 stroke: &Stroke,
7716 transform: Transform,
7717 params: &StrokeParams,
7718 out_w: u32,
7719 out_h: u32,
7720 icc: Option<&IccCache>,
7721 no_aa: bool,
7722) {
7723 let resolution_scale = (transform.sx * transform.sx + transform.sy * transform.sy)
7725 .sqrt()
7726 .max(1.0);
7727 let dashed_op;
7728 let stroke_src = if let Some(ref dash) = stroke.dash {
7729 dashed_op = skia_path.dash(dash, resolution_scale);
7730 match dashed_op.as_ref() {
7731 Some(p) => p,
7732 None => skia_path,
7733 }
7734 } else {
7735 skia_path
7736 };
7737 let Some(stroked_user) = stroke_src.stroke(stroke, resolution_scale) else {
7738 return;
7739 };
7740 let Some(stroked) = stroked_user.transform(transform) else {
7741 return;
7742 };
7743
7744 let mut coverage_mask = match Mask::new(out_w, out_h) {
7745 Some(m) => m,
7746 None => return,
7747 };
7748 coverage_mask.fill_path(
7749 &stroked,
7750 SkiaFillRule::Winding,
7751 !no_aa,
7752 Transform::identity(),
7753 );
7754
7755 let (bbox_x0, bbox_y0, bbox_x1, bbox_y1) =
7756 path_device_bbox(&stroked, Transform::identity(), out_w, out_h);
7757
7758 let clip_coverage: Option<&[u8]> = match &band_state.clip_region {
7760 None => None,
7761 Some(ClipRegion::Rect(r)) => {
7762 let data = coverage_mask.data_mut();
7763 let stride = out_w as usize;
7764 for y in bbox_y0..bbox_y1 {
7765 let row_start = y * stride;
7766 for x in bbox_x0..bbox_x1 {
7767 let yu = y as u32;
7768 let xu = x as u32;
7769 if yu < r.y0 || yu >= r.y1 || xu < r.x0 || xu >= r.x1 {
7770 data[row_start + x] = 0;
7771 }
7772 }
7773 }
7774 None
7775 }
7776 Some(ClipRegion::Mask(clip_mask)) => Some(clip_mask.data()),
7777 };
7778
7779 let (src_c, src_m, src_y, src_k) = params.color.native_cmyk.unwrap_or_else(|| {
7780 let r = params.color.r;
7781 let g = params.color.g;
7782 let b = params.color.b;
7783 (1.0 - r, 1.0 - g, 1.0 - b, 0.0)
7784 });
7785
7786 let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
7790
7791 let mut channels = params.painted_channels;
7792 if channels == 0 {
7793 channels = stet_graphics::device::CMYK_ALL;
7794 }
7795 if params.overprint_mode == 1
7796 && channels == stet_graphics::device::CMYK_ALL
7797 && params.is_device_cmyk
7798 {
7799 channels = 0;
7800 if src_c != 0.0 {
7801 channels |= stet_graphics::device::CMYK_C;
7802 }
7803 if src_m != 0.0 {
7804 channels |= stet_graphics::device::CMYK_M;
7805 }
7806 if src_y != 0.0 {
7807 channels |= stet_graphics::device::CMYK_Y;
7808 }
7809 if src_k != 0.0 {
7810 channels |= stet_graphics::device::CMYK_K;
7811 }
7812 if channels == 0 && !params.opm_paired {
7816 channels = stet_graphics::device::CMYK_ALL;
7817 }
7818 }
7819
7820 let is_k_only_cmyk = params.is_device_cmyk
7821 && params.overprint_mode == 0
7822 && src_c == 0.0
7823 && src_m == 0.0
7824 && src_y == 0.0;
7825 if channels == stet_graphics::device::CMYK_ALL && !is_custom_spot && !is_k_only_cmyk {
7826 let cov_data = coverage_mask.data();
7831 let stride = out_w as usize;
7832 for y in bbox_y0..bbox_y1 {
7833 for x in bbox_x0..bbox_x1 {
7834 let mi = y * stride + x;
7835 let mut cov = cov_data[mi] as f32 / 255.0;
7836 if let Some(clip) = clip_coverage {
7837 cov *= clip[mi] as f32 / 255.0;
7838 }
7839 if cov > 0.0 {
7840 let ci = mi * 4;
7841 cmyk_buf[ci] = src_c as f32;
7842 cmyk_buf[ci + 1] = src_m as f32;
7843 cmyk_buf[ci + 2] = src_y as f32;
7844 cmyk_buf[ci + 3] = src_k as f32;
7845 }
7846 }
7847 }
7848 let mut temp_mask = None;
7849 let Some(mask_ref) =
7850 resolve_clip_mask(&band_state.clip_region, &mut temp_mask, out_w, out_h)
7851 else {
7852 return;
7853 };
7854 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, no_aa);
7855 pixmap.stroke_path(skia_path, &paint, stroke, transform, mask_ref);
7856 return;
7857 }
7858
7859 let cov_data = coverage_mask.data();
7860 let stride = out_w as usize;
7861 let px_data = pixmap.data_mut();
7862 let px_stride = out_w as usize * 4;
7863
7864 for y in bbox_y0..bbox_y1 {
7865 for x in bbox_x0..bbox_x1 {
7866 let mi = y * stride + x;
7867 let mut cov = cov_data[mi] as f32 / 255.0;
7868 if let Some(clip) = clip_coverage {
7869 cov *= clip[mi] as f32 / 255.0;
7870 }
7871 if cov <= 0.0 {
7872 continue;
7873 }
7874
7875 let ci = mi * 4;
7876 let pi = y * px_stride + x * 4;
7877 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
7882 op_bg[pi] = px_data[pi];
7883 op_bg[pi + 1] = px_data[pi + 1];
7884 op_bg[pi + 2] = px_data[pi + 2];
7885 op_bg[pi + 3] = px_data[pi + 3];
7886 op_touched[mi] = 1;
7887 }
7888 let cur_c = cmyk_buf[ci] as f64;
7889 let cur_m = cmyk_buf[ci + 1] as f64;
7890 let cur_y = cmyk_buf[ci + 2] as f64;
7891 let cur_k = cmyk_buf[ci + 3] as f64;
7892 let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
7893 let pixmap_has_colour = px_data[pi + 3] > 0
7894 && (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
7895 let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
7903
7904 let is_promoted_gray = params.painted_channels == stet_graphics::device::CMYK_K
7907 && channels == stet_graphics::device::CMYK_K
7908 && params.is_device_cmyk
7909 && src_c == 0.0
7910 && src_m == 0.0
7911 && src_y == 0.0;
7912 let effective_channels = if is_promoted_gray && spot_mask[mi] == 0 {
7913 stet_graphics::device::CMYK_ALL
7914 } else {
7915 channels
7916 };
7917
7918 let new_c = if effective_channels & stet_graphics::device::CMYK_C != 0 {
7919 src_c
7920 } else {
7921 cur_c
7922 };
7923 let new_m = if effective_channels & stet_graphics::device::CMYK_M != 0 {
7924 src_m
7925 } else {
7926 cur_m
7927 };
7928 let new_y = if effective_channels & stet_graphics::device::CMYK_Y != 0 {
7929 src_y
7930 } else {
7931 cur_y
7932 };
7933 let new_k = if effective_channels & stet_graphics::device::CMYK_K != 0 {
7934 src_k
7935 } else {
7936 cur_k
7937 };
7938
7939 if !is_custom_spot {
7940 cmyk_buf[ci] = new_c as f32;
7941 cmyk_buf[ci + 1] = new_m as f32;
7942 cmyk_buf[ci + 2] = new_y as f32;
7943 cmyk_buf[ci + 3] = new_k as f32;
7944 }
7945
7946 let delta = (new_c - cur_c)
7948 .abs()
7949 .max((new_m - cur_m).abs())
7950 .max((new_y - cur_y).abs())
7951 .max((new_k - cur_k).abs());
7952 if delta < 1e-4 && spot_mask[mi] != 0 && pixmap_has_colour && !is_custom_spot {
7953 continue;
7954 }
7955
7956 let (r, g, b) =
7957 if is_promoted_gray && effective_channels == stet_graphics::device::CMYK_ALL {
7958 (params.color.r, params.color.g, params.color.b)
7964 } else if use_multiplicative {
7965 let bg_r = px_data[pi] as f64 / 255.0;
7966 let bg_g = px_data[pi + 1] as f64 / 255.0;
7967 let bg_b = px_data[pi + 2] as f64 / 255.0;
7968 let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
7969 1.0 - src_c
7970 } else {
7971 1.0
7972 };
7973 let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
7974 1.0 - src_m
7975 } else {
7976 1.0
7977 };
7978 let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
7979 1.0 - src_y
7980 } else {
7981 1.0
7982 };
7983 let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
7984 1.0 - src_k
7985 } else {
7986 1.0
7987 };
7988 (
7989 (bg_r * over_r * k_fac).clamp(0.0, 1.0),
7990 (bg_g * over_g * k_fac).clamp(0.0, 1.0),
7991 (bg_b * over_b * k_fac).clamp(0.0, 1.0),
7992 )
7993 } else if let Some(icc_cache) = icc {
7994 icc_cache
7995 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
7996 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
7997 } else {
7998 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
7999 };
8000
8001 let a = (cov * params.alpha as f32).min(1.0);
8002 let (bk_r, bk_g, bk_b, bk_a) = if op_touched[mi] != 0 {
8007 let new_r = (r as f32 * 255.0).clamp(0.0, 255.0);
8008 let new_g = (g as f32 * 255.0).clamp(0.0, 255.0);
8009 let new_b = (b as f32 * 255.0).clamp(0.0, 255.0);
8010 let dr = (op_bg[pi] as f32 - new_r).abs();
8011 let dg = (op_bg[pi + 1] as f32 - new_g).abs();
8012 let db = (op_bg[pi + 2] as f32 - new_b).abs();
8013 if dr.max(dg).max(db) <= 4.0 {
8014 (op_bg[pi], op_bg[pi + 1], op_bg[pi + 2], op_bg[pi + 3])
8015 } else {
8016 (
8017 px_data[pi],
8018 px_data[pi + 1],
8019 px_data[pi + 2],
8020 px_data[pi + 3],
8021 )
8022 }
8023 } else {
8024 (
8025 px_data[pi],
8026 px_data[pi + 1],
8027 px_data[pi + 2],
8028 px_data[pi + 3],
8029 )
8030 };
8031 let dst_a = bk_a as f32 / 255.0;
8032 let one_minus_a = 1.0 - a;
8033 let out_a = a + dst_a * one_minus_a;
8034 if out_a > 0.0 {
8035 px_data[pi] = ((r as f32 * a + (bk_r as f32 / 255.0) * one_minus_a) * 255.0)
8037 .clamp(0.0, 255.0)
8038 .round() as u8;
8039 px_data[pi + 1] = ((g as f32 * a + (bk_g as f32 / 255.0) * one_minus_a) * 255.0)
8040 .clamp(0.0, 255.0)
8041 .round() as u8;
8042 px_data[pi + 2] = ((b as f32 * a + (bk_b as f32 / 255.0) * one_minus_a) * 255.0)
8043 .clamp(0.0, 255.0)
8044 .round() as u8;
8045 px_data[pi + 3] = (out_a * 255.0).round() as u8;
8046 }
8047 }
8048 }
8049}
8050
8051#[allow(clippy::too_many_arguments)]
8056fn update_cmyk_buffer_for_stroke(
8057 cmyk_buf: &mut [f32],
8058 spot_mask: &mut [u8],
8059 path: &PsPath,
8060 params: &StrokeParams,
8061 stroke: &Stroke,
8062 transform: Transform,
8063 out_w: u32,
8064 out_h: u32,
8065 clip_region: &Option<ClipRegion>,
8066 no_aa: bool,
8067 icc: Option<&IccCache>,
8068) {
8069 let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
8073 let has_spot_contrib = (is_custom_spot && params.color.native_cmyk.is_some())
8075 || matches!(
8076 (params.color.native_cmyk, params.color.process_cmyk),
8077 (Some(nat), Some(proc_))
8078 if (nat.0 - proc_.0).abs() > 1e-6
8079 || (nat.1 - proc_.1).abs() > 1e-6
8080 || (nat.2 - proc_.2).abs() > 1e-6
8081 || (nat.3 - proc_.3).abs() > 1e-6
8082 );
8083
8084 let (src_c, src_m, src_y, src_k) = if is_custom_spot {
8085 (0.0, 0.0, 0.0, 0.0)
8086 } else if let Some(c) = params.color.process_cmyk {
8087 c
8088 } else if let Some(c) = params.color.native_cmyk {
8089 c
8090 } else if let Some(cmyk) = icc.and_then(|i| {
8091 i.convert_rgb_to_cmyk_readonly(params.color.r, params.color.g, params.color.b)
8092 }) {
8093 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
8094 } else {
8095 (
8096 (1.0 - params.color.r).clamp(0.0, 1.0),
8097 (1.0 - params.color.g).clamp(0.0, 1.0),
8098 (1.0 - params.color.b).clamp(0.0, 1.0),
8099 0.0,
8100 )
8101 };
8102
8103 let Some(skia_path) = build_skia_path(path) else {
8104 return;
8105 };
8106
8107 let resolution_scale = (transform.sx * transform.sx + transform.sy * transform.sy)
8111 .sqrt()
8112 .max(1.0);
8113 let dashed_op;
8114 let stroke_src = if let Some(ref dash) = stroke.dash {
8115 dashed_op = skia_path.dash(dash, resolution_scale);
8116 match dashed_op.as_ref() {
8117 Some(p) => p,
8118 None => &skia_path,
8119 }
8120 } else {
8121 &skia_path
8122 };
8123 let Some(stroked_user) = stroke_src.stroke(stroke, resolution_scale) else {
8124 return;
8125 };
8126 let Some(stroked) = stroked_user.transform(transform) else {
8127 return;
8128 };
8129
8130 let mut coverage_mask = match Mask::new(out_w, out_h) {
8131 Some(m) => m,
8132 None => return,
8133 };
8134 coverage_mask.fill_path(
8135 &stroked,
8136 SkiaFillRule::Winding,
8137 !no_aa,
8138 Transform::identity(),
8139 );
8140
8141 let cov_data = coverage_mask.data();
8142 let clip_data: Option<&[u8]> = match clip_region {
8143 Some(ClipRegion::Mask(m)) => Some(m.data()),
8144 _ => None,
8145 };
8146
8147 let (mut bx0, mut by0, mut bx1, mut by1) =
8148 path_device_bbox(&stroked, Transform::identity(), out_w, out_h);
8149 if let Some(ClipRegion::Rect(r)) = clip_region {
8150 bx0 = bx0.max(r.x0 as usize);
8151 by0 = by0.max(r.y0 as usize);
8152 bx1 = bx1.min(r.x1 as usize);
8153 by1 = by1.min(r.y1 as usize);
8154 }
8155
8156 let stride = out_w as usize;
8157 for y in by0..by1 {
8158 for x in bx0..bx1 {
8159 let mi = y * stride + x;
8160 let mut cov = cov_data[mi] as f32 / 255.0;
8161 if let Some(clip) = clip_data {
8162 cov *= clip[mi] as f32 / 255.0;
8163 }
8164 if cov > 0.0 {
8165 let ci = mi * 4;
8166 cmyk_buf[ci] = src_c as f32;
8167 cmyk_buf[ci + 1] = src_m as f32;
8168 cmyk_buf[ci + 2] = src_y as f32;
8169 cmyk_buf[ci + 3] = src_k as f32;
8170 if has_spot_contrib {
8171 spot_mask[mi] = 1;
8172 }
8173 }
8174 }
8175 }
8176}
8177
8178#[allow(clippy::too_many_arguments)]
8180fn render_overprint_image(
8181 pixmap: &mut Pixmap,
8182 cmyk_buf: &mut [f32],
8183 op_bg: &mut [u8],
8184 op_touched: &mut [u8],
8185 band_state: &mut BandState,
8186 sample_data: &[u8],
8187 params: &ImageParams,
8188 vp_x: f32,
8189 vp_y: f32,
8190 scale_x: f32,
8191 scale_y: f32,
8192 out_w: u32,
8193 out_h: u32,
8194 icc: Option<&IccCache>,
8195) {
8196 let iw = params.width as usize;
8197 let ih = params.height as usize;
8198 let Some(image_inv) = params.image_matrix.invert() else {
8199 return;
8200 };
8201 let combined = params.ctm.concat(&image_inv);
8202 let Some(inv_combined) = combined.invert() else {
8203 return;
8204 };
8205
8206 let px_data = pixmap.data_mut();
8207 let stride = out_w as usize;
8208 let inv_sx = 1.0 / scale_x as f64;
8209 let inv_sy = 1.0 / scale_y as f64;
8210
8211 let clip_data: Option<&[u8]> = match &band_state.clip_region {
8212 Some(ClipRegion::Mask(m)) => Some(m.data()),
8213 _ => None,
8214 };
8215 let clip_rect = match &band_state.clip_region {
8216 Some(ClipRegion::Rect(r)) => Some(*r),
8217 _ => None,
8218 };
8219
8220 let mask_info = if let ImageColorSpace::Mask { color, polarity } = ¶ms.color_space {
8221 let (src_c, src_m, src_y, src_k) = color.native_cmyk.unwrap_or_else(|| {
8222 let r = color.r;
8223 let g = color.g;
8224 let b = color.b;
8225 (1.0 - r, 1.0 - g, 1.0 - b, 0.0)
8226 });
8227 Some((src_c, src_m, src_y, src_k, *polarity, iw.div_ceil(8)))
8228 } else {
8229 None
8230 };
8231
8232 for by in 0..out_h as usize {
8233 for bx in 0..out_w as usize {
8234 if let Some(ref r) = clip_rect
8235 && ((by as u32) < r.y0
8236 || (by as u32) >= r.y1
8237 || (bx as u32) < r.x0
8238 || (bx as u32) >= r.x1)
8239 {
8240 continue;
8241 }
8242 if let Some(clip) = clip_data {
8243 let ci_clip = by * stride + bx;
8244 if clip[ci_clip] == 0 {
8245 let bh = out_h as usize;
8246 let has_neighbor = (bx > 0 && clip[ci_clip - 1] != 0)
8247 || (bx + 1 < stride && clip[ci_clip + 1] != 0)
8248 || (by > 0 && clip[ci_clip - stride] != 0)
8249 || (by + 1 < bh && clip[ci_clip + stride] != 0)
8250 || (bx > 0 && by > 0 && clip[ci_clip - stride - 1] != 0)
8251 || (bx + 1 < stride && by > 0 && clip[ci_clip - stride + 1] != 0)
8252 || (bx > 0 && by + 1 < bh && clip[ci_clip + stride - 1] != 0)
8253 || (bx + 1 < stride && by + 1 < bh && clip[ci_clip + stride + 1] != 0);
8254 if !has_neighbor {
8255 continue;
8256 }
8257 }
8258 }
8259
8260 let dx = (bx as f64 + 0.5) * inv_sx + vp_x as f64;
8262 let dy = (by as f64 + 0.5) * inv_sy + vp_y as f64;
8263 let ix = inv_combined.a * dx + inv_combined.c * dy + inv_combined.tx;
8264 let iy = inv_combined.b * dx + inv_combined.d * dy + inv_combined.ty;
8265
8266 let col = ix.floor() as i64;
8267 let row = iy.floor() as i64;
8268 if col < 0 || col >= iw as i64 || row < 0 || row >= ih as i64 {
8269 continue;
8270 }
8271 let col = col as usize;
8272 let row = row as usize;
8273
8274 let (src_c, src_m, src_y, src_k) =
8275 if let Some((mc, mm, my, mk, polarity, bytes_per_row)) = mask_info {
8276 let byte_idx = row * bytes_per_row + col / 8;
8277 let bit_offset = 7 - (col % 8);
8278 let bit = if byte_idx < sample_data.len() {
8279 (sample_data[byte_idx] >> bit_offset) & 1
8280 } else {
8281 0
8282 };
8283 let paint = if polarity { bit == 1 } else { bit == 0 };
8284 if !paint {
8285 continue;
8286 }
8287 (mc, mm, my, mk)
8288 } else if let Some(cmyk) =
8289 sample_pixel_cmyk(sample_data, ¶ms.color_space, iw, row, col)
8290 {
8291 cmyk
8292 } else {
8293 continue;
8294 };
8295
8296 let mi = by * stride + bx;
8297 let ci = mi * 4;
8298 let pi = mi * 4;
8299
8300 if image_cs_has_spot_tint_transform(¶ms.color_space) {
8319 let cur_c = cmyk_buf[ci] as f64;
8320 let cur_m = cmyk_buf[ci + 1] as f64;
8321 let cur_y = cmyk_buf[ci + 2] as f64;
8322 let cur_k = cmyk_buf[ci + 3] as f64;
8323 let cur_is_zero = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
8324 let named = params.painted_channels;
8325 let opm1 = params.overprint_mode == 1;
8332 let (new_c, new_m, new_y, new_k) = if cur_is_zero {
8333 (src_c, src_m, src_y, src_k)
8334 } else {
8335 let nc =
8336 if named & stet_graphics::device::CMYK_C != 0 && !(opm1 && src_c == 0.0) {
8337 src_c
8338 } else {
8339 cur_c
8340 };
8341 let nm =
8342 if named & stet_graphics::device::CMYK_M != 0 && !(opm1 && src_m == 0.0) {
8343 src_m
8344 } else {
8345 cur_m
8346 };
8347 let ny =
8348 if named & stet_graphics::device::CMYK_Y != 0 && !(opm1 && src_y == 0.0) {
8349 src_y
8350 } else {
8351 cur_y
8352 };
8353 let nk =
8354 if named & stet_graphics::device::CMYK_K != 0 && !(opm1 && src_k == 0.0) {
8355 src_k
8356 } else {
8357 cur_k
8358 };
8359 (nc, nm, ny, nk)
8360 };
8361 cmyk_buf[ci] = new_c as f32;
8362 cmyk_buf[ci + 1] = new_m as f32;
8363 cmyk_buf[ci + 2] = new_y as f32;
8364 cmyk_buf[ci + 3] = new_k as f32;
8365 let (r, g, b) = if let Some(icc_cache) = icc {
8366 icc_cache
8367 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
8368 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
8369 } else {
8370 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
8371 };
8372 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
8373 op_bg[pi] = px_data[pi];
8374 op_bg[pi + 1] = px_data[pi + 1];
8375 op_bg[pi + 2] = px_data[pi + 2];
8376 op_bg[pi + 3] = px_data[pi + 3];
8377 op_touched[mi] = 1;
8378 }
8379 px_data[pi] = (r * 255.0).round() as u8;
8380 px_data[pi + 1] = (g * 255.0).round() as u8;
8381 px_data[pi + 2] = (b * 255.0).round() as u8;
8382 px_data[pi + 3] = 255;
8383 continue;
8384 }
8385
8386 let mut channels = params.painted_channels;
8387 if channels == 0 {
8390 channels = stet_graphics::device::CMYK_ALL;
8391 }
8392 let is_direct_cmyk = matches!(
8393 ¶ms.color_space,
8394 ImageColorSpace::DeviceCMYK
8395 | ImageColorSpace::ICCBased { n: 4, .. }
8396 | ImageColorSpace::Mask { .. }
8397 );
8398 let is_custom_spot = params.painted_channels == 0
8408 && !is_cmyk_color_space(¶ms.color_space)
8409 && match ¶ms.color_space {
8410 ImageColorSpace::Mask { color, .. } => color.native_cmyk.is_some(),
8411 _ => true,
8412 };
8413 if params.overprint_mode == 1
8414 && channels == stet_graphics::device::CMYK_ALL
8415 && is_direct_cmyk
8416 {
8417 channels = 0;
8418 if src_c != 0.0 {
8419 channels |= stet_graphics::device::CMYK_C;
8420 }
8421 if src_m != 0.0 {
8422 channels |= stet_graphics::device::CMYK_M;
8423 }
8424 if src_y != 0.0 {
8425 channels |= stet_graphics::device::CMYK_Y;
8426 }
8427 if src_k != 0.0 {
8428 channels |= stet_graphics::device::CMYK_K;
8429 }
8430 }
8431
8432 let cur_c = cmyk_buf[ci] as f64;
8433 let cur_m = cmyk_buf[ci + 1] as f64;
8434 let cur_y = cmyk_buf[ci + 2] as f64;
8435 let cur_k = cmyk_buf[ci + 3] as f64;
8436 let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
8437 let pixmap_has_colour = px_data[pi + 3] > 0
8438 && (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
8439 let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
8447
8448 let new_c = if channels & stet_graphics::device::CMYK_C != 0 {
8449 src_c
8450 } else {
8451 cur_c
8452 };
8453 let new_m = if channels & stet_graphics::device::CMYK_M != 0 {
8454 src_m
8455 } else {
8456 cur_m
8457 };
8458 let new_y = if channels & stet_graphics::device::CMYK_Y != 0 {
8459 src_y
8460 } else {
8461 cur_y
8462 };
8463 let new_k = if channels & stet_graphics::device::CMYK_K != 0 {
8464 src_k
8465 } else {
8466 cur_k
8467 };
8468
8469 if !is_custom_spot {
8470 cmyk_buf[ci] = new_c as f32;
8471 cmyk_buf[ci + 1] = new_m as f32;
8472 cmyk_buf[ci + 2] = new_y as f32;
8473 cmyk_buf[ci + 3] = new_k as f32;
8474 }
8475
8476 let (r, g, b) = if use_multiplicative {
8477 let bg_r = px_data[pi] as f64 / 255.0;
8478 let bg_g = px_data[pi + 1] as f64 / 255.0;
8479 let bg_b = px_data[pi + 2] as f64 / 255.0;
8480 let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
8481 1.0 - src_c
8482 } else {
8483 1.0
8484 };
8485 let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
8486 1.0 - src_m
8487 } else {
8488 1.0
8489 };
8490 let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
8491 1.0 - src_y
8492 } else {
8493 1.0
8494 };
8495 let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
8496 1.0 - src_k
8497 } else {
8498 1.0
8499 };
8500 (
8501 (bg_r * over_r * k_fac).clamp(0.0, 1.0),
8502 (bg_g * over_g * k_fac).clamp(0.0, 1.0),
8503 (bg_b * over_b * k_fac).clamp(0.0, 1.0),
8504 )
8505 } else if let Some(icc_cache) = icc {
8506 icc_cache
8507 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
8508 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
8509 } else {
8510 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
8511 };
8512
8513 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
8516 op_bg[pi] = px_data[pi];
8517 op_bg[pi + 1] = px_data[pi + 1];
8518 op_bg[pi + 2] = px_data[pi + 2];
8519 op_bg[pi + 3] = px_data[pi + 3];
8520 op_touched[mi] = 1;
8521 }
8522
8523 px_data[pi] = (r * 255.0).round() as u8;
8524 px_data[pi + 1] = (g * 255.0).round() as u8;
8525 px_data[pi + 2] = (b * 255.0).round() as u8;
8526 px_data[pi + 3] = 255;
8527 }
8528 }
8529}
8530
8531#[allow(clippy::too_many_arguments)]
8541fn update_cmyk_buffer_for_image(
8542 cmyk_buf: &mut [f32],
8543 sample_data: &[u8],
8544 pixmap_rgba: &[u8],
8545 params: &ImageParams,
8546 vp_x: f32,
8547 vp_y: f32,
8548 scale_x: f32,
8549 scale_y: f32,
8550 out_w: u32,
8551 out_h: u32,
8552 clip_region: &Option<ClipRegion>,
8553 icc: Option<&IccCache>,
8554) {
8555 let iw = params.width as usize;
8556 let ih = params.height as usize;
8557 let Some(image_inv) = params.image_matrix.invert() else {
8558 return;
8559 };
8560 let combined = params.ctm.concat(&image_inv);
8561 let Some(inv_combined) = combined.invert() else {
8562 return;
8563 };
8564 let stride = out_w as usize;
8565 let inv_sx = 1.0 / scale_x as f64;
8566 let inv_sy = 1.0 / scale_y as f64;
8567
8568 let mask_info = if let ImageColorSpace::Mask { color, polarity } = ¶ms.color_space {
8569 let Some((c, m, y, k)) = color.native_cmyk else {
8570 return;
8571 };
8572 Some((
8573 c as f32,
8574 m as f32,
8575 y as f32,
8576 k as f32,
8577 *polarity,
8578 iw.div_ceil(8),
8579 ))
8580 } else {
8581 None
8582 };
8583
8584 let clip_data: Option<&[u8]> = match clip_region {
8585 Some(ClipRegion::Mask(m)) => Some(m.data()),
8586 _ => None,
8587 };
8588 let clip_rect = match clip_region {
8589 Some(ClipRegion::Rect(r)) => Some(*r),
8590 _ => None,
8591 };
8592
8593 for by in 0..out_h as usize {
8594 for bx in 0..out_w as usize {
8595 if let Some(ref r) = clip_rect
8596 && ((by as u32) < r.y0
8597 || (by as u32) >= r.y1
8598 || (bx as u32) < r.x0
8599 || (bx as u32) >= r.x1)
8600 {
8601 continue;
8602 }
8603 if let Some(clip) = clip_data
8604 && clip[by * stride + bx] == 0
8605 {
8606 continue;
8607 }
8608
8609 let dx = (bx as f64 + 0.5) * inv_sx + vp_x as f64;
8610 let dy = (by as f64 + 0.5) * inv_sy + vp_y as f64;
8611 let ix = inv_combined.a * dx + inv_combined.c * dy + inv_combined.tx;
8612 let iy = inv_combined.b * dx + inv_combined.d * dy + inv_combined.ty;
8613
8614 let col = ix.floor() as i64;
8615 let row = iy.floor() as i64;
8616 if col < 0 || col >= iw as i64 || row < 0 || row >= ih as i64 {
8617 continue;
8618 }
8619 let col = col as usize;
8620 let row = row as usize;
8621
8622 let ci = (by * stride + bx) * 4;
8623 if let Some((sc, sm, sy, sk, polarity, bytes_per_row)) = mask_info {
8624 let byte_idx = row * bytes_per_row + col / 8;
8625 let bit_offset = 7 - (col % 8);
8626 let bit = if byte_idx < sample_data.len() {
8627 (sample_data[byte_idx] >> bit_offset) & 1
8628 } else {
8629 0
8630 };
8631 let paint = if polarity { bit == 1 } else { bit == 0 };
8632 if paint {
8633 cmyk_buf[ci] = sc;
8634 cmyk_buf[ci + 1] = sm;
8635 cmyk_buf[ci + 2] = sy;
8636 cmyk_buf[ci + 3] = sk;
8637 }
8638 } else if let Some((sc, sm, sy, sk)) =
8639 sample_pixel_cmyk(sample_data, ¶ms.color_space, iw, row, col)
8640 {
8641 cmyk_buf[ci] = sc as f32;
8642 cmyk_buf[ci + 1] = sm as f32;
8643 cmyk_buf[ci + 2] = sy as f32;
8644 cmyk_buf[ci + 3] = sk as f32;
8645 } else if ci + 3 < pixmap_rgba.len() && pixmap_rgba[ci + 3] > 0 {
8646 let r = pixmap_rgba[ci] as f64 / 255.0;
8650 let g = pixmap_rgba[ci + 1] as f64 / 255.0;
8651 let b = pixmap_rgba[ci + 2] as f64 / 255.0;
8652 let cmyk =
8653 if let Some(c) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(r, g, b)) {
8654 c
8655 } else {
8656 [
8657 (1.0 - r).clamp(0.0, 1.0),
8658 (1.0 - g).clamp(0.0, 1.0),
8659 (1.0 - b).clamp(0.0, 1.0),
8660 0.0,
8661 ]
8662 };
8663 cmyk_buf[ci] = cmyk[0] as f32;
8664 cmyk_buf[ci + 1] = cmyk[1] as f32;
8665 cmyk_buf[ci + 2] = cmyk[2] as f32;
8666 cmyk_buf[ci + 3] = cmyk[3] as f32;
8667 }
8668 }
8669 }
8670}
8671fn image_supports_overprint(cs: &ImageColorSpace) -> bool {
8675 match cs {
8676 ImageColorSpace::Mask { .. } => true,
8677 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. } => true,
8678 ImageColorSpace::Separation { alt_space, .. }
8679 | ImageColorSpace::DeviceN { alt_space, .. } => {
8680 matches!(
8681 alt_space.as_ref(),
8682 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8683 )
8684 }
8685 ImageColorSpace::Indexed { base, .. } => image_supports_overprint(base),
8686 _ => false,
8687 }
8688}
8689
8690fn is_cmyk_color_space(cs: &ImageColorSpace) -> bool {
8692 match cs {
8693 ImageColorSpace::DeviceCMYK => true,
8694 ImageColorSpace::ICCBased { n: 4, .. } => true,
8695 ImageColorSpace::Indexed { base, .. } => is_cmyk_color_space(base),
8696 _ => false,
8697 }
8698}
8699
8700fn image_cs_has_spot_tint_transform(cs: &ImageColorSpace) -> bool {
8708 use stet_graphics::device::cmyk_channel_for_name;
8709 match cs {
8710 ImageColorSpace::Separation {
8711 name, alt_space, ..
8712 } => {
8713 cmyk_channel_for_name(name) == 0
8714 && matches!(
8715 alt_space.as_ref(),
8716 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8717 )
8718 }
8719 ImageColorSpace::DeviceN {
8720 names, alt_space, ..
8721 } => {
8722 matches!(
8723 alt_space.as_ref(),
8724 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8725 ) && names.iter().any(|n| cmyk_channel_for_name(n) == 0)
8726 }
8727 ImageColorSpace::Indexed { base, .. } => image_cs_has_spot_tint_transform(base),
8728 _ => false,
8729 }
8730}
8731
8732fn sample_pixel_cmyk(
8736 sample_data: &[u8],
8737 cs: &ImageColorSpace,
8738 iw: usize,
8739 row: usize,
8740 col: usize,
8741) -> Option<(f64, f64, f64, f64)> {
8742 match cs {
8743 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. } => {
8744 let si = (row * iw + col) * 4;
8745 if si + 3 < sample_data.len() {
8746 Some((
8747 sample_data[si] as f64 / 255.0,
8748 sample_data[si + 1] as f64 / 255.0,
8749 sample_data[si + 2] as f64 / 255.0,
8750 sample_data[si + 3] as f64 / 255.0,
8751 ))
8752 } else {
8753 None
8754 }
8755 }
8756 ImageColorSpace::Separation {
8757 alt_space,
8758 tint_table,
8759 ..
8760 } => {
8761 if !matches!(
8762 alt_space.as_ref(),
8763 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8764 ) {
8765 return None;
8766 }
8767 let si = row * iw + col;
8768 if si >= sample_data.len() {
8769 return None;
8770 }
8771 let tint = sample_data[si] as f32 / 255.0;
8772 let mut alt = [0.0f32; 4];
8773 tint_table.lookup_1d(tint, &mut alt);
8774 Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64))
8775 }
8776 ImageColorSpace::DeviceN {
8777 alt_space,
8778 tint_table,
8779 ..
8780 } => {
8781 if !matches!(
8782 alt_space.as_ref(),
8783 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8784 ) {
8785 return None;
8786 }
8787 let ni = tint_table.num_inputs as usize;
8788 let si = (row * iw + col) * ni;
8789 if si + ni > sample_data.len() {
8790 return None;
8791 }
8792 let mut inputs = vec![0.0f32; ni];
8793 for (c, inp) in inputs.iter_mut().enumerate() {
8794 *inp = sample_data[si + c] as f32 / 255.0;
8795 }
8796 let mut alt = [0.0f32; 4];
8797 tint_table.lookup_nd(&inputs, &mut alt);
8798 Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64))
8799 }
8800 ImageColorSpace::Indexed {
8801 base,
8802 hival,
8803 lookup,
8804 } => {
8805 let pi = row * iw + col;
8806 if pi >= sample_data.len() {
8807 return None;
8808 }
8809 let idx = sample_data[pi] as usize;
8810 let idx = idx.min(*hival as usize);
8811 let base_ncomp = base.num_components() as usize;
8812 let li = idx * base_ncomp;
8813 if is_cmyk_color_space(base) && base_ncomp == 4 && li + 3 < lookup.len() {
8815 return Some((
8816 lookup[li] as f64 / 255.0,
8817 lookup[li + 1] as f64 / 255.0,
8818 lookup[li + 2] as f64 / 255.0,
8819 lookup[li + 3] as f64 / 255.0,
8820 ));
8821 }
8822 if li + base_ncomp <= lookup.len() {
8824 match base.as_ref() {
8825 ImageColorSpace::Separation {
8826 alt_space,
8827 tint_table,
8828 ..
8829 } if matches!(
8830 alt_space.as_ref(),
8831 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8832 ) =>
8833 {
8834 let tint = lookup[li] as f32 / 255.0;
8835 let mut alt = [0.0f32; 4];
8836 tint_table.lookup_1d(tint, &mut alt);
8837 return Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64));
8838 }
8839 ImageColorSpace::DeviceN {
8840 alt_space,
8841 tint_table,
8842 ..
8843 } if matches!(
8844 alt_space.as_ref(),
8845 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8846 ) =>
8847 {
8848 let ni = tint_table.num_inputs as usize;
8849 let mut inputs = vec![0.0f32; ni];
8850 for (c, inp) in inputs.iter_mut().enumerate() {
8851 if c < base_ncomp {
8852 *inp = lookup[li + c] as f32 / 255.0;
8853 }
8854 }
8855 let mut alt = [0.0f32; 4];
8856 tint_table.lookup_nd(&inputs, &mut alt);
8857 return Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64));
8858 }
8859 _ => {}
8860 }
8861 }
8862 None
8863 }
8864 _ => None,
8865 }
8866}
8867#[allow(clippy::too_many_arguments)]
8873fn render_banded_to_sink(
8874 page_w: u32,
8875 page_h: u32,
8876 band_h: u32,
8877 dpi: f64,
8878 list: &DisplayList,
8879 sink: &mut dyn stet_graphics::device::PageSink,
8880 icc_cache: &IccCache,
8881 no_aa: bool,
8882 layer_set: &LayerSet,
8883) -> Result<(), String> {
8884 let bboxes = precompute_bboxes(list, dpi);
8886
8887 let epochs = build_clip_epochs(list, &bboxes);
8891
8892 let clip_seen = precompute_clip_seen(list);
8894
8895 use stet_graphics::display_list::GroupColorSpace;
8900 let needs_cmyk_buffer = has_overprint_elements(list)
8901 || list.page_group_color_space() == GroupColorSpace::DeviceCMYK
8902 || has_cmyk_group(list);
8903
8904 let preprocessed_images = preprocess_images_for_bands(list, Some(icc_cache));
8906
8907 const BAND_OVERLAP: u32 = 6;
8911
8912 let render_h = band_h + 2 * BAND_OVERLAP;
8913
8914 sink.begin_page(page_w, page_h)?;
8916
8917 let num_bands = page_h.div_ceil(band_h);
8918 let elements = list.elements();
8919 let row_bytes = page_w as usize * 4;
8920 let icc_ref = Some(icc_cache);
8921
8922 let render_band = |band_idx: u32| -> Vec<u8> {
8924 let y_start = band_idx * band_h;
8925 let actual_h = (page_h - y_start).min(band_h);
8926
8927 let render_y_start = y_start.saturating_sub(BAND_OVERLAP);
8928 let render_y_end_f = ((y_start + actual_h + BAND_OVERLAP).min(page_h)) as f64;
8929 let band_offset = y_start - render_y_start;
8930
8931 let mut band_pixmap = Pixmap::new(page_w, render_h).expect("Failed to create band pixmap");
8932 band_pixmap.as_mut().data_mut().fill(0x00);
8934
8935 let cmyk_buf = if needs_cmyk_buffer {
8936 Some(vec![0.0f32; page_w as usize * render_h as usize * 4])
8938 } else {
8939 None
8940 };
8941
8942 let mut band_state = BandState {
8943 clip_region: None,
8944 spare_mask: None,
8945 clip_mask_cache: HashMap::new(),
8946 clip_mask_seen: clip_seen.clone(),
8947 mask_pool: Vec::new(),
8948 cmyk_buffer: cmyk_buf,
8949 op_bg_snapshot: None,
8950 op_touched: None,
8951 spot_mask: None,
8952 };
8953
8954 for epoch in &epochs {
8956 if !epoch.has_erase_page {
8957 match epoch.paint_bbox {
8958 Some(ref pb)
8959 if pb.y_max <= render_y_start as f64 || pb.y_min >= render_y_end_f =>
8960 {
8961 continue;
8962 }
8963 None => continue,
8964 _ => {}
8965 }
8966 }
8967
8968 for i in epoch.start_idx..epoch.end_idx {
8969 let force_process = matches!(
8975 &elements[i],
8976 DisplayElement::OcgGroup { elements: inner, .. }
8977 if contains_clip_op(inner)
8978 );
8979 if !force_process
8980 && let Some(ref bbox) = bboxes[i]
8981 && (bbox.y_max <= render_y_start as f64 || bbox.y_min >= render_y_end_f)
8982 {
8983 continue;
8984 }
8985 let ctx = RenderContext {
8986 vp_x: 0.0,
8987 vp_y: render_y_start as f32,
8988 scale_x: 1.0,
8989 scale_y: 1.0,
8990 out_w: page_w,
8991 out_h: render_h,
8992 effective_dpi: dpi,
8993 icc: icc_ref,
8994 image_cache: None,
8995 preprocessed: Some(&preprocessed_images),
8996 elem_idx: i,
8997 no_aa,
8998 opm_zero_transparent: false,
8999 knockout_painter_pass: KnockoutPainterPass::None,
9000 parent_group_isolated: false,
9001 alpha_extraction_pass: false,
9002 layer_set,
9003 };
9004 render_element(&mut band_pixmap, &mut band_state, &elements[i], &ctx);
9005 }
9006 }
9007
9008 composite_onto_white(band_pixmap.data_mut());
9010
9011 let start_byte = band_offset as usize * row_bytes;
9013 let total_bytes = actual_h as usize * row_bytes;
9014 band_pixmap.data()[start_byte..start_byte + total_bytes].to_vec()
9015 };
9016
9017 #[cfg(feature = "parallel")]
9019 {
9020 let chunk_size = rayon::current_num_threads().max(1);
9025
9026 for chunk_start in (0..num_bands).step_by(chunk_size) {
9027 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
9028
9029 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
9030 .into_par_iter()
9031 .map(&render_band)
9032 .collect();
9033
9034 for (i, band_data) in rendered.iter().enumerate() {
9035 let band_idx = chunk_start + i as u32;
9036 let y_start = band_idx * band_h;
9037 let actual_h = (page_h - y_start).min(band_h);
9038 sink.write_rows(band_data, actual_h)?;
9039 }
9040 }
9041 }
9042 #[cfg(not(feature = "parallel"))]
9043 {
9044 for band_idx in 0..num_bands {
9046 let band_data = render_band(band_idx);
9047 let y_start = band_idx * band_h;
9048 let actual_h = (page_h - y_start).min(band_h);
9049 sink.write_rows(&band_data, actual_h)?;
9050 }
9051 }
9052
9053 sink.end_page()
9054}
9055
9056#[derive(Clone, Copy)]
9058struct BBox2D {
9059 x_min: f64,
9060 y_min: f64,
9061 x_max: f64,
9062 y_max: f64,
9063}
9064
9065fn precompute_full_bboxes(list: &DisplayList, dpi: f64) -> Vec<Option<BBox2D>> {
9067 list.elements()
9068 .iter()
9069 .map(|elem| match elem {
9070 DisplayElement::Fill { path, params } => fill_device_full_bbox(path, ¶ms.ctm),
9071 DisplayElement::Stroke { path, params } => {
9072 path_full_bbox(path).map(|mut bbox| {
9073 let effective_lw = params.line_width.max(hairline_min_width(¶ms.ctm, dpi));
9075 let expand = effective_lw * params.miter_limit * 0.5;
9076 let m = ¶ms.ctm;
9077 let is_identity = m.a == 1.0
9078 && m.b == 0.0
9079 && m.c == 0.0
9080 && m.d == 1.0
9081 && m.tx == 0.0
9082 && m.ty == 0.0;
9083 if is_identity {
9084 bbox.x_min -= expand;
9085 bbox.x_max += expand;
9086 bbox.y_min -= expand;
9087 bbox.y_max += expand;
9088 } else {
9089 let col_x_len = (m.a * m.a + m.b * m.b).sqrt().max(1.0);
9092 let col_y_len = (m.c * m.c + m.d * m.d).sqrt().max(1.0);
9093 let expand_x = effective_lw * col_x_len * params.miter_limit * 0.5;
9094 let expand_y = effective_lw * col_y_len * params.miter_limit * 0.5;
9095 bbox.x_min -= expand_x;
9096 bbox.x_max += expand_x;
9097 bbox.y_min -= expand_y;
9098 bbox.y_max += expand_y;
9099 let corners = [
9101 (
9102 m.a * bbox.x_min + m.c * bbox.y_min + m.tx,
9103 m.b * bbox.x_min + m.d * bbox.y_min + m.ty,
9104 ),
9105 (
9106 m.a * bbox.x_max + m.c * bbox.y_min + m.tx,
9107 m.b * bbox.x_max + m.d * bbox.y_min + m.ty,
9108 ),
9109 (
9110 m.a * bbox.x_min + m.c * bbox.y_max + m.tx,
9111 m.b * bbox.x_min + m.d * bbox.y_max + m.ty,
9112 ),
9113 (
9114 m.a * bbox.x_max + m.c * bbox.y_max + m.tx,
9115 m.b * bbox.x_max + m.d * bbox.y_max + m.ty,
9116 ),
9117 ];
9118 bbox.x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
9119 bbox.x_max = corners
9120 .iter()
9121 .map(|c| c.0)
9122 .fold(f64::NEG_INFINITY, f64::max);
9123 bbox.y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
9124 bbox.y_max = corners
9125 .iter()
9126 .map(|c| c.1)
9127 .fold(f64::NEG_INFINITY, f64::max);
9128 }
9129 bbox
9130 })
9131 }
9132 DisplayElement::Image { params, .. } => image_full_bbox(params),
9133 DisplayElement::AxialShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9134 DisplayElement::RadialShading { params } => {
9135 shading_full_bbox(¶ms.bbox, ¶ms.ctm)
9136 }
9137 DisplayElement::MeshShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9138 DisplayElement::PatchShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9139 DisplayElement::PatternFill { params } => pattern_fill_full_bbox(params),
9140 DisplayElement::Group { params, .. } => Some(BBox2D {
9141 x_min: params.bbox[0],
9142 y_min: params.bbox[1],
9143 x_max: params.bbox[2],
9144 y_max: params.bbox[3],
9145 }),
9146 DisplayElement::SoftMasked { params, .. } => Some(BBox2D {
9147 x_min: params.bbox[0],
9148 y_min: params.bbox[1],
9149 x_max: params.bbox[2],
9150 y_max: params.bbox[3],
9151 }),
9152 DisplayElement::OcgGroup {
9153 elements,
9154 visibility,
9155 } => {
9156 if !visibility.default_visible() && !contains_clip_op(elements) {
9161 return None;
9162 }
9163 let child_bboxes = precompute_full_bboxes(elements, dpi);
9164 let mut x_min = f64::INFINITY;
9165 let mut y_min = f64::INFINITY;
9166 let mut x_max = f64::NEG_INFINITY;
9167 let mut y_max = f64::NEG_INFINITY;
9168 for cb in child_bboxes.into_iter().flatten() {
9169 x_min = x_min.min(cb.x_min);
9170 y_min = y_min.min(cb.y_min);
9171 x_max = x_max.max(cb.x_max);
9172 y_max = y_max.max(cb.y_max);
9173 }
9174 if x_min <= x_max && y_min <= y_max {
9175 Some(BBox2D {
9176 x_min,
9177 y_min,
9178 x_max,
9179 y_max,
9180 })
9181 } else {
9182 None
9183 }
9184 }
9185 _ => None, })
9187 .collect()
9188}
9189
9190fn clip_path_bbox(path: &PsPath, params: &ClipParams) -> Option<BBox2D> {
9199 let mut bbox = path_full_bbox(path)?;
9200 let ctm = ¶ms.ctm;
9201 let is_identity = ctm.a == 1.0
9202 && ctm.b == 0.0
9203 && ctm.c == 0.0
9204 && ctm.d == 1.0
9205 && ctm.tx == 0.0
9206 && ctm.ty == 0.0;
9207 if !is_identity {
9208 let corners = [
9209 ctm.transform_point(bbox.x_min, bbox.y_min),
9210 ctm.transform_point(bbox.x_max, bbox.y_min),
9211 ctm.transform_point(bbox.x_min, bbox.y_max),
9212 ctm.transform_point(bbox.x_max, bbox.y_max),
9213 ];
9214 bbox.x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
9215 bbox.x_max = corners
9216 .iter()
9217 .map(|c| c.0)
9218 .fold(f64::NEG_INFINITY, f64::max);
9219 bbox.y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
9220 bbox.y_max = corners
9221 .iter()
9222 .map(|c| c.1)
9223 .fold(f64::NEG_INFINITY, f64::max);
9224 }
9225 if let Some(sp) = ¶ms.stroke_params {
9226 let scale = (ctm.a * ctm.a + ctm.b * ctm.b)
9227 .sqrt()
9228 .max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt())
9229 .max(1.0);
9230 let expand = sp.line_width * 0.5 * scale;
9231 bbox.x_min -= expand;
9232 bbox.x_max += expand;
9233 bbox.y_min -= expand;
9234 bbox.y_max += expand;
9235 }
9236 Some(bbox)
9237}
9238
9239fn intersect_bbox(a: &BBox2D, b: &BBox2D) -> Option<BBox2D> {
9241 let x_min = a.x_min.max(b.x_min);
9242 let y_min = a.y_min.max(b.y_min);
9243 let x_max = a.x_max.min(b.x_max);
9244 let y_max = a.y_max.min(b.y_max);
9245 if x_min < x_max && y_min < y_max {
9246 Some(BBox2D {
9247 x_min,
9248 y_min,
9249 x_max,
9250 y_max,
9251 })
9252 } else {
9253 None
9254 }
9255}
9256
9257fn compute_paint_bounds(list: &DisplayList, _dpi: f64) -> Option<BBox2D> {
9278 let mut clip_stack: Vec<BBox2D> = Vec::new();
9282 let mut union: Option<BBox2D> = None;
9283
9284 let push_paint = |union: &mut Option<BBox2D>, clip_stack: &[BBox2D], bbox: BBox2D| {
9285 let visible = match clip_stack.last() {
9289 Some(clip) => match intersect_bbox(clip, &bbox) {
9290 Some(b) => b,
9291 None => return,
9292 },
9293 None => bbox,
9294 };
9295 *union = Some(match union.take() {
9296 None => visible,
9297 Some(u) => BBox2D {
9298 x_min: u.x_min.min(visible.x_min),
9299 y_min: u.y_min.min(visible.y_min),
9300 x_max: u.x_max.max(visible.x_max),
9301 y_max: u.y_max.max(visible.y_max),
9302 },
9303 });
9304 };
9305
9306 for elem in list.elements() {
9307 match elem {
9308 DisplayElement::Clip { path, params } => {
9309 if let Some(cb) = clip_path_bbox(path, params) {
9310 let new_top = match clip_stack.last() {
9311 Some(prev) => match intersect_bbox(prev, &cb) {
9312 Some(b) => b,
9313 None => BBox2D {
9317 x_min: 0.0,
9318 y_min: 0.0,
9319 x_max: 0.0,
9320 y_max: 0.0,
9321 },
9322 },
9323 None => cb,
9324 };
9325 clip_stack.push(new_top);
9326 }
9327 }
9328 DisplayElement::InitClip | DisplayElement::ErasePage => {
9329 clip_stack.clear();
9330 }
9331 DisplayElement::Fill { path, .. } => {
9332 if let Some(b) = path_full_bbox(path) {
9333 push_paint(&mut union, &clip_stack, b);
9334 }
9335 }
9336 DisplayElement::Stroke { path, params } => {
9337 if let Some(mut b) = path_full_bbox(path) {
9338 let expand = params.line_width * params.miter_limit * 0.5;
9339 b.x_min -= expand;
9340 b.x_max += expand;
9341 b.y_min -= expand;
9342 b.y_max += expand;
9343 push_paint(&mut union, &clip_stack, b);
9344 }
9345 }
9346 DisplayElement::Image { params, .. } => {
9347 if let Some(b) = image_full_bbox(params) {
9348 push_paint(&mut union, &clip_stack, b);
9349 }
9350 }
9351 DisplayElement::AxialShading { params } => {
9352 let b = match ¶ms.bbox {
9353 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9354 None => clip_stack.last().copied(),
9355 };
9356 if let Some(b) = b {
9357 push_paint(&mut union, &clip_stack, b);
9358 }
9359 }
9360 DisplayElement::RadialShading { params } => {
9361 let b = match ¶ms.bbox {
9362 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9363 None => clip_stack.last().copied(),
9364 };
9365 if let Some(b) = b {
9366 push_paint(&mut union, &clip_stack, b);
9367 }
9368 }
9369 DisplayElement::MeshShading { params } => {
9370 let b = match ¶ms.bbox {
9371 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9372 None => clip_stack.last().copied(),
9373 };
9374 if let Some(b) = b {
9375 push_paint(&mut union, &clip_stack, b);
9376 }
9377 }
9378 DisplayElement::PatchShading { params } => {
9379 let b = match ¶ms.bbox {
9380 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9381 None => clip_stack.last().copied(),
9382 };
9383 if let Some(b) = b {
9384 push_paint(&mut union, &clip_stack, b);
9385 }
9386 }
9387 DisplayElement::PatternFill { params } => {
9388 if let Some(b) = pattern_fill_full_bbox(params) {
9389 push_paint(&mut union, &clip_stack, b);
9390 }
9391 }
9392 DisplayElement::Group { params, .. } => {
9393 push_paint(
9394 &mut union,
9395 &clip_stack,
9396 BBox2D {
9397 x_min: params.bbox[0],
9398 y_min: params.bbox[1],
9399 x_max: params.bbox[2],
9400 y_max: params.bbox[3],
9401 },
9402 );
9403 }
9404 DisplayElement::SoftMasked { params, .. } => {
9405 push_paint(
9406 &mut union,
9407 &clip_stack,
9408 BBox2D {
9409 x_min: params.bbox[0],
9410 y_min: params.bbox[1],
9411 x_max: params.bbox[2],
9412 y_max: params.bbox[3],
9413 },
9414 );
9415 }
9416 DisplayElement::Text { .. } => {} DisplayElement::OcgGroup { .. } => {
9418 }
9423 _ => {}
9424 }
9425 }
9426 union
9427}
9428
9429fn fill_device_full_bbox(path: &PsPath, ctm: &Matrix) -> Option<BBox2D> {
9434 let bbox = path_full_bbox(path)?;
9435 let is_identity = ctm.a == 1.0
9436 && ctm.b == 0.0
9437 && ctm.c == 0.0
9438 && ctm.d == 1.0
9439 && ctm.tx == 0.0
9440 && ctm.ty == 0.0;
9441 if is_identity {
9442 return Some(bbox);
9443 }
9444 let corners = [
9445 (bbox.x_min, bbox.y_min),
9446 (bbox.x_max, bbox.y_min),
9447 (bbox.x_min, bbox.y_max),
9448 (bbox.x_max, bbox.y_max),
9449 ];
9450 let mut x_min = f64::INFINITY;
9451 let mut x_max = f64::NEG_INFINITY;
9452 let mut y_min = f64::INFINITY;
9453 let mut y_max = f64::NEG_INFINITY;
9454 for (x, y) in &corners {
9455 let dx = ctm.a * x + ctm.c * y + ctm.tx;
9456 let dy = ctm.b * x + ctm.d * y + ctm.ty;
9457 x_min = x_min.min(dx);
9458 x_max = x_max.max(dx);
9459 y_min = y_min.min(dy);
9460 y_max = y_max.max(dy);
9461 }
9462 Some(BBox2D {
9463 x_min,
9464 y_min,
9465 x_max,
9466 y_max,
9467 })
9468}
9469
9470fn path_full_bbox(path: &PsPath) -> Option<BBox2D> {
9471 let mut x_min = f64::INFINITY;
9472 let mut x_max = f64::NEG_INFINITY;
9473 let mut y_min = f64::INFINITY;
9474 let mut y_max = f64::NEG_INFINITY;
9475 for seg in &path.segments {
9476 match seg {
9477 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => {
9478 x_min = x_min.min(*x);
9479 x_max = x_max.max(*x);
9480 y_min = y_min.min(*y);
9481 y_max = y_max.max(*y);
9482 }
9483 PathSegment::CurveTo {
9484 x1,
9485 y1,
9486 x2,
9487 y2,
9488 x3,
9489 y3,
9490 } => {
9491 x_min = x_min.min(*x1).min(*x2).min(*x3);
9492 x_max = x_max.max(*x1).max(*x2).max(*x3);
9493 y_min = y_min.min(*y1).min(*y2).min(*y3);
9494 y_max = y_max.max(*y1).max(*y2).max(*y3);
9495 }
9496 PathSegment::ClosePath => {}
9497 }
9498 }
9499 if x_min <= x_max {
9500 Some(BBox2D {
9501 x_min,
9502 y_min,
9503 x_max,
9504 y_max,
9505 })
9506 } else {
9507 None
9508 }
9509}
9510
9511fn pattern_fill_full_bbox(params: &stet_graphics::device::PatternFillParams) -> Option<BBox2D> {
9516 if let Some(ref sp) = params.stroke_params {
9517 let bbox = path_full_bbox(¶ms.path)?;
9518 let ctm = &sp.ctm;
9519 let corners = [
9520 ctm.transform_point(bbox.x_min, bbox.y_min),
9521 ctm.transform_point(bbox.x_max, bbox.y_min),
9522 ctm.transform_point(bbox.x_min, bbox.y_max),
9523 ctm.transform_point(bbox.x_max, bbox.y_max),
9524 ];
9525 let mut dev_bbox = BBox2D {
9526 x_min: f64::INFINITY,
9527 y_min: f64::INFINITY,
9528 x_max: f64::NEG_INFINITY,
9529 y_max: f64::NEG_INFINITY,
9530 };
9531 for (x, y) in &corners {
9532 dev_bbox.x_min = dev_bbox.x_min.min(*x);
9533 dev_bbox.y_min = dev_bbox.y_min.min(*y);
9534 dev_bbox.x_max = dev_bbox.x_max.max(*x);
9535 dev_bbox.y_max = dev_bbox.y_max.max(*y);
9536 }
9537 let half_w = sp.line_width
9538 * 0.5
9539 * (ctm.a * ctm.a + ctm.b * ctm.b)
9540 .sqrt()
9541 .max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt());
9542 dev_bbox.x_min -= half_w;
9543 dev_bbox.y_min -= half_w;
9544 dev_bbox.x_max += half_w;
9545 dev_bbox.y_max += half_w;
9546 Some(dev_bbox)
9547 } else {
9548 path_full_bbox(¶ms.path)
9549 }
9550}
9551
9552fn pattern_fill_y_bbox(params: &stet_graphics::device::PatternFillParams) -> Option<YBBox> {
9554 let bbox = pattern_fill_full_bbox(params)?;
9555 Some(YBBox {
9556 y_min: bbox.y_min,
9557 y_max: bbox.y_max,
9558 })
9559}
9560
9561fn image_full_bbox(params: &ImageParams) -> Option<BBox2D> {
9563 let m = ¶ms.ctm;
9564 let im = ¶ms.image_matrix;
9565 let im_inv = im.invert()?;
9566 let combined = m.concat(&im_inv);
9567 let w = params.width as f64;
9569 let h = params.height as f64;
9570 let corners = [
9571 combined.transform_point(0.0, 0.0),
9572 combined.transform_point(w, 0.0),
9573 combined.transform_point(0.0, h),
9574 combined.transform_point(w, h),
9575 ];
9576 let mut x_min = f64::INFINITY;
9577 let mut x_max = f64::NEG_INFINITY;
9578 let mut y_min = f64::INFINITY;
9579 let mut y_max = f64::NEG_INFINITY;
9580 for (x, y) in &corners {
9581 x_min = x_min.min(*x);
9582 x_max = x_max.max(*x);
9583 y_min = y_min.min(*y);
9584 y_max = y_max.max(*y);
9585 }
9586 Some(BBox2D {
9587 x_min,
9588 y_min,
9589 x_max,
9590 y_max,
9591 })
9592}
9593
9594fn shading_full_bbox(bbox: &Option<[f64; 4]>, ctm: &Matrix) -> Option<BBox2D> {
9596 if let Some(bbox) = bbox {
9597 let corners = [
9598 ctm.transform_point(bbox[0], bbox[1]),
9599 ctm.transform_point(bbox[2], bbox[1]),
9600 ctm.transform_point(bbox[0], bbox[3]),
9601 ctm.transform_point(bbox[2], bbox[3]),
9602 ];
9603 let mut x_min = f64::INFINITY;
9604 let mut x_max = f64::NEG_INFINITY;
9605 let mut y_min = f64::INFINITY;
9606 let mut y_max = f64::NEG_INFINITY;
9607 for (x, y) in &corners {
9608 x_min = x_min.min(*x);
9609 x_max = x_max.max(*x);
9610 y_min = y_min.min(*y);
9611 y_max = y_max.max(*y);
9612 }
9613 Some(BBox2D {
9614 x_min,
9615 y_min,
9616 x_max,
9617 y_max,
9618 })
9619 } else {
9620 Some(BBox2D {
9621 x_min: 0.0,
9622 y_min: 0.0,
9623 x_max: 1e9,
9624 y_max: 1e9,
9625 })
9626 }
9627}
9628
9629fn build_viewport_epochs(list: &DisplayList, bboxes: &[Option<BBox2D>]) -> Vec<ViewportEpoch> {
9631 let elements = list.elements();
9632 let mut epochs = Vec::new();
9633 let mut epoch_start = 0;
9634 let mut x_min = f64::INFINITY;
9635 let mut x_max = f64::NEG_INFINITY;
9636 let mut y_min = f64::INFINITY;
9637 let mut y_max = f64::NEG_INFINITY;
9638 let mut has_erase = false;
9639
9640 for (i, element) in elements.iter().enumerate() {
9641 if matches!(element, DisplayElement::InitClip) && i > epoch_start {
9642 epochs.push(ViewportEpoch {
9643 start_idx: epoch_start,
9644 end_idx: i,
9645 paint_bbox: if x_min <= x_max {
9646 Some(BBox2D {
9647 x_min,
9648 y_min,
9649 x_max,
9650 y_max,
9651 })
9652 } else {
9653 None
9654 },
9655 has_erase_page: has_erase,
9656 });
9657 epoch_start = i;
9658 x_min = f64::INFINITY;
9659 x_max = f64::NEG_INFINITY;
9660 y_min = f64::INFINITY;
9661 y_max = f64::NEG_INFINITY;
9662 has_erase = false;
9663 }
9664 if matches!(element, DisplayElement::ErasePage) {
9665 has_erase = true;
9666 }
9667 if let Some(ref bbox) = bboxes[i] {
9668 x_min = x_min.min(bbox.x_min);
9669 x_max = x_max.max(bbox.x_max);
9670 y_min = y_min.min(bbox.y_min);
9671 y_max = y_max.max(bbox.y_max);
9672 }
9673 }
9674 if epoch_start < elements.len() {
9675 epochs.push(ViewportEpoch {
9676 start_idx: epoch_start,
9677 end_idx: elements.len(),
9678 paint_bbox: if x_min <= x_max {
9679 Some(BBox2D {
9680 x_min,
9681 y_min,
9682 x_max,
9683 y_max,
9684 })
9685 } else {
9686 None
9687 },
9688 has_erase_page: has_erase,
9689 });
9690 }
9691 epochs
9692}
9693
9694struct ViewportEpoch {
9696 start_idx: usize,
9697 end_idx: usize,
9698 paint_bbox: Option<BBox2D>,
9699 has_erase_page: bool,
9700}
9701
9702pub struct PreparedDisplayList {
9708 bboxes: Vec<Option<BBox2D>>,
9709 epochs: Vec<ViewportEpoch>,
9710 clip_seen: HashSet<u64>,
9711}
9712
9713pub fn prepare_display_list(list: &DisplayList) -> PreparedDisplayList {
9718 let bboxes = precompute_full_bboxes(list, 72.0);
9719 let epochs = build_viewport_epochs(list, &bboxes);
9720 let clip_seen = precompute_clip_seen(list);
9721 PreparedDisplayList {
9722 bboxes,
9723 epochs,
9724 clip_seen,
9725 }
9726}
9727
9728struct PreprocessedImage {
9733 data: Vec<u8>,
9735 width: u32,
9737 height: u32,
9738 adj_sx: f32,
9742 adj_ky: f32,
9743 adj_kx: f32,
9744 adj_sy: f32,
9745 quality: stet_tiny_skia::FilterQuality,
9747}
9748
9749pub struct ImageCache {
9755 entries: Vec<Option<Vec<u8>>>,
9757}
9758
9759impl ImageCache {
9760 pub fn build(list: &DisplayList, icc: Option<&IccCache>) -> Self {
9762 let entries = list
9763 .elements()
9764 .iter()
9765 .map(|elem| {
9766 if let DisplayElement::Image {
9767 sample_data,
9768 params,
9769 } = elem
9770 {
9771 if params.width == 0 || params.height == 0 {
9772 return None;
9773 }
9774 let mut rgba = samples_to_rgba(sample_data, params, icc, false);
9775 if params.mask_color.is_some() {
9776 apply_mask_color_rgba(&mut rgba, sample_data, params);
9777 }
9778 Some(rgba)
9779 } else {
9780 None
9781 }
9782 })
9783 .collect();
9784 Self { entries }
9785 }
9786
9787 pub fn get(&self, index: usize) -> Option<&[u8]> {
9789 self.entries.get(index).and_then(|e| e.as_deref())
9790 }
9791}
9792
9793fn preprocess_images_for_bands(
9798 list: &DisplayList,
9799 icc: Option<&IccCache>,
9800) -> Vec<Option<PreprocessedImage>> {
9801 list.elements()
9802 .iter()
9803 .map(|elem| {
9804 let DisplayElement::Image {
9805 sample_data,
9806 params,
9807 } = elem
9808 else {
9809 return None;
9810 };
9811 let iw = params.width;
9812 let ih = params.height;
9813 if iw == 0 || ih == 0 {
9814 return None;
9815 }
9816 if params.overprint {
9818 return None;
9819 }
9820
9821 let mut rgba = samples_to_rgba(sample_data, params, icc, false);
9823 if params.mask_color.is_some() {
9824 apply_mask_color_rgba(&mut rgba, sample_data, params);
9825 }
9826
9827 let image_inv = params.image_matrix.invert()?;
9829 let combined = params.ctm.concat(&image_inv);
9830 let base_transform = enforce_min_image_size(to_transform(&combined), iw, ih);
9831
9832 let (data, width, height, adj_t) =
9834 match prescale_image(&rgba, iw, ih, base_transform, params.interpolate) {
9835 Some((d, w, h, t)) => {
9836 drop(rgba); (d, w, h, t)
9838 }
9839 None => (rgba, iw, ih, base_transform),
9840 };
9841
9842 let quality = image_filter_quality(adj_t, params.interpolate);
9843
9844 Some(PreprocessedImage {
9845 data,
9846 width,
9847 height,
9848 adj_sx: adj_t.sx,
9849 adj_ky: adj_t.ky,
9850 adj_kx: adj_t.kx,
9851 adj_sy: adj_t.sy,
9852 quality,
9853 })
9854 })
9855 .collect()
9856}
9857
9858#[allow(clippy::too_many_arguments)]
9864pub fn render_region_prepared(
9865 list: &DisplayList,
9866 prepared: &PreparedDisplayList,
9867 vp_x: f64,
9868 vp_y: f64,
9869 vp_w: f64,
9870 vp_h: f64,
9871 pixel_w: u32,
9872 pixel_h: u32,
9873 dpi: f64,
9874 icc: Option<&IccCache>,
9875 image_cache: Option<&ImageCache>,
9876 no_aa: bool,
9877) -> Vec<u8> {
9878 if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
9879 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
9880 }
9881
9882 let layer_set = LayerSet::new();
9883 let scale_x = pixel_w as f64 / vp_w;
9884 let scale_y = pixel_h as f64 / vp_h;
9885 let effective_dpi = dpi * scale_x;
9886
9887 const OVERLAP: u32 = 6;
9898 let render_h = pixel_h + 2 * OVERLAP;
9899 let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create viewport pixmap");
9900 pixmap.fill(Color::TRANSPARENT);
9902
9903 let cmyk_buf = if has_overprint_elements(list)
9904 || list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
9905 || has_cmyk_group(list)
9906 {
9907 Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
9908 } else {
9909 None
9910 };
9911
9912 let mut state = BandState {
9913 clip_region: None,
9914 spare_mask: None,
9915 clip_mask_cache: HashMap::new(),
9916 clip_mask_seen: prepared.clip_seen.clone(),
9917 mask_pool: Vec::new(),
9918 cmyk_buffer: cmyk_buf,
9919 op_bg_snapshot: None,
9920 op_touched: None,
9921 spot_mask: None,
9922 };
9923
9924 let elements = list.elements();
9925 let vp_x_f = vp_x as f32;
9926 let vp_y_f = vp_y as f32;
9927 let sx = scale_x as f32;
9928 let sy = scale_y as f32;
9929 let vp_x_max = vp_x + vp_w;
9930 let vp_y_max = vp_y + vp_h;
9931
9932 for epoch in &prepared.epochs {
9933 if !epoch.has_erase_page {
9934 match epoch.paint_bbox {
9935 Some(ref pb)
9936 if pb.x_max <= vp_x
9937 || pb.x_min >= vp_x_max
9938 || pb.y_max <= vp_y
9939 || pb.y_min >= vp_y_max =>
9940 {
9941 continue;
9942 }
9943 None => continue,
9944 _ => {}
9945 }
9946 }
9947
9948 #[allow(clippy::needless_range_loop)]
9949 for i in epoch.start_idx..epoch.end_idx {
9950 let force_process = matches!(
9953 &elements[i],
9954 DisplayElement::OcgGroup { elements: inner, .. }
9955 if contains_clip_op(inner)
9956 );
9957 if !force_process
9958 && let Some(ref bbox) = prepared.bboxes[i]
9959 && (bbox.x_max <= vp_x
9960 || bbox.x_min >= vp_x_max
9961 || bbox.y_max <= vp_y
9962 || bbox.y_min >= vp_y_max)
9963 {
9964 continue;
9965 }
9966 let ctx = RenderContext {
9967 vp_x: vp_x_f,
9968 vp_y: vp_y_f,
9969 scale_x: sx,
9970 scale_y: sy,
9971 out_w: pixel_w,
9972 out_h: render_h,
9973 effective_dpi,
9974 icc,
9975 image_cache,
9976 preprocessed: None,
9977 elem_idx: i,
9978 no_aa,
9979 opm_zero_transparent: false,
9980 knockout_painter_pass: KnockoutPainterPass::None,
9981 parent_group_isolated: false,
9982 alpha_extraction_pass: false,
9983 layer_set: &layer_set,
9984 };
9985 render_element(&mut pixmap, &mut state, &elements[i], &ctx);
9986 }
9987 }
9988
9989 composite_onto_white(pixmap.data_mut());
9991 let row_bytes = pixel_w as usize * 4;
9993 let end = pixel_h as usize * row_bytes;
9994 pixmap.data()[..end].to_vec()
9995}
9996
9997pub fn viewport_band_count(pixel_w: u32, pixel_h: u32) -> (u32, u32) {
10002 let band_h = select_band_height(pixel_w, pixel_h);
10003 let num_bands = if band_h >= pixel_h {
10004 1
10005 } else {
10006 pixel_h.div_ceil(band_h)
10007 };
10008 (num_bands, band_h)
10009}
10010
10011#[allow(clippy::too_many_arguments)]
10020pub fn render_region_single_band(
10021 list: &DisplayList,
10022 prepared: &PreparedDisplayList,
10023 vp_x: f64,
10024 vp_y: f64,
10025 vp_w: f64,
10026 vp_h: f64,
10027 pixel_w: u32,
10028 pixel_h: u32,
10029 band_idx: u32,
10030 band_h: u32,
10031 num_bands: u32,
10032 dpi: f64,
10033 icc: Option<&IccCache>,
10034 image_cache: Option<&ImageCache>,
10035 no_aa: bool,
10036) -> Vec<u8> {
10037 if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
10038 let actual_h = if band_idx < num_bands - 1 {
10039 band_h
10040 } else {
10041 pixel_h - band_idx * band_h
10042 };
10043 return vec![0xFF; pixel_w as usize * actual_h as usize * 4];
10044 }
10045
10046 let layer_set = LayerSet::new();
10047 let scale_x = pixel_w as f64 / vp_w;
10048 let scale_y = pixel_h as f64 / vp_h;
10049 let effective_dpi = dpi * scale_x;
10050
10051 let out_y_start = band_idx * band_h;
10053 let actual_h = if band_idx < num_bands - 1 {
10054 band_h
10055 } else {
10056 pixel_h - out_y_start
10057 };
10058
10059 const OVERLAP: u32 = 6;
10070 let render_y_start = out_y_start.saturating_sub(OVERLAP);
10071 let render_y_end = (out_y_start + actual_h + OVERLAP).min(pixel_h);
10072 let render_h = band_h + 2 * OVERLAP;
10073 let overlap_top = out_y_start - render_y_start;
10074
10075 let src_y_min = vp_y + render_y_start as f64 / scale_y;
10077 let src_y_max = vp_y + render_y_end as f64 / scale_y;
10078
10079 let band_vp_y = vp_y + render_y_start as f64 / scale_y;
10081
10082 let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create band pixmap");
10083 pixmap.fill(Color::TRANSPARENT);
10084
10085 let cmyk_buf = if has_overprint_elements(list)
10086 || list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
10087 || has_cmyk_group(list)
10088 {
10089 Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
10090 } else {
10091 None
10092 };
10093
10094 let mut state = BandState {
10095 clip_region: None,
10096 spare_mask: None,
10097 clip_mask_cache: HashMap::new(),
10098 clip_mask_seen: prepared.clip_seen.clone(),
10099 mask_pool: Vec::new(),
10100 cmyk_buffer: cmyk_buf,
10101 op_bg_snapshot: None,
10102 op_touched: None,
10103 spot_mask: None,
10104 };
10105
10106 let elements = list.elements();
10107 let vp_x_f = vp_x as f32;
10108 let band_vp_y_f = band_vp_y as f32;
10109 let sx = scale_x as f32;
10110 let sy = scale_y as f32;
10111 let vp_x_max = vp_x + vp_w;
10112
10113 for epoch in &prepared.epochs {
10114 if !epoch.has_erase_page {
10115 match epoch.paint_bbox {
10116 Some(ref pb)
10117 if pb.x_max <= vp_x
10118 || pb.x_min >= vp_x_max
10119 || pb.y_max <= src_y_min
10120 || pb.y_min >= src_y_max =>
10121 {
10122 continue;
10123 }
10124 None => continue,
10125 _ => {}
10126 }
10127 }
10128
10129 #[allow(clippy::needless_range_loop)]
10130 for i in epoch.start_idx..epoch.end_idx {
10131 let force_process = matches!(
10135 &elements[i],
10136 DisplayElement::OcgGroup { elements: inner, .. }
10137 if contains_clip_op(inner)
10138 );
10139 if !force_process
10140 && let Some(ref bbox) = prepared.bboxes[i]
10141 && (bbox.x_max <= vp_x
10142 || bbox.x_min >= vp_x_max
10143 || bbox.y_max <= src_y_min
10144 || bbox.y_min >= src_y_max)
10145 {
10146 continue;
10147 }
10148 let ctx = RenderContext {
10149 vp_x: vp_x_f,
10150 vp_y: band_vp_y_f,
10151 scale_x: sx,
10152 scale_y: sy,
10153 out_w: pixel_w,
10154 out_h: render_h,
10155 effective_dpi,
10156 icc,
10157 image_cache,
10158 preprocessed: None,
10159 elem_idx: i,
10160 no_aa,
10161 opm_zero_transparent: false,
10162 knockout_painter_pass: KnockoutPainterPass::None,
10163 parent_group_isolated: false,
10164 alpha_extraction_pass: false,
10165 layer_set: &layer_set,
10166 };
10167 render_element(&mut pixmap, &mut state, &elements[i], &ctx);
10168 }
10169 }
10170
10171 composite_onto_white(pixmap.data_mut());
10173
10174 let row_bytes = pixel_w as usize * 4;
10176 let start = overlap_top as usize * row_bytes;
10177 let end = start + actual_h as usize * row_bytes;
10178 pixmap.data()[start..end].to_vec()
10179}
10180
10181#[allow(clippy::too_many_arguments)]
10190pub fn render_region_prepared_parallel(
10191 list: &DisplayList,
10192 prepared: &PreparedDisplayList,
10193 vp_x: f64,
10194 vp_y: f64,
10195 vp_w: f64,
10196 vp_h: f64,
10197 pixel_w: u32,
10198 pixel_h: u32,
10199 dpi: f64,
10200 icc: Option<&IccCache>,
10201 image_cache: Option<&ImageCache>,
10202 no_aa: bool,
10203) -> Vec<u8> {
10204 let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
10205
10206 if num_bands <= 1 {
10207 return render_region_prepared(
10209 list,
10210 prepared,
10211 vp_x,
10212 vp_y,
10213 vp_w,
10214 vp_h,
10215 pixel_w,
10216 pixel_h,
10217 dpi,
10218 icc,
10219 image_cache,
10220 no_aa,
10221 );
10222 }
10223
10224 let render_band = |band_idx: u32| -> Vec<u8> {
10225 render_region_single_band(
10226 list,
10227 prepared,
10228 vp_x,
10229 vp_y,
10230 vp_w,
10231 vp_h,
10232 pixel_w,
10233 pixel_h,
10234 band_idx,
10235 band_h,
10236 num_bands,
10237 dpi,
10238 icc,
10239 image_cache,
10240 no_aa,
10241 )
10242 };
10243
10244 let row_bytes = pixel_w as usize * 4;
10245 let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
10246
10247 #[cfg(feature = "parallel")]
10248 {
10249 let chunk_size = rayon::current_num_threads().max(1);
10250
10251 for chunk_start in (0..num_bands).step_by(chunk_size) {
10252 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
10253
10254 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
10255 .into_par_iter()
10256 .map(&render_band)
10257 .collect();
10258
10259 for (i, band_data) in rendered.iter().enumerate() {
10260 let band_idx = chunk_start + i as u32;
10261 let y_start = (band_idx * band_h) as usize;
10262 let dest_start = y_start * row_bytes;
10263 let len = band_data.len();
10264 result[dest_start..dest_start + len].copy_from_slice(band_data);
10265 }
10266 }
10267 }
10268 #[cfg(not(feature = "parallel"))]
10269 {
10270 for band_idx in 0..num_bands {
10271 let band_data = render_band(band_idx);
10272 let y_start = (band_idx * band_h) as usize;
10273 let dest_start = y_start * row_bytes;
10274 let len = band_data.len();
10275 result[dest_start..dest_start + len].copy_from_slice(&band_data);
10276 }
10277 }
10278
10279 result
10280}
10281
10282#[allow(clippy::too_many_arguments)]
10287pub fn render_region_prepared_parallel_with_progress(
10288 list: &DisplayList,
10289 prepared: &PreparedDisplayList,
10290 vp_x: f64,
10291 vp_y: f64,
10292 vp_w: f64,
10293 vp_h: f64,
10294 pixel_w: u32,
10295 pixel_h: u32,
10296 dpi: f64,
10297 icc: Option<&IccCache>,
10298 image_cache: Option<&ImageCache>,
10299 no_aa: bool,
10300 progress: &std::sync::atomic::AtomicU32,
10301) -> Vec<u8> {
10302 let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
10303
10304 if num_bands <= 1 {
10305 let result = render_region_prepared(
10306 list,
10307 prepared,
10308 vp_x,
10309 vp_y,
10310 vp_w,
10311 vp_h,
10312 pixel_w,
10313 pixel_h,
10314 dpi,
10315 icc,
10316 image_cache,
10317 no_aa,
10318 );
10319 progress.store(1, std::sync::atomic::Ordering::Relaxed);
10320 return result;
10321 }
10322
10323 let render_band = |band_idx: u32| -> Vec<u8> {
10324 render_region_single_band(
10325 list,
10326 prepared,
10327 vp_x,
10328 vp_y,
10329 vp_w,
10330 vp_h,
10331 pixel_w,
10332 pixel_h,
10333 band_idx,
10334 band_h,
10335 num_bands,
10336 dpi,
10337 icc,
10338 image_cache,
10339 no_aa,
10340 )
10341 };
10342
10343 let row_bytes = pixel_w as usize * 4;
10344 let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
10345
10346 #[cfg(feature = "parallel")]
10347 {
10348 let chunk_size = rayon::current_num_threads().max(1);
10349
10350 for chunk_start in (0..num_bands).step_by(chunk_size) {
10351 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
10352
10353 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
10354 .into_par_iter()
10355 .map(&render_band)
10356 .collect();
10357
10358 for (i, band_data) in rendered.iter().enumerate() {
10359 let band_idx = chunk_start + i as u32;
10360 let y_start = (band_idx * band_h) as usize;
10361 let dest_start = y_start * row_bytes;
10362 let len = band_data.len();
10363 result[dest_start..dest_start + len].copy_from_slice(band_data);
10364 }
10365 progress.store(chunk_end, std::sync::atomic::Ordering::Relaxed);
10366 }
10367 }
10368 #[cfg(not(feature = "parallel"))]
10369 {
10370 for band_idx in 0..num_bands {
10371 let band_data = render_band(band_idx);
10372 let y_start = (band_idx * band_h) as usize;
10373 let dest_start = y_start * row_bytes;
10374 let len = band_data.len();
10375 result[dest_start..dest_start + len].copy_from_slice(&band_data);
10376 progress.store(band_idx + 1, std::sync::atomic::Ordering::Relaxed);
10377 }
10378 }
10379
10380 result
10381}
10382
10383#[allow(clippy::too_many_arguments)]
10386pub fn render_region_prepared_parallel_cancellable(
10387 list: &DisplayList,
10388 prepared: &PreparedDisplayList,
10389 vp_x: f64,
10390 vp_y: f64,
10391 vp_w: f64,
10392 vp_h: f64,
10393 pixel_w: u32,
10394 pixel_h: u32,
10395 dpi: f64,
10396 icc: Option<&IccCache>,
10397 image_cache: Option<&ImageCache>,
10398 no_aa: bool,
10399 cancelled: &std::sync::atomic::AtomicBool,
10400) -> Option<Vec<u8>> {
10401 if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
10402 return None;
10403 }
10404
10405 let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
10406
10407 if num_bands <= 1 {
10408 return Some(render_region_prepared(
10409 list,
10410 prepared,
10411 vp_x,
10412 vp_y,
10413 vp_w,
10414 vp_h,
10415 pixel_w,
10416 pixel_h,
10417 dpi,
10418 icc,
10419 image_cache,
10420 no_aa,
10421 ));
10422 }
10423
10424 let render_band = |band_idx: u32| -> Vec<u8> {
10425 render_region_single_band(
10426 list,
10427 prepared,
10428 vp_x,
10429 vp_y,
10430 vp_w,
10431 vp_h,
10432 pixel_w,
10433 pixel_h,
10434 band_idx,
10435 band_h,
10436 num_bands,
10437 dpi,
10438 icc,
10439 image_cache,
10440 no_aa,
10441 )
10442 };
10443
10444 let row_bytes = pixel_w as usize * 4;
10445 let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
10446
10447 #[cfg(feature = "parallel")]
10448 {
10449 let chunk_size = rayon::current_num_threads().max(1);
10450
10451 for chunk_start in (0..num_bands).step_by(chunk_size) {
10452 if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
10453 return None;
10454 }
10455 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
10456
10457 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
10458 .into_par_iter()
10459 .map(&render_band)
10460 .collect();
10461
10462 for (i, band_data) in rendered.iter().enumerate() {
10463 let band_idx = chunk_start + i as u32;
10464 let y_start = (band_idx * band_h) as usize;
10465 let dest_start = y_start * row_bytes;
10466 let len = band_data.len();
10467 result[dest_start..dest_start + len].copy_from_slice(band_data);
10468 }
10469 }
10470 }
10471 #[cfg(not(feature = "parallel"))]
10472 {
10473 for band_idx in 0..num_bands {
10474 if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
10475 return None;
10476 }
10477 let band_data = render_band(band_idx);
10478 let y_start = (band_idx * band_h) as usize;
10479 let dest_start = y_start * row_bytes;
10480 let len = band_data.len();
10481 result[dest_start..dest_start + len].copy_from_slice(&band_data);
10482 }
10483 }
10484
10485 Some(result)
10486}
10487
10488pub fn render_to_rgba(
10496 list: &DisplayList,
10497 pixel_w: u32,
10498 pixel_h: u32,
10499 dpi: f64,
10500 icc: Option<&IccCache>,
10501 no_aa: bool,
10502) -> Vec<u8> {
10503 render_to_rgba_with_layers(list, pixel_w, pixel_h, dpi, icc, no_aa, &LayerSet::new())
10504}
10505
10506#[allow(clippy::too_many_arguments)]
10513pub fn render_to_rgba_with_layers(
10514 list: &DisplayList,
10515 pixel_w: u32,
10516 pixel_h: u32,
10517 dpi: f64,
10518 icc: Option<&IccCache>,
10519 no_aa: bool,
10520 layer_set: &LayerSet,
10521) -> Vec<u8> {
10522 if pixel_w == 0 || pixel_h == 0 {
10523 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10524 }
10525
10526 let mut icc_cache = match icc {
10527 Some(c) => c.clone(),
10528 None => IccCache::new(),
10529 };
10530 register_shading_icc_profiles(list, &mut icc_cache);
10533
10534 let mut sink = MemorySink {
10535 data: Vec::new(),
10536 width: 0,
10537 };
10538
10539 let band_h = select_band_height(pixel_w, pixel_h);
10540 if let Err(e) = render_banded_to_sink(
10541 pixel_w, pixel_h, band_h, dpi, list, &mut sink, &icc_cache, no_aa, layer_set,
10542 ) {
10543 eprintln!("render_to_rgba: banded render failed: {e}");
10544 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10545 }
10546
10547 sink.data
10548}
10549
10550pub fn render_to_rgba_viewport(
10563 list: &DisplayList,
10564 pixel_w: u32,
10565 pixel_h: u32,
10566 dpi: f64,
10567 icc: Option<&IccCache>,
10568 no_aa: bool,
10569) -> Vec<u8> {
10570 if pixel_w == 0 || pixel_h == 0 {
10571 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10572 }
10573
10574 let mut icc_cache = match icc {
10575 Some(c) => c.clone(),
10576 None => IccCache::new(),
10577 };
10578 register_shading_icc_profiles(list, &mut icc_cache);
10579
10580 let prepared = prepare_display_list(list);
10581 render_region_prepared_parallel(
10582 list,
10583 &prepared,
10584 0.0,
10585 0.0,
10586 pixel_w as f64,
10587 pixel_h as f64,
10588 pixel_w,
10589 pixel_h,
10590 dpi,
10591 Some(&icc_cache),
10592 None,
10593 no_aa,
10594 )
10595}
10596
10597fn debug_bbox_lines(list: &DisplayList, dpi: f64, depth: usize, out: &mut Vec<String>) {
10604 let y_bboxes = precompute_bboxes(list, dpi);
10605 let full_bboxes = precompute_full_bboxes(list, dpi);
10606 let elements = list.elements();
10607 let indent = " ".repeat(depth);
10608 for (i, elem) in elements.iter().enumerate() {
10609 let kind = match elem {
10610 DisplayElement::Fill { .. } => "Fill",
10611 DisplayElement::Stroke { .. } => "Stroke",
10612 DisplayElement::Image { .. } => "Image",
10613 DisplayElement::AxialShading { .. } => "AxialShading",
10614 DisplayElement::RadialShading { .. } => "RadialShading",
10615 DisplayElement::MeshShading { .. } => "MeshShading",
10616 DisplayElement::PatchShading { .. } => "PatchShading",
10617 DisplayElement::PatternFill { .. } => "PatternFill",
10618 DisplayElement::Group { .. } => "Group",
10619 DisplayElement::SoftMasked { .. } => "SoftMasked",
10620 DisplayElement::OcgGroup { .. } => "OcgGroup",
10621 DisplayElement::Clip { .. } => "Clip",
10622 DisplayElement::InitClip => "InitClip",
10623 DisplayElement::ErasePage => "ErasePage",
10624 DisplayElement::Text { .. } => "Text",
10625 _ => "Unknown",
10626 };
10627 let yb = &y_bboxes[i];
10628 let fb = &full_bboxes[i];
10629 let mut diff = false;
10630 if yb.is_some() != fb.is_some() {
10631 diff = true;
10632 }
10633 if let (Some(yb), Some(fb)) = (yb, fb)
10634 && ((yb.y_min - fb.y_min).abs() > 1e-9 || (yb.y_max - fb.y_max).abs() > 1e-9)
10635 {
10636 diff = true;
10637 }
10638 let yb_s = match yb {
10639 Some(b) => format!("Y[{:8.3}..{:8.3}]", b.y_min, b.y_max),
10640 None => "Y[None]".to_string(),
10641 };
10642 let fb_s = match fb {
10643 Some(b) => format!(
10644 "2D[x {:8.3}..{:8.3} y {:8.3}..{:8.3}]",
10645 b.x_min, b.x_max, b.y_min, b.y_max
10646 ),
10647 None => "2D[None]".to_string(),
10648 };
10649 out.push(format!(
10650 "{}{:4} {:15} {:30} {:55} {}",
10651 indent,
10652 i,
10653 kind,
10654 yb_s,
10655 fb_s,
10656 if diff { "DIFF" } else { "" }
10657 ));
10658 if let DisplayElement::Stroke { path, params } = elem {
10659 let rp = path_full_bbox(path);
10660 let m = ¶ms.ctm;
10661 out.push(format!(
10662 "{} ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] lw={:.4} miter={:.4} raw={}",
10663 indent,
10664 m.a,
10665 m.b,
10666 m.c,
10667 m.d,
10668 m.tx,
10669 m.ty,
10670 params.line_width,
10671 params.miter_limit,
10672 match rp {
10673 Some(b) => format!(
10674 "x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
10675 b.x_min, b.x_max, b.y_min, b.y_max
10676 ),
10677 None => "None".to_string(),
10678 }
10679 ));
10680 }
10681 if let DisplayElement::Clip { path, params } = elem {
10682 let rp = path_full_bbox(path);
10683 let m = ¶ms.ctm;
10684 out.push(format!(
10685 "{} clip ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] rule={:?} raw={}",
10686 indent,
10687 m.a,
10688 m.b,
10689 m.c,
10690 m.d,
10691 m.tx,
10692 m.ty,
10693 params.fill_rule,
10694 match rp {
10695 Some(b) => format!(
10696 "x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
10697 b.x_min, b.x_max, b.y_min, b.y_max
10698 ),
10699 None => "None".to_string(),
10700 }
10701 ));
10702 }
10703 if let DisplayElement::PatchShading { params } = elem {
10704 out.push(format!(
10705 "{} patch ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] bbox={:?} patches={}",
10706 indent,
10707 params.ctm.a,
10708 params.ctm.b,
10709 params.ctm.c,
10710 params.ctm.d,
10711 params.ctm.tx,
10712 params.ctm.ty,
10713 params.bbox,
10714 params.patches.len()
10715 ));
10716 if !params.patches.is_empty() {
10717 let patch = ¶ms.patches[0];
10718 let mut x_min = f64::INFINITY;
10720 let mut y_min = f64::INFINITY;
10721 let mut x_max = f64::NEG_INFINITY;
10722 let mut y_max = f64::NEG_INFINITY;
10723 for &(px, py) in &patch.points {
10724 let (dx, dy) = params.ctm.transform_point(px, py);
10725 x_min = x_min.min(dx);
10726 y_min = y_min.min(dy);
10727 x_max = x_max.max(dx);
10728 y_max = y_max.max(dy);
10729 }
10730 out.push(format!(
10731 "{} patch[0] pts={} dev x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
10732 indent,
10733 patch.points.len(),
10734 x_min,
10735 x_max,
10736 y_min,
10737 y_max
10738 ));
10739 }
10740 }
10741 if let DisplayElement::Group {
10742 elements: inner,
10743 params,
10744 } = elem
10745 {
10746 out.push(format!(
10747 "{} group bbox={:?} iso={} ko={} alpha={} bm={} cs={:?}",
10748 indent,
10749 params.bbox,
10750 params.isolated,
10751 params.knockout,
10752 params.alpha,
10753 params.blend_mode,
10754 params.color_space
10755 ));
10756 debug_bbox_lines(inner, dpi, depth + 1, out);
10757 }
10758 if let DisplayElement::SoftMasked {
10759 content, params, ..
10760 } = elem
10761 {
10762 out.push(format!(
10763 "{} softmasked bbox={:?}",
10764 indent, params.bbox
10765 ));
10766 debug_bbox_lines(content, dpi, depth + 1, out);
10767 }
10768 if let DisplayElement::OcgGroup {
10769 elements: inner,
10770 visibility,
10771 } = elem
10772 {
10773 out.push(format!(
10774 "{} ocg default_visible={}",
10775 indent,
10776 visibility.default_visible()
10777 ));
10778 debug_bbox_lines(inner, dpi, depth + 1, out);
10779 }
10780 }
10781}
10782
10783pub fn debug_bbox_comparison(list: &DisplayList, dpi: f64) -> Vec<String> {
10784 let mut out = Vec::new();
10785 debug_bbox_lines(list, dpi, 0, &mut out);
10786 out
10787}
10788
10789struct MemorySink {
10791 data: Vec<u8>,
10792 width: u32,
10793}
10794
10795impl stet_graphics::device::PageSink for MemorySink {
10796 fn begin_page(&mut self, width: u32, height: u32) -> Result<(), String> {
10797 self.width = width;
10798 self.data.reserve(width as usize * height as usize * 4);
10799 Ok(())
10800 }
10801
10802 fn write_rows(&mut self, rgba_rows: &[u8], _num_rows: u32) -> Result<(), String> {
10803 self.data.extend_from_slice(rgba_rows);
10804 Ok(())
10805 }
10806
10807 fn end_page(&mut self) -> Result<(), String> {
10808 Ok(())
10809 }
10810}
10811
10812#[allow(clippy::too_many_arguments)]
10821pub fn render_region(
10822 list: &DisplayList,
10823 vp_x: f64,
10824 vp_y: f64,
10825 vp_w: f64,
10826 vp_h: f64,
10827 pixel_w: u32,
10828 pixel_h: u32,
10829 dpi: f64,
10830 icc: Option<&IccCache>,
10831 image_cache: Option<&ImageCache>,
10832 no_aa: bool,
10833) -> Vec<u8> {
10834 if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
10835 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10836 }
10837
10838 let layer_set = LayerSet::new();
10839 let scale_x = pixel_w as f64 / vp_w;
10840 let scale_y = pixel_h as f64 / vp_h;
10841 let effective_dpi = dpi * scale_x;
10843
10844 let bboxes = precompute_full_bboxes(list, effective_dpi);
10845 let epochs = build_viewport_epochs(list, &bboxes);
10846 let clip_seen = precompute_clip_seen(list);
10847
10848 const OVERLAP: u32 = 6;
10852 let render_h = pixel_h + 2 * OVERLAP;
10853
10854 let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create viewport pixmap");
10855 pixmap.fill(Color::TRANSPARENT);
10856
10857 let cmyk_buf = if has_overprint_elements(list)
10858 || list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
10859 || has_cmyk_group(list)
10860 {
10861 Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
10862 } else {
10863 None
10864 };
10865
10866 let mut state = BandState {
10867 clip_region: None,
10868 spare_mask: None,
10869 clip_mask_cache: HashMap::new(),
10870 clip_mask_seen: clip_seen,
10871 mask_pool: Vec::new(),
10872 cmyk_buffer: cmyk_buf,
10873 op_bg_snapshot: None,
10874 op_touched: None,
10875 spot_mask: None,
10876 };
10877
10878 let elements = list.elements();
10879 let vp_x_f = vp_x as f32;
10880 let vp_y_f = vp_y as f32;
10881 let sx = scale_x as f32;
10882 let sy = scale_y as f32;
10883 let vp_x_max = vp_x + vp_w;
10884 let vp_y_max = vp_y + vp_h;
10885
10886 for epoch in &epochs {
10887 if !epoch.has_erase_page {
10889 match epoch.paint_bbox {
10890 Some(ref pb)
10891 if pb.x_max <= vp_x
10892 || pb.x_min >= vp_x_max
10893 || pb.y_max <= vp_y
10894 || pb.y_min >= vp_y_max =>
10895 {
10896 continue;
10897 }
10898 None => continue,
10899 _ => {}
10900 }
10901 }
10902
10903 for i in epoch.start_idx..epoch.end_idx {
10904 let force_process = matches!(
10907 &elements[i],
10908 DisplayElement::OcgGroup { elements: inner, .. }
10909 if contains_clip_op(inner)
10910 );
10911 if !force_process
10913 && let Some(ref bbox) = bboxes[i]
10914 && (bbox.x_max <= vp_x
10915 || bbox.x_min >= vp_x_max
10916 || bbox.y_max <= vp_y
10917 || bbox.y_min >= vp_y_max)
10918 {
10919 continue;
10920 }
10921 let ctx = RenderContext {
10922 vp_x: vp_x_f,
10923 vp_y: vp_y_f,
10924 scale_x: sx,
10925 scale_y: sy,
10926 out_w: pixel_w,
10927 out_h: render_h,
10928 effective_dpi,
10929 icc,
10930 image_cache,
10931 preprocessed: None,
10932 elem_idx: i,
10933 no_aa,
10934 opm_zero_transparent: false,
10935 knockout_painter_pass: KnockoutPainterPass::None,
10936 parent_group_isolated: false,
10937 alpha_extraction_pass: false,
10938 layer_set: &layer_set,
10939 };
10940 render_element(&mut pixmap, &mut state, &elements[i], &ctx);
10941 }
10942 }
10943
10944 composite_onto_white(pixmap.data_mut());
10945 let row_bytes = pixel_w as usize * 4;
10947 let end = pixel_h as usize * row_bytes;
10948 pixmap.data()[..end].to_vec()
10949}
10950fn copy_backdrop_crop(
10952 parent: &Pixmap,
10953 crop_x: i32,
10954 crop_y: i32,
10955 crop_w: u32,
10956 crop_h: u32,
10957) -> Vec<u8> {
10958 let pw = parent.width() as usize;
10959 let src = parent.data();
10960 let cw = crop_w as usize;
10961 let ch = crop_h as usize;
10962 let cx = crop_x as usize;
10963 let cy = crop_y as usize;
10964 let mut backdrop = vec![0u8; cw * ch * 4];
10965 for row in 0..ch {
10966 let src_off = ((cy + row) * pw + cx) * 4;
10967 let dst_off = row * cw * 4;
10968 backdrop[dst_off..dst_off + cw * 4].copy_from_slice(&src[src_off..src_off + cw * 4]);
10969 }
10970 backdrop
10971}
10972fn clip_polygon_halfplane(
10977 poly: &[(f32, f32)],
10978 nx: f32,
10979 ny: f32,
10980 px: f32,
10981 py: f32,
10982) -> Vec<(f32, f32)> {
10983 if poly.is_empty() {
10984 return vec![];
10985 }
10986 let dot = |x: f32, y: f32| nx * (x - px) + ny * (y - py);
10987 let mut out = Vec::with_capacity(poly.len() + 1);
10988 let n = poly.len();
10989 for i in 0..n {
10990 let (ax, ay) = poly[i];
10991 let (bx, by) = poly[(i + 1) % n];
10992 let da = dot(ax, ay);
10993 let db = dot(bx, by);
10994 if da >= 0.0 {
10995 out.push((ax, ay));
10996 }
10997 if (da >= 0.0) != (db >= 0.0) {
10998 let t = da / (da - db);
11000 out.push((ax + t * (bx - ax), ay + t * (by - ay)));
11001 }
11002 }
11003 out
11004}
11005
11006#[allow(clippy::too_many_arguments)]
11008fn render_axial_shading(
11009 pixmap: &mut Pixmap,
11010 params: &AxialShadingParams,
11011 vp_x: f32,
11012 vp_y: f32,
11013 scale_x: f32,
11014 scale_y: f32,
11015 clip_mask: Option<&Mask>,
11016 no_aa: bool,
11017 cmyk_buf: Option<&mut [f32]>,
11018 icc: Option<&IccCache>,
11019) {
11020 let pw = pixmap.width();
11021 let ph = pixmap.height();
11022 if params.color_stops.is_empty() || pw == 0 || ph == 0 {
11023 return;
11024 }
11025
11026 let (mut rx_min, mut ry_min, mut rx_max, mut ry_max) = if let Some(bbox) = ¶ms.bbox {
11027 let corners = [
11028 params.ctm.transform_point(bbox[0], bbox[1]),
11029 params.ctm.transform_point(bbox[2], bbox[1]),
11030 params.ctm.transform_point(bbox[0], bbox[3]),
11031 params.ctm.transform_point(bbox[2], bbox[3]),
11032 ];
11033 let x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
11034 let y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
11035 let x_max = corners
11036 .iter()
11037 .map(|c| c.0)
11038 .fold(f64::NEG_INFINITY, f64::max);
11039 let y_max = corners
11040 .iter()
11041 .map(|c| c.1)
11042 .fold(f64::NEG_INFINITY, f64::max);
11043 (
11044 ((x_min as f32 - vp_x) * scale_x).max(0.0),
11045 ((y_min as f32 - vp_y) * scale_y).max(0.0),
11046 ((x_max as f32 - vp_x) * scale_x).min(pw as f32),
11047 ((y_max as f32 - vp_y) * scale_y).min(ph as f32),
11048 )
11049 } else {
11050 (0.0, 0.0, pw as f32, ph as f32)
11051 };
11052
11053 if rx_max <= rx_min || ry_max <= ry_min {
11054 return;
11055 }
11056
11057 let (dx0, dy0) = params.ctm.transform_point(params.x0, params.y0);
11059 let (dx1, dy1) = params.ctm.transform_point(params.x1, params.y1);
11060
11061 let needs_perpendicular_clip = (!params.extend_start || !params.extend_end) && {
11065 let axis_x = dx1 - dx0;
11066 let axis_y = dy1 - dy0;
11067 axis_x.abs() > 1e-6 && axis_y.abs() > 1e-6
11068 };
11069
11070 let bbox_is_rotated =
11073 params.bbox.is_some() && (params.ctm.b.abs() > 1e-10 || params.ctm.c.abs() > 1e-10);
11074
11075 if needs_perpendicular_clip {
11076 let stops = build_gradient_stops(¶ms.color_stops);
11079 if stops.is_empty() {
11080 return;
11081 }
11082 let start = stet_tiny_skia::Point::from_xy(params.x0 as f32, params.y0 as f32);
11083 let end = stet_tiny_skia::Point::from_xy(params.x1 as f32, params.y1 as f32);
11084 let gradient_transform =
11085 viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
11086 let Some(gradient) = stet_tiny_skia::LinearGradient::new(
11087 start,
11088 end,
11089 stops,
11090 stet_tiny_skia::SpreadMode::Pad,
11091 gradient_transform,
11092 ) else {
11093 return;
11094 };
11095 let paint = Paint {
11096 shader: gradient,
11097 anti_alias: !no_aa,
11098 ..Paint::default()
11099 };
11100
11101 let mut poly: Vec<(f32, f32)> = if bbox_is_rotated {
11103 let bbox = params.bbox.as_ref().unwrap();
11104 let corners = [
11105 params.ctm.transform_point(bbox[0], bbox[1]),
11106 params.ctm.transform_point(bbox[2], bbox[1]),
11107 params.ctm.transform_point(bbox[2], bbox[3]),
11108 params.ctm.transform_point(bbox[0], bbox[3]),
11109 ];
11110 corners
11111 .iter()
11112 .map(|(x, y)| ((*x as f32 - vp_x) * scale_x, (*y as f32 - vp_y) * scale_y))
11113 .collect()
11114 } else {
11115 vec![
11116 (rx_min, ry_min),
11117 (rx_max, ry_min),
11118 (rx_max, ry_max),
11119 (rx_min, ry_max),
11120 ]
11121 };
11122 let ax = (dx1 - dx0) as f32 * scale_x;
11123 let ay = (dy1 - dy0) as f32 * scale_y;
11124 if !params.extend_start {
11125 let px = (dx0 as f32 - vp_x) * scale_x;
11126 let py = (dy0 as f32 - vp_y) * scale_y;
11127 poly = clip_polygon_halfplane(&poly, ax, ay, px, py);
11128 }
11129 if !params.extend_end {
11130 let px = (dx1 as f32 - vp_x) * scale_x;
11131 let py = (dy1 as f32 - vp_y) * scale_y;
11132 poly = clip_polygon_halfplane(&poly, -ax, -ay, px, py);
11133 }
11134 if poly.len() >= 3 {
11135 let mut pb = PathBuilder::new();
11136 pb.move_to(poly[0].0, poly[0].1);
11137 for &(x, y) in &poly[1..] {
11138 pb.line_to(x, y);
11139 }
11140 pb.close();
11141 if let Some(path) = pb.finish() {
11142 pixmap.fill_path(
11143 &path,
11144 &paint,
11145 SkiaFillRule::Winding,
11146 Transform::identity(),
11147 clip_mask,
11148 );
11149 }
11150 }
11151 } else {
11152 if !params.extend_start || !params.extend_end {
11155 let axis_x = dx1 - dx0;
11156 let axis_y = dy1 - dy0;
11157 let gx0 = (dx0 as f32 - vp_x) * scale_x;
11158 let gy0 = (dy0 as f32 - vp_y) * scale_y;
11159 let gx1 = (dx1 as f32 - vp_x) * scale_x;
11160 let gy1 = (dy1 as f32 - vp_y) * scale_y;
11161
11162 if axis_x.abs() >= axis_y.abs() {
11163 if !params.extend_start {
11164 if axis_x >= 0.0 {
11165 rx_min = rx_min.max(gx0);
11166 } else {
11167 rx_max = rx_max.min(gx0);
11168 }
11169 }
11170 if !params.extend_end {
11171 if axis_x >= 0.0 {
11172 rx_max = rx_max.min(gx1);
11173 } else {
11174 rx_min = rx_min.max(gx1);
11175 }
11176 }
11177 } else {
11178 if !params.extend_start {
11179 if axis_y >= 0.0 {
11180 ry_min = ry_min.max(gy0);
11181 } else {
11182 ry_max = ry_max.min(gy0);
11183 }
11184 }
11185 if !params.extend_end {
11186 if axis_y >= 0.0 {
11187 ry_max = ry_max.min(gy1);
11188 } else {
11189 ry_min = ry_min.max(gy1);
11190 }
11191 }
11192 }
11193 if rx_max <= rx_min || ry_max <= ry_min {
11194 return;
11195 }
11196 }
11197
11198 let ax = params.x1 - params.x0;
11200 let ay = params.y1 - params.y0;
11201 let axis_sq = ax * ax + ay * ay;
11202 if axis_sq < 1e-20 {
11203 return;
11204 }
11205
11206 let pixel_dx = (dx1 - dx0) * scale_x as f64;
11210 let pixel_dy = (dy1 - dy0) * scale_y as f64;
11211 let pixel_axis_len = (pixel_dx * pixel_dx + pixel_dy * pixel_dy).sqrt();
11212 let lut_size = (pixel_axis_len as usize)
11213 .max(params.color_stops.len())
11214 .max(256)
11215 .min(16384);
11216 let lut = build_gradient_lut(¶ms.color_stops, lut_size);
11217
11218 let Some(inv) = params.ctm.invert() else {
11219 return;
11220 };
11221 let inv_sx = 1.0 / scale_x as f64;
11222 let inv_sy = 1.0 / scale_y as f64;
11223 let dev_origin_x = vp_x as f64;
11224 let dev_origin_y = vp_y as f64;
11225
11226 let sx_base = inv.a * dev_origin_x + inv.c * dev_origin_y + inv.tx;
11230 let sy_base = inv.b * dev_origin_x + inv.d * dev_origin_y + inv.ty;
11231 let dsx_dx = inv.a * inv_sx;
11232 let dsx_dy = inv.c * inv_sy;
11233 let dsy_dx = inv.b * inv_sx;
11234 let dsy_dy = inv.d * inv_sy;
11235
11236 let inv_axis_sq = 1.0 / axis_sq;
11238 let t_origin = ((sx_base - params.x0) * ax + (sy_base - params.y0) * ay) * inv_axis_sq;
11239 let dt_dx = (dsx_dx * ax + dsy_dx * ay) * inv_axis_sq;
11240 let dt_dy = (dsx_dy * ax + dsy_dy * ay) * inv_axis_sq;
11241
11242 let bbox_pixel_clip = if bbox_is_rotated {
11245 let bbox = params.bbox.as_ref().unwrap();
11246 let (bx0, bx1) = (bbox[0].min(bbox[2]), bbox[0].max(bbox[2]));
11247 let (by0, by1) = (bbox[1].min(bbox[3]), bbox[1].max(bbox[3]));
11248 Some((
11249 dsx_dx, dsx_dy, sx_base, dsy_dx, dsy_dy, sy_base, bx0, by0, bx1, by1,
11250 ))
11251 } else {
11252 None
11253 };
11254
11255 let ix_min = rx_min.floor() as u32;
11256 let ix_max = rx_max.ceil().min(pw as f32) as u32;
11257 let iy_min = ry_min.floor() as u32;
11258 let iy_max = ry_max.ceil().min(ph as f32) as u32;
11259
11260 let stride = pw as usize * 4;
11261 let data = pixmap.data_mut();
11262 let mask_data = clip_mask.map(|m| m.data());
11263 let alpha = (params.alpha.clamp(0.0, 1.0) * 255.0 + 0.5) as u16;
11264
11265 for py in iy_min..iy_max {
11266 let t_row = t_origin + dt_dy * py as f64;
11267 let row_offset = py as usize * stride;
11268
11269 let (ux_row, uy_row) =
11271 if let Some((_, dux_dy, ux_base, _, duy_dy, uy_base, ..)) = &bbox_pixel_clip {
11272 (ux_base + dux_dy * py as f64, uy_base + duy_dy * py as f64)
11273 } else {
11274 (0.0, 0.0)
11275 };
11276
11277 for px in ix_min..ix_max {
11278 if let Some(md) = mask_data {
11280 if md[py as usize * pw as usize + px as usize] == 0 {
11281 continue;
11282 }
11283 }
11284
11285 if let Some((dux_dx, _, _, duy_dx, _, _, bx0, by0, bx1, by1)) = &bbox_pixel_clip {
11287 let ux = ux_row + dux_dx * px as f64;
11288 let uy = uy_row + duy_dx * px as f64;
11289 if ux < *bx0 || ux > *bx1 || uy < *by0 || uy > *by1 {
11290 continue;
11291 }
11292 }
11293
11294 let t = t_row + dt_dx * px as f64;
11295 let t_clamped = t.clamp(0.0, 1.0);
11296 let idx = (t_clamped * (lut_size - 1) as f64 + 0.5) as usize;
11297 let [r, g, b, _] = lut[idx.min(lut_size - 1)];
11298
11299 let offset = row_offset + px as usize * 4;
11300 if alpha >= 255 {
11301 data[offset] = r;
11302 data[offset + 1] = g;
11303 data[offset + 2] = b;
11304 data[offset + 3] = 255;
11305 } else {
11306 let a = alpha as u16;
11308 let inv_a = 255 - a;
11309 data[offset] = ((r as u16 * a + data[offset] as u16 * inv_a + 127) / 255) as u8;
11310 data[offset + 1] =
11311 ((g as u16 * a + data[offset + 1] as u16 * inv_a + 127) / 255) as u8;
11312 data[offset + 2] =
11313 ((b as u16 * a + data[offset + 2] as u16 * inv_a + 127) / 255) as u8;
11314 data[offset + 3] = ((a + data[offset + 3] as u16 * inv_a / 255).min(255)) as u8;
11315 }
11316 }
11317 }
11318 }
11319
11320 if let Some(buf) = cmyk_buf {
11322 let pw = pixmap.width();
11323 let inv_sx = 1.0 / scale_x as f64;
11324 let inv_sy = 1.0 / scale_y as f64;
11325 let axis_x = params.x1 - params.x0;
11326 let axis_y = params.y1 - params.y0;
11327 let axis_len_sq = axis_x * axis_x + axis_y * axis_y;
11328 let Some(inv_ctm) = params.ctm.invert() else {
11329 return;
11330 };
11331
11332 let iy_min = ry_min.floor() as u32;
11333 let iy_max = ry_max.ceil().min(pixmap.height() as f32) as u32;
11334 let ix_min = rx_min.floor() as u32;
11335 let ix_max = rx_max.ceil().min(pw as f32) as u32;
11336
11337 for py in iy_min..iy_max {
11338 let dev_y = py as f64 * inv_sy + vp_y as f64;
11339 for px in ix_min..ix_max {
11340 let dev_x = px as f64 * inv_sx + vp_x as f64;
11341 let (ux, uy) = inv_ctm.transform_point(dev_x, dev_y);
11342 let t = if axis_len_sq > 1e-10 {
11343 ((ux - params.x0) * axis_x + (uy - params.y0) * axis_y) / axis_len_sq
11344 } else {
11345 0.0
11346 };
11347 if t < 0.0 && !params.extend_start {
11348 continue;
11349 }
11350 if t > 1.0 && !params.extend_end {
11351 continue;
11352 }
11353 let clamped = t.clamp(0.0, 1.0);
11354
11355 if let Some(mask) = clip_mask {
11356 let mi = py as usize * pw as usize + px as usize;
11357 if mask.data()[mi] == 0 {
11358 continue;
11359 }
11360 }
11361
11362 let color = interpolate_color_stops(¶ms.color_stops, clamped);
11363 let cmyk = interpolate_cmyk_from_stops(
11364 ¶ms.color_stops,
11365 ¶ms.color_space,
11366 clamped,
11367 &color,
11368 icc,
11369 );
11370 let ci = (py as usize * pw as usize + px as usize) * 4;
11371 if ci + 3 < buf.len() {
11372 if params.spot_tint_blend && params.overprint {
11373 let cur_c = buf[ci] as f64;
11406 let cur_m = buf[ci + 1] as f64;
11407 let cur_y = buf[ci + 2] as f64;
11408 let cur_k = buf[ci + 3] as f64;
11409 let cur_is_zero =
11410 cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
11411 let named = params.painted_channels;
11412 if cur_is_zero {
11413 if named & stet_graphics::device::CMYK_C != 0 {
11414 buf[ci] = cmyk.0 as f32;
11415 }
11416 if named & stet_graphics::device::CMYK_M != 0 {
11417 buf[ci + 1] = cmyk.1 as f32;
11418 }
11419 if named & stet_graphics::device::CMYK_Y != 0 {
11420 buf[ci + 2] = cmyk.2 as f32;
11421 }
11422 if named & stet_graphics::device::CMYK_K != 0 {
11423 buf[ci + 3] = cmyk.3 as f32;
11424 }
11425 } else {
11426 let new_c = if named & stet_graphics::device::CMYK_C != 0 {
11427 cmyk.0
11428 } else {
11429 cur_c
11430 };
11431 let new_m = if named & stet_graphics::device::CMYK_M != 0 {
11432 cmyk.1
11433 } else {
11434 cur_m
11435 };
11436 let new_y = if named & stet_graphics::device::CMYK_Y != 0 {
11437 cmyk.2
11438 } else {
11439 cur_y
11440 };
11441 let new_k = if named & stet_graphics::device::CMYK_K != 0 {
11442 cmyk.3
11443 } else {
11444 cur_k
11445 };
11446 buf[ci] = new_c as f32;
11447 buf[ci + 1] = new_m as f32;
11448 buf[ci + 2] = new_y as f32;
11449 buf[ci + 3] = new_k as f32;
11450 let (rv, gv, bv) = if let Some(icc_cache) = icc {
11451 icc_cache
11452 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
11453 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
11454 } else {
11455 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
11456 };
11457 let stride = pixmap.data().len() / pixmap.height() as usize;
11458 let offset = py as usize * stride + px as usize * 4;
11459 let data = pixmap.data_mut();
11460 data[offset] = (rv * 255.0).round().clamp(0.0, 255.0) as u8;
11461 data[offset + 1] = (gv * 255.0).round().clamp(0.0, 255.0) as u8;
11462 data[offset + 2] = (bv * 255.0).round().clamp(0.0, 255.0) as u8;
11463 }
11464 } else if params.overprint
11465 && params.painted_channels != stet_graphics::device::CMYK_ALL
11466 {
11467 if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
11468 buf[ci] = cmyk.0 as f32;
11469 }
11470 if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
11471 buf[ci + 1] = cmyk.1 as f32;
11472 }
11473 if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
11474 buf[ci + 2] = cmyk.2 as f32;
11475 }
11476 if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
11477 buf[ci + 3] = cmyk.3 as f32;
11478 }
11479 let c = buf[ci] as f64;
11481 let m = buf[ci + 1] as f64;
11482 let y = buf[ci + 2] as f64;
11483 let k = buf[ci + 3] as f64;
11484 let (rv, gv, bv) = if let Some(icc_cache) = icc {
11485 icc_cache
11486 .convert_cmyk_readonly(c, m, y, k)
11487 .unwrap_or_else(|| cmyk_to_rgb_plrm(c, m, y, k))
11488 } else {
11489 cmyk_to_rgb_plrm(c, m, y, k)
11490 };
11491 let stride = pixmap.data().len() / pixmap.height() as usize;
11492 let offset = py as usize * stride + px as usize * 4;
11493 let data = pixmap.data_mut();
11494 data[offset] = (rv * 255.0).round().clamp(0.0, 255.0) as u8;
11495 data[offset + 1] = (gv * 255.0).round().clamp(0.0, 255.0) as u8;
11496 data[offset + 2] = (bv * 255.0).round().clamp(0.0, 255.0) as u8;
11497 } else {
11498 buf[ci] = cmyk.0 as f32;
11515 buf[ci + 1] = cmyk.1 as f32;
11516 buf[ci + 2] = cmyk.2 as f32;
11517 buf[ci + 3] = cmyk.3 as f32;
11518 }
11519 }
11520 }
11521 }
11522 }
11523}
11524
11525#[allow(clippy::too_many_arguments)]
11527fn render_radial_shading(
11528 pixmap: &mut Pixmap,
11529 params: &RadialShadingParams,
11530 vp_x: f32,
11531 vp_y: f32,
11532 scale_x: f32,
11533 scale_y: f32,
11534 clip_mask: Option<&Mask>,
11535 _no_aa: bool,
11536 mut cmyk_buf: Option<&mut [f32]>,
11537 icc: Option<&IccCache>,
11538) {
11539 let pw = pixmap.width();
11540 let ph = pixmap.height();
11541 if params.color_stops.is_empty() || pw == 0 || ph == 0 {
11542 return;
11543 }
11544
11545 let Some(inv_ctm) = params.ctm.invert() else {
11546 return;
11547 };
11548
11549 let (px_min, py_min, px_max, py_max) = if let Some(bbox) = ¶ms.bbox {
11550 let corners = [
11551 params.ctm.transform_point(bbox[0], bbox[1]),
11552 params.ctm.transform_point(bbox[2], bbox[1]),
11553 params.ctm.transform_point(bbox[0], bbox[3]),
11554 params.ctm.transform_point(bbox[2], bbox[3]),
11555 ];
11556 let x_min = corners
11557 .iter()
11558 .map(|c| c.0 as f32)
11559 .fold(f32::INFINITY, f32::min);
11560 let y_min = corners
11561 .iter()
11562 .map(|c| c.1 as f32)
11563 .fold(f32::INFINITY, f32::min);
11564 let x_max = corners
11565 .iter()
11566 .map(|c| c.0 as f32)
11567 .fold(f32::NEG_INFINITY, f32::max);
11568 let y_max = corners
11569 .iter()
11570 .map(|c| c.1 as f32)
11571 .fold(f32::NEG_INFINITY, f32::max);
11572 (
11573 ((x_min - vp_x) * scale_x).max(0.0) as u32,
11574 ((y_min - vp_y) * scale_y).max(0.0) as u32,
11575 (((x_max - vp_x) * scale_x).ceil() as u32).min(pw),
11576 (((y_max - vp_y) * scale_y).ceil() as u32).min(ph),
11577 )
11578 } else {
11579 (0, 0, pw, ph)
11580 };
11581
11582 let inv_sx = 1.0 / scale_x as f64;
11583 let inv_sy = 1.0 / scale_y as f64;
11584
11585 let rotated_bbox = if let Some(bbox) = ¶ms.bbox {
11587 if params.ctm.b.abs() > 1e-10 || params.ctm.c.abs() > 1e-10 {
11588 let (bx0, bx1) = (bbox[0].min(bbox[2]), bbox[0].max(bbox[2]));
11589 let (by0, by1) = (bbox[1].min(bbox[3]), bbox[1].max(bbox[3]));
11590 Some((bx0, by0, bx1, by1))
11591 } else {
11592 None
11593 }
11594 } else {
11595 None
11596 };
11597
11598 let data = pixmap.data_mut();
11599 let stride = pw as usize * 4;
11600
11601 for py in py_min..py_max {
11602 let dev_y = py as f64 * inv_sy + vp_y as f64;
11603 for px in px_min..px_max {
11604 let dev_x = px as f64 * inv_sx + vp_x as f64;
11605 let (ux, uy) = inv_ctm.transform_point(dev_x, dev_y);
11606
11607 if let Some((bx0, by0, bx1, by1)) = rotated_bbox {
11609 if ux < bx0 || ux > bx1 || uy < by0 || uy > by1 {
11610 continue;
11611 }
11612 }
11613
11614 let t = solve_radial_t(
11615 ux,
11616 uy,
11617 params.x0,
11618 params.y0,
11619 params.r0,
11620 params.x1,
11621 params.y1,
11622 params.r1,
11623 params.extend_start,
11624 params.extend_end,
11625 );
11626 if let Some(t) = t {
11627 let clamped = t.clamp(0.0, 1.0);
11628 let color = interpolate_color_stops(¶ms.color_stops, clamped);
11629
11630 let clipped = clip_mask
11631 .is_some_and(|mask| mask.data()[py as usize * pw as usize + px as usize] == 0);
11632
11633 if clipped {
11634 continue;
11635 }
11636
11637 let cmyk = interpolate_cmyk_from_stops(
11646 ¶ms.color_stops,
11647 ¶ms.color_space,
11648 clamped,
11649 &color,
11650 icc,
11651 );
11652 let ci = (py as usize * pw as usize + px as usize) * 4;
11653 let buffer_clean = if let Some(ref buf) = cmyk_buf {
11654 if ci + 3 < buf.len() {
11655 buf[ci] == 0.0
11656 && buf[ci + 1] == 0.0
11657 && buf[ci + 2] == 0.0
11658 && buf[ci + 3] == 0.0
11659 } else {
11660 false
11661 }
11662 } else {
11663 false
11664 };
11665 let offset_for_check = py as usize * stride + px as usize * 4;
11666 let pixmap_has_colour = data[offset_for_check + 3] > 0
11667 && (data[offset_for_check] < 250
11668 || data[offset_for_check + 1] < 250
11669 || data[offset_for_check + 2] < 250);
11670 let use_multiplicative = params.overprint
11671 && params.painted_channels != stet_graphics::device::CMYK_ALL
11672 && buffer_clean
11673 && pixmap_has_colour;
11674
11675 if let Some(ref mut buf) = cmyk_buf
11677 && ci + 3 < buf.len()
11678 {
11679 if params.overprint
11680 && params.painted_channels != stet_graphics::device::CMYK_ALL
11681 {
11682 if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
11683 buf[ci] = cmyk.0 as f32;
11684 }
11685 if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
11686 buf[ci + 1] = cmyk.1 as f32;
11687 }
11688 if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
11689 buf[ci + 2] = cmyk.2 as f32;
11690 }
11691 if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
11692 buf[ci + 3] = cmyk.3 as f32;
11693 }
11694 } else {
11695 buf[ci] = cmyk.0 as f32;
11696 buf[ci + 1] = cmyk.1 as f32;
11697 buf[ci + 2] = cmyk.2 as f32;
11698 buf[ci + 3] = cmyk.3 as f32;
11699 }
11700 }
11701
11702 let offset = py as usize * stride + px as usize * 4;
11703 if use_multiplicative {
11704 let bg_r = data[offset] as f64 / 255.0;
11709 let bg_g = data[offset + 1] as f64 / 255.0;
11710 let bg_b = data[offset + 2] as f64 / 255.0;
11711 let over_r = if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
11712 1.0 - cmyk.0
11713 } else {
11714 1.0
11715 };
11716 let over_g = if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
11717 1.0 - cmyk.1
11718 } else {
11719 1.0
11720 };
11721 let over_b = if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
11722 1.0 - cmyk.2
11723 } else {
11724 1.0
11725 };
11726 let k_fac = if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
11727 1.0 - cmyk.3
11728 } else {
11729 1.0
11730 };
11731 data[offset] = ((bg_r * over_r * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
11732 data[offset + 1] =
11733 ((bg_g * over_g * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
11734 data[offset + 2] =
11735 ((bg_b * over_b * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
11736 data[offset + 3] = 255;
11737 } else {
11738 data[offset] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
11739 data[offset + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
11740 data[offset + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
11741 data[offset + 3] = 255;
11742
11743 if params.overprint
11758 && params.painted_channels != stet_graphics::device::CMYK_ALL
11759 && matches!(params.color_space, ShadingColorSpace::DeviceCMYK)
11760 && let Some(ref mut buf) = cmyk_buf
11761 && ci + 3 < buf.len()
11762 && let Some(icc_cache) = icc
11763 {
11764 let c = buf[ci] as f64;
11765 let m = buf[ci + 1] as f64;
11766 let y = buf[ci + 2] as f64;
11767 let k = buf[ci + 3] as f64;
11768 if let Some((r, g, b)) = icc_cache.convert_cmyk_readonly(c, m, y, k) {
11769 data[offset] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
11770 data[offset + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
11771 data[offset + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
11772 }
11773 }
11774 }
11775 }
11776 }
11777 }
11778}
11779#[allow(clippy::too_many_arguments)]
11784fn solve_radial_t(
11785 px: f64,
11786 py: f64,
11787 x0: f64,
11788 y0: f64,
11789 r0: f64,
11790 x1: f64,
11791 y1: f64,
11792 r1: f64,
11793 extend_start: bool,
11794 extend_end: bool,
11795) -> Option<f64> {
11796 let cdx = x1 - x0;
11799 let cdy = y1 - y0;
11800 let dr = r1 - r0;
11801
11802 let a = cdx * cdx + cdy * cdy - dr * dr;
11803 let dpx = px - x0;
11804 let dpy = py - y0;
11805 let b = -2.0 * (dpx * cdx + dpy * cdy + r0 * dr);
11806 let c = dpx * dpx + dpy * dpy - r0 * r0;
11807
11808 let in_domain = |t: f64| -> bool {
11810 (0.0..=1.0).contains(&t) || (t < 0.0 && extend_start) || (t > 1.0 && extend_end)
11811 };
11812
11813 if a.abs() < 1e-10 {
11814 if b.abs() < 1e-10 {
11816 return None;
11817 }
11818 let t = -c / b;
11819 let radius = r0 + t * dr;
11820 if radius >= 0.0 && in_domain(t) {
11821 return Some(t);
11822 }
11823 return None;
11824 }
11825
11826 let discriminant = b * b - 4.0 * a * c;
11827 if discriminant < 0.0 {
11828 return None;
11829 }
11830 let sqrt_d = discriminant.sqrt();
11831 let t1 = (-b + sqrt_d) / (2.0 * a);
11832 let t2 = (-b - sqrt_d) / (2.0 * a);
11833
11834 let mut best: Option<f64> = None;
11836 for t in [t1, t2] {
11837 let radius = r0 + t * dr;
11838 if radius >= 0.0 && in_domain(t) {
11839 best = Some(match best {
11840 Some(prev) => prev.max(t),
11841 None => t,
11842 });
11843 }
11844 }
11845 best
11846}
11847
11848#[allow(clippy::too_many_arguments)]
11850fn render_mesh_shading(
11851 pixmap: &mut Pixmap,
11852 params: &MeshShadingParams,
11853 vp_x: f32,
11854 vp_y: f32,
11855 scale_x: f32,
11856 scale_y: f32,
11857 clip_mask: Option<&Mask>,
11858 mut cmyk_buf: Option<&mut [f32]>,
11859 icc: Option<&IccCache>,
11860) {
11861 let pw = pixmap.width() as usize;
11862 let ph = pixmap.height() as usize;
11863 if pw == 0 || ph == 0 {
11864 return;
11865 }
11866 let data = pixmap.data_mut();
11867 let stride = pw * 4;
11868
11869 let lut = params.color_lut.as_deref();
11870
11871 for tri in ¶ms.triangles {
11872 let (dx0, dy0) = params.ctm.transform_point(tri.v0.x, tri.v0.y);
11873 let (dx1, dy1) = params.ctm.transform_point(tri.v1.x, tri.v1.y);
11874 let (dx2, dy2) = params.ctm.transform_point(tri.v2.x, tri.v2.y);
11875
11876 let x0 = (dx0 as f32 - vp_x) * scale_x;
11877 let y0 = (dy0 as f32 - vp_y) * scale_y;
11878 let x1 = (dx1 as f32 - vp_x) * scale_x;
11879 let y1 = (dy1 as f32 - vp_y) * scale_y;
11880 let x2 = (dx2 as f32 - vp_x) * scale_x;
11881 let y2 = (dy2 as f32 - vp_y) * scale_y;
11882
11883 let min_x = (x0.min(x1).min(x2).floor().max(0.0)) as usize;
11884 let max_x = (x0.max(x1).max(x2).ceil() as usize).min(pw);
11885 let min_y = (y0.min(y1).min(y2).floor().max(0.0)) as usize;
11886 let max_y = (y0.max(y1).max(y2).ceil() as usize).min(ph);
11887
11888 if min_x >= max_x || min_y >= max_y {
11889 continue;
11890 }
11891
11892 let x0 = x0 as f64;
11893 let y0 = y0 as f64;
11894 let x1 = x1 as f64;
11895 let y1 = y1 as f64;
11896 let x2 = x2 as f64;
11897 let y2 = y2 as f64;
11898 let denom = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2);
11903 if denom.abs() < 1e-10 {
11904 continue;
11905 }
11906 let (x1, y1, x2, y2) = if denom < 0.0 {
11907 (x2, y2, x1, y1)
11908 } else {
11909 (x1, y1, x2, y2)
11910 };
11911 let (v1_ref, v2_ref) = if denom < 0.0 {
11912 (&tri.v2, &tri.v1)
11913 } else {
11914 (&tri.v1, &tri.v2)
11915 };
11916 let denom = denom.abs();
11917 let inv_denom = 1.0 / denom;
11918
11919 for py in min_y..max_y {
11920 for px in min_x..max_x {
11921 let pxf = px as f64 + 0.5;
11922 let pyf = py as f64 + 0.5;
11923
11924 let w0 = ((y1 - y2) * (pxf - x2) + (x2 - x1) * (pyf - y2)) * inv_denom;
11925 let w1 = ((y2 - y0) * (pxf - x2) + (x0 - x2) * (pyf - y2)) * inv_denom;
11926 let w2 = 1.0 - w0 - w1;
11927
11928 if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 {
11929 continue;
11930 }
11931
11932 let clipped = clip_mask.is_some_and(|mask| mask.data()[py * pw + px] == 0);
11933
11934 let w0c = w0.max(0.0);
11935 let w1c = w1.max(0.0);
11936 let w2c = w2.max(0.0);
11937 let wsum = w0c + w1c + w2c;
11938 let w0n = w0c / wsum;
11939 let w1n = w1c / wsum;
11940 let w2n = w2c / wsum;
11941
11942 let (r, g, b) = if let Some(lut) = lut {
11945 let raw = w0n * tri.v0.raw_components[0]
11947 + w1n * v1_ref.raw_components[0]
11948 + w2n * v2_ref.raw_components[0];
11949 let raw = raw.clamp(0.0, 1.0);
11950 let fi = raw * (lut.len() - 1) as f64;
11952 let i0 = (fi as usize).min(lut.len().saturating_sub(2));
11953 let frac = fi - i0 as f64;
11954 let c0 = &lut[i0];
11955 let c1 = &lut[i0 + 1];
11956 (
11957 c0.r + frac * (c1.r - c0.r),
11958 c0.g + frac * (c1.g - c0.g),
11959 c0.b + frac * (c1.b - c0.b),
11960 )
11961 } else {
11962 (
11963 w0n * tri.v0.color.r + w1n * v1_ref.color.r + w2n * v2_ref.color.r,
11964 w0n * tri.v0.color.g + w1n * v1_ref.color.g + w2n * v2_ref.color.g,
11965 w0n * tri.v0.color.b + w1n * v1_ref.color.b + w2n * v2_ref.color.b,
11966 )
11967 };
11968
11969 if let Some(ref mut buf) = cmyk_buf {
11971 let ci = (py * pw + px) * 4;
11972 if ci + 3 < buf.len() {
11973 let cmyk = interpolate_cmyk_from_vertices(
11974 &tri.v0,
11975 v1_ref,
11976 v2_ref,
11977 w0n,
11978 w1n,
11979 w2n,
11980 ¶ms.color_space,
11981 r,
11982 g,
11983 b,
11984 icc,
11985 );
11986 if params.overprint
11987 && params.painted_channels != stet_graphics::device::CMYK_ALL
11988 {
11989 if !clipped {
11990 if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
11991 buf[ci] = cmyk.0 as f32;
11992 }
11993 if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
11994 buf[ci + 1] = cmyk.1 as f32;
11995 }
11996 if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
11997 buf[ci + 2] = cmyk.2 as f32;
11998 }
11999 if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
12000 buf[ci + 3] = cmyk.3 as f32;
12001 }
12002 }
12003 } else {
12004 buf[ci] = cmyk.0 as f32;
12005 buf[ci + 1] = cmyk.1 as f32;
12006 buf[ci + 2] = cmyk.2 as f32;
12007 buf[ci + 3] = cmyk.3 as f32;
12008 }
12009 }
12010 }
12011
12012 if clipped {
12013 continue;
12014 }
12015
12016 let offset = py * stride + px * 4;
12017 data[offset] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
12018 data[offset + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
12019 data[offset + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
12020 data[offset + 3] = 255;
12021 }
12022 }
12023 }
12024}
12025
12026#[allow(clippy::too_many_arguments)]
12028fn render_patch_shading(
12029 pixmap: &mut Pixmap,
12030 params: &PatchShadingParams,
12031 vp_x: f32,
12032 vp_y: f32,
12033 scale_x: f32,
12034 scale_y: f32,
12035 clip_mask: Option<&Mask>,
12036 cmyk_buf: Option<&mut [f32]>,
12037 icc: Option<&IccCache>,
12038) {
12039 let mut triangles = Vec::new();
12040 let scale = scale_x.max(scale_y) as f64;
12041 for patch in ¶ms.patches {
12042 if patch.points.len() >= 12 {
12043 let mut x_min = f64::INFINITY;
12045 let mut y_min = f64::INFINITY;
12046 let mut x_max = f64::NEG_INFINITY;
12047 let mut y_max = f64::NEG_INFINITY;
12048 for &(px, py) in &patch.points {
12049 let (dx, dy) = params.ctm.transform_point(px, py);
12050 x_min = x_min.min(dx);
12051 y_min = y_min.min(dy);
12052 x_max = x_max.max(dx);
12053 y_max = y_max.max(dy);
12054 }
12055 let extent = (x_max - x_min).max(y_max - y_min).abs() * scale;
12056 let n = (extent / 2.0).ceil().clamp(8.0, 64.0) as usize;
12058 let icc_profile_hash = match ¶ms.color_space {
12060 stet_graphics::device::ShadingColorSpace::ICCBased { profile_hash, .. } => {
12061 Some(profile_hash)
12062 }
12063 _ => None,
12064 };
12065 subdivide_patch_to_triangles(patch, &mut triangles, n, icc_profile_hash, icc);
12066 }
12067 }
12068 if !triangles.is_empty() {
12069 let mesh_params = MeshShadingParams {
12070 triangles,
12071 ctm: params.ctm,
12072 bbox: params.bbox,
12073 color_space: params.color_space.clone(),
12074 overprint: params.overprint,
12075 painted_channels: params.painted_channels,
12076 color_lut: params.color_lut.clone(),
12077 alpha: params.alpha,
12078 blend_mode: params.blend_mode,
12079 alpha_is_shape: params.alpha_is_shape,
12080 };
12081 render_mesh_shading(
12082 pixmap,
12083 &mesh_params,
12084 vp_x,
12085 vp_y,
12086 scale_x,
12087 scale_y,
12088 clip_mask,
12089 cmyk_buf,
12090 icc,
12091 );
12092 }
12093}
12094fn subdivide_patch_to_triangles(
12099 patch: &stet_graphics::device::ShadingPatch,
12100 triangles: &mut Vec<stet_graphics::device::ShadingTriangle>,
12101 n: usize,
12102 icc_profile_hash: Option<&stet_graphics::icc::ProfileHash>,
12103 icc_cache: Option<&IccCache>,
12104) {
12105 let mut grid: Vec<(f64, f64, DeviceColor, Vec<f64>)> = Vec::with_capacity((n + 1) * (n + 1));
12109 let use_tensor = patch.points.len() >= 16;
12110 let has_raw = !patch.raw_colors[0].is_empty();
12111 let use_icc_interp = has_raw && icc_profile_hash.is_some() && icc_cache.is_some();
12113
12114 for row in 0..=n {
12115 let v = row as f64 / n as f64;
12116 for col in 0..=n {
12117 let u = col as f64 / n as f64;
12118 let (x, y) = if use_tensor {
12119 eval_tensor_patch(patch, u, v)
12120 } else {
12121 eval_coons_patch(patch, u, v)
12122 };
12123 let raw = if has_raw {
12124 bilinear_raw(&patch.raw_colors, u, v)
12125 } else {
12126 vec![]
12127 };
12128 let color = if use_icc_interp {
12134 if let Some((r, g, b)) = icc_cache
12135 .unwrap()
12136 .convert_color_readonly(icc_profile_hash.unwrap(), &raw)
12137 {
12138 DeviceColor::from_rgb(r, g, b)
12139 } else {
12140 bilinear_color(&patch.colors, u, v)
12141 }
12142 } else {
12143 bilinear_color(&patch.colors, u, v)
12144 };
12145 grid.push((x, y, color, raw));
12146 }
12147 }
12148
12149 let cols = n + 1;
12151 for row in 0..n {
12152 for col in 0..n {
12153 let i00 = row * cols + col;
12154 let i10 = i00 + 1;
12155 let i01 = i00 + cols;
12156 let i11 = i01 + 1;
12157
12158 let (x00, y00, c00, r00) = &grid[i00];
12159 let (x10, y10, c10, r10) = &grid[i10];
12160 let (x01, y01, c01, r01) = &grid[i01];
12161 let (x11, y11, c11, r11) = &grid[i11];
12162
12163 use stet_graphics::device::ShadingVertex;
12164 triangles.push(stet_graphics::device::ShadingTriangle {
12165 v0: ShadingVertex {
12166 x: *x00,
12167 y: *y00,
12168 color: c00.clone(),
12169 raw_components: r00.clone(),
12170 },
12171 v1: ShadingVertex {
12172 x: *x10,
12173 y: *y10,
12174 color: c10.clone(),
12175 raw_components: r10.clone(),
12176 },
12177 v2: ShadingVertex {
12178 x: *x01,
12179 y: *y01,
12180 color: c01.clone(),
12181 raw_components: r01.clone(),
12182 },
12183 });
12184 triangles.push(stet_graphics::device::ShadingTriangle {
12185 v0: ShadingVertex {
12186 x: *x10,
12187 y: *y10,
12188 color: c10.clone(),
12189 raw_components: r10.clone(),
12190 },
12191 v1: ShadingVertex {
12192 x: *x11,
12193 y: *y11,
12194 color: c11.clone(),
12195 raw_components: r11.clone(),
12196 },
12197 v2: ShadingVertex {
12198 x: *x01,
12199 y: *y01,
12200 color: c01.clone(),
12201 raw_components: r01.clone(),
12202 },
12203 });
12204 }
12205 }
12206}
12207
12208fn eval_coons_patch(patch: &stet_graphics::device::ShadingPatch, u: f64, v: f64) -> (f64, f64) {
12211 let pts = &patch.points;
12212 if pts.len() < 12 {
12213 return (0.0, 0.0);
12214 }
12215
12216 let c0 = eval_cubic_bezier(pts[0], pts[1], pts[2], pts[3], u);
12221 let c2 = eval_cubic_bezier(pts[6], pts[7], pts[8], pts[9], 1.0 - u);
12222 let d0 = eval_cubic_bezier(pts[0], pts[11], pts[10], pts[9], v);
12223 let d1 = eval_cubic_bezier(pts[3], pts[4], pts[5], pts[6], v);
12224
12225 let p00 = pts[0];
12227 let p10 = pts[3];
12228 let p01 = pts[9];
12229 let p11 = pts[6];
12230 let bx = (1.0 - u) * (1.0 - v) * p00.0
12231 + u * (1.0 - v) * p10.0
12232 + (1.0 - u) * v * p01.0
12233 + u * v * p11.0;
12234 let by = (1.0 - u) * (1.0 - v) * p00.1
12235 + u * (1.0 - v) * p10.1
12236 + (1.0 - u) * v * p01.1
12237 + u * v * p11.1;
12238
12239 let x = (1.0 - v) * c0.0 + v * c2.0 + (1.0 - u) * d0.0 + u * d1.0 - bx;
12241 let y = (1.0 - v) * c0.1 + v * c2.1 + (1.0 - u) * d0.1 + u * d1.1 - by;
12242
12243 (x, y)
12244}
12245
12246fn eval_tensor_patch(patch: &stet_graphics::device::ShadingPatch, u: f64, v: f64) -> (f64, f64) {
12260 let pts = &patch.points;
12261
12262 let grid: [[usize; 4]; 4] = [[0, 1, 2, 3], [11, 12, 13, 4], [10, 15, 14, 5], [9, 8, 7, 6]];
12266
12267 let su = 1.0 - u;
12269 let bu = [su * su * su, 3.0 * su * su * u, 3.0 * su * u * u, u * u * u];
12270 let sv = 1.0 - v;
12271 let bv = [sv * sv * sv, 3.0 * sv * sv * v, 3.0 * sv * v * v, v * v * v];
12272
12273 let mut x = 0.0;
12274 let mut y = 0.0;
12275 for j in 0..4 {
12276 for i in 0..4 {
12277 let w = bu[i] * bv[j];
12278 let p = pts[grid[j][i]];
12279 x += w * p.0;
12280 y += w * p.1;
12281 }
12282 }
12283 (x, y)
12284}
12285
12286fn eval_cubic_bezier(
12288 p0: (f64, f64),
12289 p1: (f64, f64),
12290 p2: (f64, f64),
12291 p3: (f64, f64),
12292 t: f64,
12293) -> (f64, f64) {
12294 let s = 1.0 - t;
12295 let s2 = s * s;
12296 let t2 = t * t;
12297 let b0 = s2 * s;
12298 let b1 = 3.0 * s2 * t;
12299 let b2 = 3.0 * s * t2;
12300 let b3 = t2 * t;
12301 (
12302 b0 * p0.0 + b1 * p1.0 + b2 * p2.0 + b3 * p3.0,
12303 b0 * p0.1 + b1 * p1.1 + b2 * p2.1 + b3 * p3.1,
12304 )
12305}
12306
12307fn bilinear_color(colors: &[DeviceColor; 4], u: f64, v: f64) -> DeviceColor {
12309 let r = (1.0 - u) * (1.0 - v) * colors[0].r
12310 + u * (1.0 - v) * colors[1].r
12311 + (1.0 - u) * v * colors[3].r
12312 + u * v * colors[2].r;
12313 let g = (1.0 - u) * (1.0 - v) * colors[0].g
12314 + u * (1.0 - v) * colors[1].g
12315 + (1.0 - u) * v * colors[3].g
12316 + u * v * colors[2].g;
12317 let b = (1.0 - u) * (1.0 - v) * colors[0].b
12318 + u * (1.0 - v) * colors[1].b
12319 + (1.0 - u) * v * colors[3].b
12320 + u * v * colors[2].b;
12321 DeviceColor::from_rgb(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0))
12322}
12323
12324fn bilinear_raw(raw_colors: &[Vec<f64>; 4], u: f64, v: f64) -> Vec<f64> {
12326 let n = raw_colors[0].len();
12327 let mut result = vec![0.0; n];
12328 for i in 0..n {
12329 result[i] = (1.0 - u) * (1.0 - v) * raw_colors[0][i]
12330 + u * (1.0 - v) * raw_colors[1][i]
12331 + (1.0 - u) * v * raw_colors[3][i]
12332 + u * v * raw_colors[2][i];
12333 }
12334 result
12335}
12336
12337fn build_gradient_lut(stops: &[stet_graphics::device::ColorStop], size: usize) -> Vec<[u8; 4]> {
12343 let size = size.max(2);
12344 let mut lut = vec![[0u8; 4]; size];
12345 if stops.is_empty() {
12346 return lut;
12347 }
12348 let mut si = 0usize; let last = (size - 1) as f64;
12350 for i in 0..size {
12351 let t = i as f64 / last;
12352 while si + 1 < stops.len() && stops[si + 1].position < t {
12354 si += 1;
12355 }
12356 let (r, g, b) = if si + 1 >= stops.len() {
12357 let c = &stops[stops.len() - 1].color;
12358 (c.r, c.g, c.b)
12359 } else if t <= stops[si].position {
12360 let c = &stops[si].color;
12361 (c.r, c.g, c.b)
12362 } else {
12363 let t0 = stops[si].position;
12364 let t1 = stops[si + 1].position;
12365 let frac = if (t1 - t0).abs() < 1e-10 {
12366 0.0
12367 } else {
12368 (t - t0) / (t1 - t0)
12369 };
12370 let c0 = &stops[si].color;
12371 let c1 = &stops[si + 1].color;
12372 (
12373 c0.r + frac * (c1.r - c0.r),
12374 c0.g + frac * (c1.g - c0.g),
12375 c0.b + frac * (c1.b - c0.b),
12376 )
12377 };
12378 lut[i] = [
12379 (r * 255.0).round().clamp(0.0, 255.0) as u8,
12380 (g * 255.0).round().clamp(0.0, 255.0) as u8,
12381 (b * 255.0).round().clamp(0.0, 255.0) as u8,
12382 255,
12383 ];
12384 }
12385 lut
12386}
12387
12388fn build_gradient_stops(
12390 stops: &[stet_graphics::device::ColorStop],
12391) -> Vec<stet_tiny_skia::GradientStop> {
12392 let mut result = Vec::with_capacity(stops.len());
12393 for stop in stops {
12394 let r = (stop.color.r * 255.0).round().clamp(0.0, 255.0) as u8;
12395 let g = (stop.color.g * 255.0).round().clamp(0.0, 255.0) as u8;
12396 let b = (stop.color.b * 255.0).round().clamp(0.0, 255.0) as u8;
12397 result.push(stet_tiny_skia::GradientStop::new(
12398 stop.position as f32,
12399 Color::from_rgba8(r, g, b, 255),
12400 ));
12401 }
12402 result
12403}
12404
12405fn interpolate_color_stops(
12407 stops: &[stet_graphics::device::ColorStop],
12408 position: f64,
12409) -> DeviceColor {
12410 if stops.is_empty() {
12411 return DeviceColor::from_gray(0.0);
12412 }
12413 if stops.len() == 1 || position <= stops[0].position {
12414 return stops[0].color.clone();
12415 }
12416 if position >= stops.last().unwrap().position {
12417 return stops.last().unwrap().color.clone();
12418 }
12419
12420 for i in 1..stops.len() {
12422 if position <= stops[i].position {
12423 let t0 = stops[i - 1].position;
12424 let t1 = stops[i].position;
12425 let frac = if (t1 - t0).abs() < 1e-10 {
12426 0.0
12427 } else {
12428 (position - t0) / (t1 - t0)
12429 };
12430 let c0 = &stops[i - 1].color;
12431 let c1 = &stops[i].color;
12432 return DeviceColor::from_rgb(
12433 (c0.r + frac * (c1.r - c0.r)).clamp(0.0, 1.0),
12434 (c0.g + frac * (c1.g - c0.g)).clamp(0.0, 1.0),
12435 (c0.b + frac * (c1.b - c0.b)).clamp(0.0, 1.0),
12436 );
12437 }
12438 }
12439
12440 stops.last().unwrap().color.clone()
12441}
12442
12443fn interpolate_cmyk_from_stops(
12455 stops: &[stet_graphics::device::ColorStop],
12456 cs: &ShadingColorSpace,
12457 t: f64,
12458 color: &DeviceColor,
12459 icc: Option<&IccCache>,
12460) -> (f64, f64, f64, f64) {
12461 let rgb_to_cmyk = |c: &DeviceColor| -> (f64, f64, f64, f64) {
12462 if let Some(cmyk) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(c.r, c.g, c.b)) {
12463 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
12464 } else {
12465 (
12466 (1.0 - c.r).clamp(0.0, 1.0),
12467 (1.0 - c.g).clamp(0.0, 1.0),
12468 (1.0 - c.b).clamp(0.0, 1.0),
12469 0.0,
12470 )
12471 }
12472 };
12473
12474 match cs {
12475 ShadingColorSpace::DeviceCMYK => {
12476 if stops.len() == 1 {
12478 let rc = &stops[0].raw_components;
12479 if rc.len() >= 4 {
12480 return (rc[0], rc[1], rc[2], rc[3]);
12481 }
12482 }
12483 let mut lo = &stops[0];
12485 let mut hi = stops.last().unwrap();
12486 for i in 0..stops.len() - 1 {
12487 if stops[i + 1].position >= t {
12488 lo = &stops[i];
12489 hi = &stops[i + 1];
12490 break;
12491 }
12492 }
12493 let span = hi.position - lo.position;
12494 let frac = if span > 1e-10 {
12495 (t - lo.position) / span
12496 } else {
12497 0.0
12498 };
12499 let frac = frac.clamp(0.0, 1.0);
12500 if lo.raw_components.len() >= 4 && hi.raw_components.len() >= 4 {
12501 (
12502 lo.raw_components[0] + frac * (hi.raw_components[0] - lo.raw_components[0]),
12503 lo.raw_components[1] + frac * (hi.raw_components[1] - lo.raw_components[1]),
12504 lo.raw_components[2] + frac * (hi.raw_components[2] - lo.raw_components[2]),
12505 lo.raw_components[3] + frac * (hi.raw_components[3] - lo.raw_components[3]),
12506 )
12507 } else {
12508 rgb_to_cmyk(color)
12509 }
12510 }
12511 _ => rgb_to_cmyk(color),
12512 }
12513}
12514
12515#[allow(clippy::too_many_arguments)]
12521fn interpolate_cmyk_from_vertices(
12522 v0: &ShadingVertex,
12523 v1: &ShadingVertex,
12524 v2: &ShadingVertex,
12525 w0: f64,
12526 w1: f64,
12527 w2: f64,
12528 cs: &ShadingColorSpace,
12529 r: f64,
12530 g: f64,
12531 b: f64,
12532 icc: Option<&IccCache>,
12533) -> (f64, f64, f64, f64) {
12534 let rgb_to_cmyk = |r: f64, g: f64, b: f64| -> (f64, f64, f64, f64) {
12535 if let Some(cmyk) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(r, g, b)) {
12536 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
12537 } else {
12538 (
12539 (1.0 - r).clamp(0.0, 1.0),
12540 (1.0 - g).clamp(0.0, 1.0),
12541 (1.0 - b).clamp(0.0, 1.0),
12542 0.0,
12543 )
12544 }
12545 };
12546
12547 match cs {
12548 ShadingColorSpace::DeviceCMYK => {
12549 if v0.raw_components.len() >= 4
12550 && v1.raw_components.len() >= 4
12551 && v2.raw_components.len() >= 4
12552 {
12553 (
12554 w0 * v0.raw_components[0]
12555 + w1 * v1.raw_components[0]
12556 + w2 * v2.raw_components[0],
12557 w0 * v0.raw_components[1]
12558 + w1 * v1.raw_components[1]
12559 + w2 * v2.raw_components[1],
12560 w0 * v0.raw_components[2]
12561 + w1 * v1.raw_components[2]
12562 + w2 * v2.raw_components[2],
12563 w0 * v0.raw_components[3]
12564 + w1 * v1.raw_components[3]
12565 + w2 * v2.raw_components[3],
12566 )
12567 } else {
12568 rgb_to_cmyk(r, g, b)
12569 }
12570 }
12571 _ => rgb_to_cmyk(r, g, b),
12572 }
12573}
12574
12575#[cfg(test)]
12576mod tests {
12577 use super::*;
12578 use stet_graphics::color::DashPattern;
12579 use stet_graphics::device::{BgUcrState, HalftoneState, TransferState};
12580
12581 #[test]
12582 fn test_create_device() {
12583 let dev = SkiaDevice::new(100, 100);
12584 assert_eq!(dev.page_size(), (100, 100));
12585 }
12586
12587 #[test]
12588 fn test_fill_rect() {
12589 let mut dev = SkiaDevice::new(100, 100);
12590 let mut path = PsPath::new();
12591 path.segments.push(PathSegment::MoveTo(10.0, 10.0));
12592 path.segments.push(PathSegment::LineTo(90.0, 10.0));
12593 path.segments.push(PathSegment::LineTo(90.0, 90.0));
12594 path.segments.push(PathSegment::LineTo(10.0, 90.0));
12595 path.segments.push(PathSegment::ClosePath);
12596
12597 let params = FillParams {
12598 color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
12599 fill_rule: FillRule::NonZeroWinding,
12600 ctm: Matrix::identity(),
12601 is_text_glyph: false,
12602 overprint: false,
12603 overprint_mode: 0,
12604 opm_paired: false,
12605 painted_channels: 0,
12606 is_device_cmyk: false,
12607 spot_color: None,
12608 rendering_intent: 0,
12609 transfer: TransferState::default(),
12610 halftone: HalftoneState::default(),
12611 bg_ucr: BgUcrState::default(),
12612 alpha: 1.0,
12613 blend_mode: 0,
12614 alpha_is_shape: false,
12615 };
12616 dev.fill_path(&path, ¶ms);
12617
12618 let pixel = dev.pixmap().pixel(50, 50).unwrap();
12620 assert_eq!(pixel.red(), 255);
12621 assert_eq!(pixel.green(), 0);
12622 assert_eq!(pixel.blue(), 0);
12623 }
12624
12625 #[test]
12626 fn test_stroke_line() {
12627 let mut dev = SkiaDevice::new(100, 100);
12628 let mut path = PsPath::new();
12629 path.segments.push(PathSegment::MoveTo(10.0, 50.0));
12630 path.segments.push(PathSegment::LineTo(90.0, 50.0));
12631
12632 let params = StrokeParams {
12633 color: DeviceColor::from_rgb(0.0, 0.0, 1.0),
12634 line_width: 4.0,
12635 line_cap: LineCap::Butt,
12636 line_join: LineJoin::Miter,
12637 miter_limit: 10.0,
12638 dash_pattern: DashPattern::solid(),
12639 ctm: Matrix::identity(),
12640 stroke_adjust: false,
12641 is_text_glyph: false,
12642 overprint: false,
12643 overprint_mode: 0,
12644 opm_paired: false,
12645 painted_channels: 0,
12646 is_device_cmyk: false,
12647 spot_color: None,
12648 rendering_intent: 0,
12649 transfer: TransferState::default(),
12650 halftone: HalftoneState::default(),
12651 bg_ucr: BgUcrState::default(),
12652 alpha: 1.0,
12653 blend_mode: 0,
12654 alpha_is_shape: false,
12655 };
12656 dev.stroke_path(&path, ¶ms);
12657
12658 let pixel = dev.pixmap().pixel(50, 50).unwrap();
12660 assert_eq!(pixel.blue(), 255);
12661 }
12662
12663 #[test]
12664 fn test_clip() {
12665 let mut dev = SkiaDevice::new(100, 100);
12666
12667 let mut clip_path = PsPath::new();
12669 clip_path.segments.push(PathSegment::MoveTo(0.0, 0.0));
12670 clip_path.segments.push(PathSegment::LineTo(50.0, 0.0));
12671 clip_path.segments.push(PathSegment::LineTo(50.0, 100.0));
12672 clip_path.segments.push(PathSegment::LineTo(0.0, 100.0));
12673 clip_path.segments.push(PathSegment::ClosePath);
12674
12675 let clip_params = ClipParams {
12676 fill_rule: FillRule::NonZeroWinding,
12677 ctm: Matrix::identity(),
12678 stroke_params: None,
12679 };
12680 dev.clip_path(&clip_path, &clip_params);
12681
12682 let mut fill_path = PsPath::new();
12684 fill_path.segments.push(PathSegment::MoveTo(0.0, 0.0));
12685 fill_path.segments.push(PathSegment::LineTo(100.0, 0.0));
12686 fill_path.segments.push(PathSegment::LineTo(100.0, 100.0));
12687 fill_path.segments.push(PathSegment::LineTo(0.0, 100.0));
12688 fill_path.segments.push(PathSegment::ClosePath);
12689
12690 let fill_params = FillParams {
12691 color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
12692 fill_rule: FillRule::NonZeroWinding,
12693 ctm: Matrix::identity(),
12694 is_text_glyph: false,
12695 overprint: false,
12696 overprint_mode: 0,
12697 opm_paired: false,
12698 painted_channels: 0,
12699 is_device_cmyk: false,
12700 spot_color: None,
12701 rendering_intent: 0,
12702 transfer: TransferState::default(),
12703 halftone: HalftoneState::default(),
12704 bg_ucr: BgUcrState::default(),
12705 alpha: 1.0,
12706 blend_mode: 0,
12707 alpha_is_shape: false,
12708 };
12709 dev.fill_path(&fill_path, &fill_params);
12710
12711 let left_pixel = dev.pixmap().pixel(25, 50).unwrap();
12713 assert_eq!(left_pixel.red(), 255);
12714
12715 let right_pixel = dev.pixmap().pixel(75, 50).unwrap();
12717 assert_eq!(right_pixel.red(), 255);
12718 assert_eq!(right_pixel.green(), 255); }
12720
12721 #[test]
12722 fn test_erase_page() {
12723 let mut dev = SkiaDevice::new(100, 100);
12724 let mut path = PsPath::new();
12726 path.segments.push(PathSegment::MoveTo(0.0, 0.0));
12727 path.segments.push(PathSegment::LineTo(100.0, 0.0));
12728 path.segments.push(PathSegment::LineTo(100.0, 100.0));
12729 path.segments.push(PathSegment::LineTo(0.0, 100.0));
12730 path.segments.push(PathSegment::ClosePath);
12731 let params = FillParams {
12732 color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
12733 fill_rule: FillRule::NonZeroWinding,
12734 ctm: Matrix::identity(),
12735 is_text_glyph: false,
12736 overprint: false,
12737 overprint_mode: 0,
12738 opm_paired: false,
12739 painted_channels: 0,
12740 is_device_cmyk: false,
12741 spot_color: None,
12742 rendering_intent: 0,
12743 transfer: TransferState::default(),
12744 halftone: HalftoneState::default(),
12745 bg_ucr: BgUcrState::default(),
12746 alpha: 1.0,
12747 blend_mode: 0,
12748 alpha_is_shape: false,
12749 };
12750 dev.fill_path(&path, ¶ms);
12751
12752 dev.erase_page();
12753
12754 let pixel = dev.pixmap().pixel(50, 50).unwrap();
12756 assert_eq!(pixel.red(), 255);
12757 assert_eq!(pixel.green(), 255);
12758 assert_eq!(pixel.blue(), 255);
12759 }
12760
12761 #[test]
12762 fn test_show_page() {
12763 let mut dev = SkiaDevice::new(10, 10);
12764 let path = std::env::temp_dir().join("stet_test_output.png");
12765 let path_str = path.to_string_lossy();
12766 let result = dev.show_page(&path_str);
12767 assert!(result.is_ok());
12768 assert!(path.exists());
12769 std::fs::remove_file(&path).ok();
12770 }
12771
12772 #[test]
12773 fn test_transform() {
12774 let mut dev = SkiaDevice::new(200, 200);
12775 let mut path = PsPath::new();
12777 path.segments.push(PathSegment::MoveTo(0.0, 0.0));
12778 path.segments.push(PathSegment::LineTo(10.0, 0.0));
12779 path.segments.push(PathSegment::LineTo(10.0, 10.0));
12780 path.segments.push(PathSegment::LineTo(0.0, 10.0));
12781 path.segments.push(PathSegment::ClosePath);
12782
12783 let params = FillParams {
12784 color: DeviceColor::from_rgb(0.0, 1.0, 0.0),
12785 fill_rule: FillRule::NonZeroWinding,
12786 ctm: Matrix::translate(100.0, 100.0),
12787 is_text_glyph: false,
12788 overprint: false,
12789 overprint_mode: 0,
12790 opm_paired: false,
12791 painted_channels: 0,
12792 is_device_cmyk: false,
12793 spot_color: None,
12794 rendering_intent: 0,
12795 transfer: TransferState::default(),
12796 halftone: HalftoneState::default(),
12797 bg_ucr: BgUcrState::default(),
12798 alpha: 1.0,
12799 blend_mode: 0,
12800 alpha_is_shape: false,
12801 };
12802 dev.fill_path(&path, ¶ms);
12803
12804 let pixel = dev.pixmap().pixel(105, 105).unwrap();
12806 assert_eq!(pixel.green(), 255);
12807 assert_eq!(pixel.red(), 0);
12808 }
12809
12810 fn make_test_fill_at(x: f64, y: f64, w: f64, h: f64) -> DisplayElement {
12811 let mut path = PsPath::new();
12812 path.segments.push(PathSegment::MoveTo(x, y));
12813 path.segments.push(PathSegment::LineTo(x + w, y));
12814 path.segments.push(PathSegment::LineTo(x + w, y + h));
12815 path.segments.push(PathSegment::LineTo(x, y + h));
12816 path.segments.push(PathSegment::ClosePath);
12817 DisplayElement::Fill {
12818 path,
12819 params: FillParams {
12820 color: DeviceColor::from_rgb(0.0, 0.0, 0.0),
12821 fill_rule: FillRule::NonZeroWinding,
12822 ctm: Matrix::identity(),
12823 is_text_glyph: false,
12824 overprint: false,
12825 overprint_mode: 0,
12826 opm_paired: false,
12827 painted_channels: 0,
12828 is_device_cmyk: false,
12829 spot_color: None,
12830 rendering_intent: 0,
12831 transfer: TransferState::default(),
12832 halftone: HalftoneState::default(),
12833 bg_ucr: BgUcrState::default(),
12834 alpha: 1.0,
12835 blend_mode: 0,
12836 alpha_is_shape: false,
12837 },
12838 }
12839 }
12840
12841 #[test]
12842 fn test_compute_paint_bounds_two_fills() {
12843 let mut list = DisplayList::new();
12844 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");
12848 assert!(
12849 (bounds.x_min - 10.0).abs() < 1e-9,
12850 "x_min was {}",
12851 bounds.x_min
12852 );
12853 assert!(
12854 (bounds.y_min - 20.0).abs() < 1e-9,
12855 "y_min was {}",
12856 bounds.y_min
12857 );
12858 assert!(
12859 (bounds.x_max - 150.0).abs() < 1e-9,
12860 "x_max was {}",
12861 bounds.x_max
12862 );
12863 assert!(
12864 (bounds.y_max - 75.0).abs() < 1e-9,
12865 "y_max was {}",
12866 bounds.y_max
12867 );
12868 }
12869
12870 #[test]
12871 fn test_compute_paint_bounds_empty_list() {
12872 let list = DisplayList::new();
12873 assert!(compute_paint_bounds(&list, 72.0).is_none());
12874 }
12875
12876 #[test]
12877 fn test_compute_paint_bounds_only_clip_returns_none() {
12878 let mut list = DisplayList::new();
12879 list.push(DisplayElement::InitClip);
12880 assert!(compute_paint_bounds(&list, 72.0).is_none());
12883 }
12884
12885 #[test]
12886 fn test_rasterize_mask_anchors_to_paint_bounds() {
12887 use stet_graphics::display_list::{SoftMaskParams, SoftMaskSubtype};
12888
12889 let mut mask = DisplayList::new();
12892 let mut path = PsPath::new();
12893 path.segments.push(PathSegment::MoveTo(200.0, 300.0));
12894 path.segments.push(PathSegment::LineTo(250.0, 300.0));
12895 path.segments.push(PathSegment::LineTo(250.0, 340.0));
12896 path.segments.push(PathSegment::LineTo(200.0, 340.0));
12897 path.segments.push(PathSegment::ClosePath);
12898 mask.push(DisplayElement::Fill {
12899 path,
12900 params: FillParams {
12901 color: DeviceColor::from_rgb(1.0, 1.0, 1.0),
12902 fill_rule: FillRule::NonZeroWinding,
12903 ctm: Matrix::identity(),
12904 is_text_glyph: false,
12905 overprint: false,
12906 overprint_mode: 0,
12907 opm_paired: false,
12908 painted_channels: 0,
12909 is_device_cmyk: false,
12910 spot_color: None,
12911 rendering_intent: 0,
12912 transfer: TransferState::default(),
12913 halftone: HalftoneState::default(),
12914 bg_ucr: BgUcrState::default(),
12915 alpha: 1.0,
12916 blend_mode: 0,
12917 alpha_is_shape: false,
12918 },
12919 });
12920
12921 let params = SoftMaskParams {
12922 subtype: SoftMaskSubtype::Luminosity,
12923 bbox: [0.0, 0.0, 100.0, 100.0],
12926 backdrop_color: None, transfer_invert: false,
12928 has_nested_mask_scope: false,
12929 parent_clip_bbox: None,
12930 };
12931
12932 let raster = rasterize_mask(
12933 &mask,
12934 ¶ms,
12935 None,
12936 false,
12937 72.0,
12938 1.0,
12939 1.0,
12940 &LayerSet::new(),
12941 )
12942 .expect("expected raster");
12943
12944 assert_eq!(raster.origin_x, 199);
12947 assert_eq!(raster.origin_y, 299);
12948 assert_eq!(raster.width, 52);
12950 assert_eq!(raster.height, 42);
12951 assert_eq!(raster.scale_x, 1.0);
12952 assert_eq!(raster.scale_y, 1.0);
12953
12954 let mx = 225 - raster.origin_x;
12958 let my = 320 - raster.origin_y;
12959 assert!(mx >= 0 && (mx as u32) < raster.width);
12960 assert!(my >= 0 && (my as u32) < raster.height);
12961 let center_value = raster.data[(my as usize) * raster.width as usize + mx as usize];
12962 assert_eq!(
12963 center_value, 255,
12964 "center of painted mask should be opaque white (lum=255)"
12965 );
12966
12967 let mx_out = 300 - raster.origin_x;
12971 let in_bounds = mx_out >= 0 && (mx_out as u32) < raster.width;
12972 assert!(!in_bounds, "page x=300 should be outside the mask raster");
12973 assert_eq!(
12974 out_of_bounds_mask_value(¶ms),
12975 0,
12976 "black backdrop → out-of-bounds = 0"
12977 );
12978 }
12979
12980 #[test]
12981 fn test_band_local_to_mask_formula() {
12982 let raster_origin_x = 200i32;
12993 let raster_origin_y = 300i32;
12994
12995 let sample = |vp_x_dev: f32,
12997 vp_y_dev: f32,
12998 scale: f32,
12999 crop_x: i32,
13000 crop_y: i32,
13001 x: i32,
13002 y: i32|
13003 -> (i32, i32) {
13004 let vp_x_pixels = (vp_x_dev * scale).round() as i32;
13005 let vp_y_pixels = (vp_y_dev * scale).round() as i32;
13006 let page_x = vp_x_pixels + crop_x + x;
13007 let page_y = vp_y_pixels + crop_y + y;
13008 let mx = page_x - raster_origin_x;
13009 let my = page_y - raster_origin_y;
13010 (mx, my)
13011 };
13012
13013 let (mx, my) = sample(0.0, 0.0, 1.0, 220, 310, 0, 0);
13018 assert_eq!((mx, my), (20, 10), "top band: smask top-left");
13019
13020 let (mx, my) = sample(0.0, 0.0, 1.0, 220, 310, 5, 5);
13022 assert_eq!((mx, my), (25, 15), "top band: 5px into smask");
13023
13024 let (mx, my) = sample(0.0, 305.0, 1.0, 220, 5, 0, 0);
13032 assert_eq!((mx, my), (20, 10), "mid band: smask top-left");
13033
13034 let vp_x_pixels = (100.0_f32 * 2.0).round() as i32;
13046 let crop_x = ((220.0_f32 - 100.0) * 2.0).floor() as i32;
13047 let page_x_for_x_zero = vp_x_pixels + crop_x;
13048 assert_eq!(page_x_for_x_zero, 440, "viewport scale-2: page-x at x=0");
13049 }
13050
13051 fn x_path() -> PsPath {
13054 let mut p = PsPath::new();
13055 p.segments.push(PathSegment::MoveTo(10.0, 10.0));
13056 p.segments.push(PathSegment::LineTo(20.0, 20.0));
13057 p.segments.push(PathSegment::LineTo(30.0, 10.0));
13058 p.segments.push(PathSegment::LineTo(20.0, 0.0));
13059 p.segments.push(PathSegment::ClosePath);
13060 p
13061 }
13062
13063 fn x_path_perturbed() -> PsPath {
13064 let mut p = PsPath::new();
13067 p.segments.push(PathSegment::MoveTo(10.001, 10.0));
13068 p.segments.push(PathSegment::LineTo(20.0, 19.999));
13069 p.segments.push(PathSegment::LineTo(30.002, 10.001));
13070 p.segments.push(PathSegment::LineTo(19.999, 0.0));
13071 p.segments.push(PathSegment::ClosePath);
13072 p
13073 }
13074
13075 fn fill(path: PsPath, alpha: f64, blend: u8) -> DisplayElement {
13076 DisplayElement::Fill {
13077 path,
13078 params: FillParams {
13079 color: DeviceColor::from_rgb(0.0, 0.0, 0.0),
13080 fill_rule: FillRule::NonZeroWinding,
13081 ctm: Matrix::identity(),
13082 is_text_glyph: false,
13083 overprint: false,
13084 overprint_mode: 0,
13085 opm_paired: false,
13086 painted_channels: 0,
13087 is_device_cmyk: false,
13088 spot_color: None,
13089 rendering_intent: 0,
13090 transfer: TransferState::default(),
13091 halftone: HalftoneState::default(),
13092 bg_ucr: BgUcrState::default(),
13093 alpha,
13094 blend_mode: blend,
13095 alpha_is_shape: false,
13096 },
13097 }
13098 }
13099
13100 fn rect_path(x0: f64, y0: f64, x1: f64, y1: f64) -> PsPath {
13101 let mut p = PsPath::new();
13102 p.segments.push(PathSegment::MoveTo(x0, y0));
13103 p.segments.push(PathSegment::LineTo(x1, y0));
13104 p.segments.push(PathSegment::LineTo(x1, y1));
13105 p.segments.push(PathSegment::LineTo(x0, y1));
13106 p.segments.push(PathSegment::ClosePath);
13107 p
13108 }
13109
13110 fn clip_elem(path: PsPath) -> DisplayElement {
13111 DisplayElement::Clip {
13112 path,
13113 params: ClipParams {
13114 fill_rule: FillRule::NonZeroWinding,
13115 ctm: Matrix::identity(),
13116 stroke_params: None,
13117 },
13118 }
13119 }
13120
13121 fn group_elem(
13122 inner: Vec<DisplayElement>,
13123 bbox: [f64; 4],
13124 isolated: bool,
13125 alpha: f64,
13126 blend: u8,
13127 ) -> DisplayElement {
13128 let mut dl = DisplayList::new();
13129 for e in inner {
13130 dl.push(e);
13131 }
13132 DisplayElement::Group {
13133 elements: dl,
13134 params: stet_graphics::display_list::GroupParams {
13135 bbox,
13136 isolated,
13137 knockout: false,
13138 blend_mode: blend,
13139 alpha,
13140 color_space: stet_graphics::display_list::GroupColorSpace::Inherited,
13141 },
13142 }
13143 }
13144
13145 fn dl(elements: Vec<DisplayElement>) -> DisplayList {
13146 let mut d = DisplayList::new();
13147 for e in elements {
13148 d.push(e);
13149 }
13150 d
13151 }
13152
13153 #[test]
13154 fn obscured_skip_fires_on_matching_fill_plus_iso_group() {
13155 let parent = fill(x_path(), 1.0, 0);
13158 let inner = vec![fill(x_path_perturbed(), 1.0, 0)];
13159 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13160 let d = dl(vec![
13161 parent,
13162 clip_elem(rect_path(0.0, -5.0, 40.0, 30.0)),
13163 grp,
13164 ]);
13165 assert_eq!(compute_obscured_fill_skips(&d), vec![0]);
13166 }
13167
13168 #[test]
13169 fn obscured_skip_does_not_fire_on_non_isolated_group() {
13170 let parent = fill(x_path(), 1.0, 0);
13171 let inner = vec![fill(x_path(), 1.0, 0)];
13172 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], false, 1.0, 0);
13173 let d = dl(vec![parent, grp]);
13174 assert!(compute_obscured_fill_skips(&d).is_empty());
13175 }
13176
13177 #[test]
13178 fn obscured_skip_does_not_fire_on_partial_alpha_group() {
13179 let parent = fill(x_path(), 1.0, 0);
13180 let inner = vec![fill(x_path(), 1.0, 0)];
13181 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 0.5, 0);
13182 let d = dl(vec![parent, grp]);
13183 assert!(compute_obscured_fill_skips(&d).is_empty());
13184 }
13185
13186 #[test]
13187 fn obscured_skip_does_not_fire_on_non_normal_blend() {
13188 let parent = fill(x_path(), 1.0, 0);
13189 let inner = vec![fill(x_path(), 1.0, 0)];
13190 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 10);
13193 let d = dl(vec![parent, grp]);
13194 assert!(compute_obscured_fill_skips(&d).is_empty());
13195 }
13196
13197 #[test]
13198 fn obscured_skip_does_not_fire_when_paths_differ() {
13199 let parent = fill(rect_path(0.0, 0.0, 5.0, 5.0), 1.0, 0);
13200 let inner = vec![fill(x_path(), 1.0, 0)];
13201 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13202 let d = dl(vec![parent, grp]);
13203 assert!(compute_obscured_fill_skips(&d).is_empty());
13204 }
13205
13206 #[test]
13207 fn obscured_skip_does_not_fire_when_group_bbox_too_small() {
13208 let big = rect_path(0.0, 0.0, 100.0, 100.0);
13212 let parent = fill(big.clone(), 1.0, 0);
13213 let inner = vec![fill(big, 1.0, 0)];
13214 let grp = group_elem(inner, [0.0, 0.0, 10.0, 10.0], true, 1.0, 0);
13216 let d = dl(vec![parent, grp]);
13217 assert!(compute_obscured_fill_skips(&d).is_empty());
13218 }
13219
13220 #[test]
13221 fn obscured_skip_does_not_fire_when_intervening_clip_too_small() {
13222 let parent = fill(x_path(), 1.0, 0);
13226 let narrow_clip = clip_elem(rect_path(12.0, 5.0, 18.0, 15.0));
13227 let inner = vec![fill(x_path(), 1.0, 0)];
13228 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13229 let d = dl(vec![parent, narrow_clip, grp]);
13230 assert!(compute_obscured_fill_skips(&d).is_empty());
13231 }
13232
13233 #[test]
13234 fn obscured_skip_does_not_fire_when_inner_clip_too_small() {
13235 let parent = fill(x_path(), 1.0, 0);
13237 let inner = vec![
13238 clip_elem(rect_path(12.0, 5.0, 18.0, 15.0)),
13239 fill(x_path(), 1.0, 0),
13240 ];
13241 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13242 let d = dl(vec![parent, grp]);
13243 assert!(compute_obscured_fill_skips(&d).is_empty());
13244 }
13245
13246 #[test]
13247 fn obscured_skip_fires_when_inner_clip_is_wider_than_parent_path() {
13248 let parent = fill(x_path(), 1.0, 0);
13251 let inner = vec![
13252 clip_elem(rect_path(-10.0, -10.0, 40.0, 30.0)),
13253 fill(x_path_perturbed(), 1.0, 0),
13254 ];
13255 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13256 let d = dl(vec![parent, grp]);
13257 assert_eq!(compute_obscured_fill_skips(&d), vec![0]);
13258 }
13259
13260 #[test]
13261 fn obscured_skip_does_not_fire_on_partial_alpha_parent() {
13262 let parent = fill(x_path(), 0.5, 0);
13265 let inner = vec![fill(x_path(), 1.0, 0)];
13266 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13267 let d = dl(vec![parent, grp]);
13268 assert!(compute_obscured_fill_skips(&d).is_empty());
13269 }
13270}