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;
27
28#[derive(Clone, Copy)]
30struct ClipRect {
31 x0: u32,
32 y0: u32, x1: u32,
34 y1: u32, }
36
37impl ClipRect {
38 fn intersect(&self, other: &ClipRect) -> ClipRect {
40 ClipRect {
41 x0: self.x0.max(other.x0),
42 y0: self.y0.max(other.y0),
43 x1: self.x1.min(other.x1),
44 y1: self.y1.min(other.y1),
45 }
46 }
47
48 fn is_empty(&self) -> bool {
49 self.x0 >= self.x1 || self.y0 >= self.y1
50 }
51
52 fn is_full_page(&self, w: u32, h: u32) -> bool {
54 self.x0 == 0 && self.y0 == 0 && self.x1 == w && self.y1 == h
55 }
56
57 fn make_mask(self, w: u32, h: u32) -> Option<Mask> {
59 if self.is_empty() {
60 return None;
61 }
62 let mut mask = Mask::new(w, h)?;
63 let data = mask.data_mut();
64 let stride = w as usize;
65 for y in self.y0..self.y1 {
66 let row_start = y as usize * stride + self.x0 as usize;
67 let row_end = y as usize * stride + self.x1 as usize;
68 data[row_start..row_end].fill(255);
69 }
70 Some(mask)
71 }
72}
73
74enum ClipRegion {
76 Rect(ClipRect),
77 Mask(Mask),
78}
79
80pub struct SkiaDevice {
82 pixmap: Pixmap,
83 page_w: u32,
86 page_h: u32,
87 dpi: f64,
89 clip_region: Option<ClipRegion>,
90 clip_mask_cache: HashMap<u64, Mask>,
93 clip_mask_seen: HashSet<u64>,
94 spare_mask: Option<Mask>,
96 pending_render: Option<std::sync::mpsc::Receiver<Result<(), String>>>,
99 sink_factory: Box<dyn PageSinkFactory>,
101 system_cmyk_bytes: Option<std::sync::Arc<Vec<u8>>>,
103 render_icc_cache: Option<IccCache>,
105 no_aa: bool,
107 use_viewport_path: bool,
112}
113
114impl SkiaDevice {
115 pub fn new(width: u32, height: u32) -> Self {
121 Self::with_sink_factory(width, height, Box::new(crate::PngSinkFactory))
122 }
123
124 pub fn with_sink_factory(
126 width: u32,
127 height: u32,
128 sink_factory: Box<dyn PageSinkFactory>,
129 ) -> Self {
130 let dpi = height as f64 * 72.0 / 792.0;
133
134 let pixmap = Pixmap::new(1, 1).expect("Failed to create placeholder pixmap");
138 Self {
139 pixmap,
140 page_w: width,
141 page_h: height,
142 dpi,
143 clip_region: None,
144 clip_mask_cache: HashMap::new(),
145 clip_mask_seen: HashSet::new(),
146 spare_mask: None,
147 pending_render: None,
148 sink_factory,
149 system_cmyk_bytes: None,
150 render_icc_cache: None,
151 no_aa: false,
152 use_viewport_path: false,
153 }
154 }
155
156 pub fn set_use_viewport_path(&mut self, on: bool) {
159 self.use_viewport_path = on;
160 }
161
162 fn ensure_full_pixmap(&mut self) {
165 if self.pixmap.width() != self.page_w || self.pixmap.height() != self.page_h {
166 self.pixmap =
167 Pixmap::new(self.page_w, self.page_h).expect("Failed to create page pixmap");
168 self.pixmap.fill(Color::WHITE);
169 }
170 }
171
172 pub fn pixmap(&self) -> &Pixmap {
174 &self.pixmap
175 }
176
177 pub fn set_system_cmyk_bytes(&mut self, bytes: std::sync::Arc<Vec<u8>>) {
179 self.system_cmyk_bytes = Some(bytes);
180 }
181
182 pub fn set_no_aa(&mut self, no_aa: bool) {
184 self.no_aa = no_aa;
185 }
186}
187
188fn to_transform(m: &Matrix) -> Transform {
190 Transform::from_row(
191 m.a as f32,
192 m.b as f32,
193 m.c as f32,
194 m.d as f32,
195 m.tx as f32,
196 m.ty as f32,
197 )
198}
199
200fn to_paint(color: &DeviceColor) -> Paint<'static> {
202 to_paint_alpha(color, 1.0, 0, false)
203}
204
205fn to_paint_alpha(color: &DeviceColor, alpha: f64, blend_mode: u8, no_aa: bool) -> Paint<'static> {
207 let mut paint = Paint::default();
208 let a = (alpha * 255.0).round().clamp(0.0, 255.0) as u8;
209 paint.set_color_rgba8(
210 (color.r * 255.0).round().clamp(0.0, 255.0) as u8,
211 (color.g * 255.0).round().clamp(0.0, 255.0) as u8,
212 (color.b * 255.0).round().clamp(0.0, 255.0) as u8,
213 a,
214 );
215 paint.anti_alias = !no_aa;
216 paint.blend_mode = u8_to_blend_mode(blend_mode);
217 paint
218}
219
220fn u8_to_blend_mode(mode: u8) -> BlendMode {
222 match mode {
223 1 => BlendMode::Multiply,
224 2 => BlendMode::Screen,
225 3 => BlendMode::Overlay,
226 4 => BlendMode::Darken,
227 5 => BlendMode::Lighten,
228 6 => BlendMode::ColorDodge,
229 7 => BlendMode::ColorBurn,
230 8 => BlendMode::HardLight,
231 9 => BlendMode::SoftLight,
232 10 => BlendMode::Difference,
233 11 => BlendMode::Exclusion,
234 12 => BlendMode::Hue,
235 13 => BlendMode::Saturation,
236 14 => BlendMode::Color,
237 15 => BlendMode::Luminosity,
238 _ => BlendMode::SourceOver,
239 }
240}
241
242const MAX_PATH_COORD: f32 = 1e6;
248
249fn build_skia_path(path: &PsPath) -> Option<stet_tiny_skia::Path> {
250 let mut pb = PathBuilder::new();
251
252 for seg in &path.segments {
253 match seg {
254 PathSegment::MoveTo(x, y) => {
255 pb.move_to(*x as f32, *y as f32);
256 }
257 PathSegment::LineTo(x, y) => {
258 pb.line_to(*x as f32, *y as f32);
259 }
260 PathSegment::CurveTo {
261 x1,
262 y1,
263 x2,
264 y2,
265 x3,
266 y3,
267 } => {
268 pb.cubic_to(
269 *x1 as f32, *y1 as f32, *x2 as f32, *y2 as f32, *x3 as f32, *y3 as f32,
270 );
271 }
272 PathSegment::ClosePath => {
273 pb.close();
274 }
275 }
276 }
277
278 let result = pb.finish()?;
279
280 let b = result.bounds();
284 if b.left().abs() > MAX_PATH_COORD
285 || b.top().abs() > MAX_PATH_COORD
286 || b.right().abs() > MAX_PATH_COORD
287 || b.bottom().abs() > MAX_PATH_COORD
288 {
289 return None;
290 }
291
292 Some(result)
293}
294
295fn is_degenerate_fill(path: &PsPath) -> bool {
307 let mut x_min = f64::INFINITY;
308 let mut x_max = f64::NEG_INFINITY;
309 let mut y_min = f64::INFINITY;
310 let mut y_max = f64::NEG_INFINITY;
311
312 for seg in &path.segments {
313 let (x, y) = match seg {
314 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => (*x, *y),
315 PathSegment::CurveTo { .. } => return false,
317 PathSegment::ClosePath => continue,
318 };
319 x_min = x_min.min(x);
320 x_max = x_max.max(x);
321 y_min = y_min.min(y);
322 y_max = y_max.max(y);
323 }
324
325 if x_min > x_max {
326 return false; }
328
329 let w = x_max - x_min;
330 let h = y_max - y_min;
331
332 let eps = 1e-6;
336 (w < eps && h > eps) || (h < eps && w > eps)
337}
338
339fn to_fill_rule(rule: &FillRule) -> SkiaFillRule {
344 match rule {
345 FillRule::NonZeroWinding => SkiaFillRule::Winding,
346 FillRule::EvenOdd => SkiaFillRule::EvenOdd,
347 }
348}
349
350fn to_line_cap(cap: LineCap) -> SkiaLineCap {
352 match cap {
353 LineCap::Butt => SkiaLineCap::Butt,
354 LineCap::Round => SkiaLineCap::Round,
355 LineCap::Square => SkiaLineCap::Square,
356 }
357}
358
359fn to_line_join(join: LineJoin) -> SkiaLineJoin {
361 match join {
362 LineJoin::Miter => SkiaLineJoin::Miter,
363 LineJoin::Round => SkiaLineJoin::Round,
364 LineJoin::Bevel => SkiaLineJoin::Bevel,
365 }
366}
367
368fn detect_rect(path: &PsPath, page_w: u32, page_h: u32) -> Option<ClipRect> {
371 let segs = &path.segments;
372 let (move_to, lines, _has_close) = match segs.len() {
376 5 => {
377 if !matches!(segs[4], PathSegment::ClosePath) {
379 return None;
380 }
381 (&segs[0], &segs[1..4], true)
382 }
383 6 => {
384 if !matches!(segs[5], PathSegment::ClosePath) {
386 return None;
387 }
388 (&segs[0], &segs[1..5], true)
389 }
390 4 => {
391 (&segs[0], &segs[1..4], false)
393 }
394 _ => return None,
395 };
396
397 let PathSegment::MoveTo(mx, my) = move_to else {
398 return None;
399 };
400
401 let mut pts = vec![(*mx, *my)];
403 for seg in lines {
404 match seg {
405 PathSegment::LineTo(x, y) => pts.push((*x, *y)),
406 _ => return None,
407 }
408 }
409
410 if pts.len() == 5 {
412 let (fx, fy) = pts[0];
413 let (lx, ly) = pts[4];
414 if (fx - lx).abs() > 0.01 || (fy - ly).abs() > 0.01 {
415 return None;
416 }
417 pts.truncate(4);
418 }
419
420 for i in 0..4 {
422 let (x1, y1) = pts[i];
423 let (x2, y2) = pts[(i + 1) % 4];
424 let dx = (x2 - x1).abs();
425 let dy = (y2 - y1).abs();
426 if dx > 0.01 && dy > 0.01 {
427 return None; }
429 }
430
431 let min_x = pts.iter().map(|p| p.0).fold(f64::INFINITY, f64::min);
433 let min_y = pts.iter().map(|p| p.1).fold(f64::INFINITY, f64::min);
434 let max_x = pts.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max);
435 let max_y = pts.iter().map(|p| p.1).fold(f64::NEG_INFINITY, f64::max);
436
437 let x0 = (min_x.floor().max(0.0) as u32).min(page_w);
439 let y0 = (min_y.floor().max(0.0) as u32).min(page_h);
440 let x1 = (max_x.ceil().max(0.0) as u32).min(page_w);
441 let y1 = (max_y.ceil().max(0.0) as u32).min(page_h);
442
443 Some(ClipRect { x0, y0, x1, y1 })
444}
445
446fn intersect_mask_with_rect(mask: &mut Mask, rect: &ClipRect, w: u32, h: u32) {
448 let data = mask.data_mut();
449 let stride = w as usize;
450
451 if rect.y0 > 0 {
453 let end = (rect.y0 as usize * stride).min(data.len());
454 data[..end].fill(0);
455 }
456
457 if rect.y1 < h {
459 let start = (rect.y1 as usize * stride).min(data.len());
460 data[start..].fill(0);
461 }
462
463 for y in rect.y0..rect.y1.min(h) {
465 let row_start = y as usize * stride;
466 if rect.x0 > 0 {
468 let end = row_start + rect.x0 as usize;
469 data[row_start..end].fill(0);
470 }
471 if rect.x1 < w {
473 let start = row_start + rect.x1 as usize;
474 let end = row_start + stride;
475 data[start..end].fill(0);
476 }
477 }
478}
479
480fn resolve_clip_mask<'a>(
485 clip_region: &'a Option<ClipRegion>,
486 temp_mask: &'a mut Option<Mask>,
487 w: u32,
488 h: u32,
489) -> Option<Option<&'a Mask>> {
490 match clip_region {
491 None => Some(None),
492 Some(ClipRegion::Mask(m)) => Some(Some(m)),
493 Some(ClipRegion::Rect(rect)) => {
494 if rect.is_empty() {
495 return None; }
497 if rect.is_full_page(w, h) {
498 return Some(None); }
500 *temp_mask = rect.make_mask(w, h);
501 Some(temp_mask.as_ref())
502 }
503 }
504}
505
506fn hash_clip_path(path: &PsPath, fill_rule: &FillRule) -> u64 {
509 let mut hasher = std::collections::hash_map::DefaultHasher::new();
510 std::mem::discriminant(fill_rule).hash(&mut hasher);
511 for seg in &path.segments {
512 match seg {
513 PathSegment::MoveTo(x, y) => {
514 0u8.hash(&mut hasher);
515 x.to_bits().hash(&mut hasher);
516 y.to_bits().hash(&mut hasher);
517 }
518 PathSegment::LineTo(x, y) => {
519 1u8.hash(&mut hasher);
520 x.to_bits().hash(&mut hasher);
521 y.to_bits().hash(&mut hasher);
522 }
523 PathSegment::CurveTo {
524 x1,
525 y1,
526 x2,
527 y2,
528 x3,
529 y3,
530 } => {
531 2u8.hash(&mut hasher);
532 x1.to_bits().hash(&mut hasher);
533 y1.to_bits().hash(&mut hasher);
534 x2.to_bits().hash(&mut hasher);
535 y2.to_bits().hash(&mut hasher);
536 x3.to_bits().hash(&mut hasher);
537 y3.to_bits().hash(&mut hasher);
538 }
539 PathSegment::ClosePath => {
540 3u8.hash(&mut hasher);
541 }
542 }
543 }
544 hasher.finish()
545}
546
547fn intersect_masks(dst: &mut Mask, src: &Mask) {
549 let dst_data = dst.data_mut();
550 let src_data = src.data();
551 for (d, s) in dst_data.iter_mut().zip(src_data.iter()) {
552 *d = ((*d as u16 * *s as u16 + 127) / 255) as u8;
553 }
554}
555
556use stet_graphics::display_list::{DisplayElement, DisplayList};
559
560struct BandState {
562 clip_region: Option<ClipRegion>,
563 spare_mask: Option<Mask>,
564 clip_mask_cache: HashMap<u64, Mask>,
566 clip_mask_seen: HashSet<u64>,
568 mask_pool: Vec<Mask>,
570 cmyk_buffer: Option<Vec<f32>>,
574 op_bg_snapshot: Option<Vec<u8>>,
581 op_touched: Option<Vec<u8>>,
586 spot_mask: Option<Vec<u8>>,
594}
595
596const MAX_POOL_MASKS: usize = 8;
599
600impl BandState {
601 #[allow(dead_code)]
603 fn recycle_cache(&mut self) {
604 for (_, mask) in self.clip_mask_cache.drain() {
605 if self.mask_pool.len() < MAX_POOL_MASKS {
606 self.mask_pool.push(mask);
607 }
608 }
610 }
611
612 fn recycle_mask(&mut self, mask: Mask) {
614 if self.mask_pool.len() < MAX_POOL_MASKS {
615 self.mask_pool.push(mask);
616 }
617 }
618
619 fn take_mask(&mut self, w: u32, h: u32) -> Mask {
621 self.spare_mask
622 .take()
623 .or_else(|| self.mask_pool.pop())
624 .unwrap_or_else(|| Mask::new(w, h).expect("Failed to create mask"))
625 }
626
627 fn take_op_buffers(&mut self, w: u32, h: u32) -> (Vec<u8>, Vec<u8>) {
632 let n = w as usize * h as usize;
633 let bg = self
634 .op_bg_snapshot
635 .take()
636 .unwrap_or_else(|| vec![0u8; n * 4]);
637 let touched = self.op_touched.take().unwrap_or_else(|| vec![0u8; n]);
638 (bg, touched)
639 }
640
641 fn restore_op_buffers(&mut self, bg: Vec<u8>, touched: Vec<u8>) {
643 self.op_bg_snapshot = Some(bg);
644 self.op_touched = Some(touched);
645 }
646
647 fn take_spot_mask(&mut self, w: u32, h: u32) -> Vec<u8> {
649 let n = w as usize * h as usize;
650 self.spot_mask.take().unwrap_or_else(|| vec![0u8; n])
651 }
652
653 fn restore_spot_mask(&mut self, mask: Vec<u8>) {
655 self.spot_mask = Some(mask);
656 }
657
658 #[allow(dead_code)]
662 fn invalidate_op_snapshot(
663 &mut self,
664 bbox_x0: usize,
665 bbox_y0: usize,
666 bbox_x1: usize,
667 bbox_y1: usize,
668 stride: usize,
669 ) {
670 if let Some(touched) = self.op_touched.as_mut() {
671 for y in bbox_y0..bbox_y1 {
672 let row = y * stride;
673 for x in bbox_x0..bbox_x1 {
674 touched[row + x] = 0;
675 }
676 }
677 }
678 }
679}
680
681struct RenderContext<'a> {
686 vp_x: f32,
688 vp_y: f32,
690 scale_x: f32,
692 scale_y: f32,
694 out_w: u32,
696 out_h: u32,
698 effective_dpi: f64,
700 icc: Option<&'a IccCache>,
702 image_cache: Option<&'a ImageCache>,
704 preprocessed: Option<&'a [Option<PreprocessedImage>]>,
706 elem_idx: usize,
708 no_aa: bool,
710 opm_zero_transparent: bool,
712 knockout_painter_pass: KnockoutPainterPass,
728 parent_group_isolated: bool,
736 alpha_extraction_pass: bool,
741}
742
743#[derive(Clone, Copy, PartialEq, Eq)]
746enum KnockoutPainterPass {
747 None,
749 ColorPass,
751 CoveragePass,
754}
755
756impl RenderContext<'_> {
757 fn transform(&self, m: &Matrix) -> Transform {
759 viewport_transform(
760 to_transform(m),
761 self.vp_x,
762 self.vp_y,
763 self.scale_x,
764 self.scale_y,
765 )
766 }
767}
768
769struct YBBox {
771 y_min: f64,
772 y_max: f64,
773}
774
775struct ClipEpoch {
780 start_idx: usize,
782 end_idx: usize,
784 paint_bbox: Option<YBBox>,
787 has_erase_page: bool,
789}
790
791fn select_band_height(w: u32, h: u32) -> u32 {
794 if w == 0 || h == 0 {
795 return h;
796 }
797 let per_row = w as u64 * 6;
799 let budget = 2 * 1024 * 1024u64; let max_rows = budget / per_row;
801
802 let band = if max_rows >= h as u64 {
804 h
805 } else {
806 let mut p = 1u32;
807 while (p as u64) * 2 <= max_rows {
808 p *= 2;
809 }
810 p.clamp(128, h)
816 };
817
818 if h.div_ceil(band) <= 2 {
820 return h;
821 }
822 band
823}
824
825fn contains_clip_op(list: &DisplayList) -> bool {
832 list.elements().iter().any(|e| match e {
833 DisplayElement::Clip { .. } | DisplayElement::InitClip => true,
834 DisplayElement::OcgGroup { elements, .. } => contains_clip_op(elements),
835 DisplayElement::Group { elements, .. } => contains_clip_op(elements),
836 DisplayElement::SoftMasked { content, .. } => contains_clip_op(content),
837 _ => false,
838 })
839}
840
841fn precompute_bboxes(list: &DisplayList, dpi: f64) -> Vec<Option<YBBox>> {
847 list.elements()
848 .iter()
849 .map(|elem| match elem {
850 DisplayElement::Fill { path, params } => fill_device_y_bbox(path, ¶ms.ctm),
851 DisplayElement::Stroke { path, params } => stroke_device_y_bbox(path, params, dpi),
852 DisplayElement::Image { params, .. } => image_y_bbox(params),
853 DisplayElement::AxialShading { params } => {
854 shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
855 }
856 DisplayElement::RadialShading { params } => {
857 shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
858 }
859 DisplayElement::MeshShading { params } => {
860 shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
861 }
862 DisplayElement::PatchShading { params } => {
863 shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
864 }
865 DisplayElement::PatternFill { params } => pattern_fill_y_bbox(params),
866 DisplayElement::Group { params, .. } => Some(YBBox {
867 y_min: params.bbox[1],
868 y_max: params.bbox[3],
869 }),
870 DisplayElement::SoftMasked { params, .. } => Some(YBBox {
871 y_min: params.bbox[1],
872 y_max: params.bbox[3],
873 }),
874 DisplayElement::OcgGroup {
875 elements,
876 default_visible,
877 ..
878 } => {
879 if !*default_visible && !contains_clip_op(elements) {
885 return None;
886 }
887 let child_bboxes = precompute_bboxes(elements, dpi);
888 let mut y_min = f64::INFINITY;
889 let mut y_max = f64::NEG_INFINITY;
890 for cb in child_bboxes.into_iter().flatten() {
891 y_min = y_min.min(cb.y_min);
892 y_max = y_max.max(cb.y_max);
893 }
894 if y_min <= y_max {
895 Some(YBBox { y_min, y_max })
896 } else {
897 None
898 }
899 }
900 _ => None, })
902 .collect()
903}
904
905fn shading_y_bbox_from_bbox(bbox: &Option<[f64; 4]>, ctm: &Matrix) -> Option<YBBox> {
909 if let Some(bbox) = bbox {
910 let corners = [
911 (bbox[0], bbox[1]),
912 (bbox[2], bbox[1]),
913 (bbox[0], bbox[3]),
914 (bbox[2], bbox[3]),
915 ];
916 let mut y_min = f64::INFINITY;
917 let mut y_max = f64::NEG_INFINITY;
918 for (x, y) in &corners {
919 let (_, dy) = ctm.transform_point(*x, *y);
920 y_min = y_min.min(dy);
921 y_max = y_max.max(dy);
922 }
923 Some(YBBox { y_min, y_max })
924 } else {
925 Some(YBBox {
928 y_min: 0.0,
929 y_max: 1e9,
930 })
931 }
932}
933
934fn stroke_device_y_bbox(path: &PsPath, params: &StrokeParams, dpi: f64) -> Option<YBBox> {
941 let m = ¶ms.ctm;
942 let is_identity =
943 m.a == 1.0 && m.b == 0.0 && m.c == 0.0 && m.d == 1.0 && m.tx == 0.0 && m.ty == 0.0;
944
945 let effective_lw = params.line_width.max(hairline_min_width(¶ms.ctm, dpi));
947
948 if is_identity {
949 return path_y_bbox(path).map(|mut bbox| {
951 let expand = effective_lw * params.miter_limit * 0.5;
952 bbox.y_min -= expand;
953 bbox.y_max += expand;
954 bbox
955 });
956 }
957
958 let (mut x_min, mut x_max) = (f64::INFINITY, f64::NEG_INFINITY);
961 let (mut y_min, mut y_max) = (f64::INFINITY, f64::NEG_INFINITY);
962 for seg in &path.segments {
963 match seg {
964 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => {
965 x_min = x_min.min(*x);
966 x_max = x_max.max(*x);
967 y_min = y_min.min(*y);
968 y_max = y_max.max(*y);
969 }
970 PathSegment::CurveTo {
971 x1,
972 y1,
973 x2,
974 y2,
975 x3,
976 y3,
977 } => {
978 x_min = x_min.min(*x1).min(*x2).min(*x3);
979 x_max = x_max.max(*x1).max(*x2).max(*x3);
980 y_min = y_min.min(*y1).min(*y2).min(*y3);
981 y_max = y_max.max(*y1).max(*y2).max(*y3);
982 }
983 PathSegment::ClosePath => {}
984 }
985 }
986 if x_min > x_max {
987 return None;
988 }
989
990 let corners = [
992 (x_min, y_min),
993 (x_max, y_min),
994 (x_min, y_max),
995 (x_max, y_max),
996 ];
997 let mut dev_y_min = f64::INFINITY;
998 let mut dev_y_max = f64::NEG_INFINITY;
999 for (x, y) in &corners {
1000 let dy = m.b * x + m.d * y + m.ty;
1001 dev_y_min = dev_y_min.min(dy);
1002 dev_y_max = dev_y_max.max(dy);
1003 }
1004
1005 let col_y_len = (m.c * m.c + m.d * m.d).sqrt().max(1.0);
1008 let expand = effective_lw * col_y_len * params.miter_limit * 0.5;
1009 dev_y_min -= expand;
1010 dev_y_max += expand;
1011
1012 Some(YBBox {
1013 y_min: dev_y_min,
1014 y_max: dev_y_max,
1015 })
1016}
1017
1018fn fill_device_y_bbox(path: &PsPath, ctm: &Matrix) -> Option<YBBox> {
1023 let is_identity = ctm.a == 1.0
1024 && ctm.b == 0.0
1025 && ctm.c == 0.0
1026 && ctm.d == 1.0
1027 && ctm.tx == 0.0
1028 && ctm.ty == 0.0;
1029 if is_identity {
1030 return path_y_bbox(path);
1031 }
1032 let bbox = path_full_bbox(path)?;
1033 let corners = [
1034 (bbox.x_min, bbox.y_min),
1035 (bbox.x_max, bbox.y_min),
1036 (bbox.x_min, bbox.y_max),
1037 (bbox.x_max, bbox.y_max),
1038 ];
1039 let mut dev_y_min = f64::INFINITY;
1040 let mut dev_y_max = f64::NEG_INFINITY;
1041 for (x, y) in &corners {
1042 let dy = ctm.b * x + ctm.d * y + ctm.ty;
1043 dev_y_min = dev_y_min.min(dy);
1044 dev_y_max = dev_y_max.max(dy);
1045 }
1046 Some(YBBox {
1047 y_min: dev_y_min,
1048 y_max: dev_y_max,
1049 })
1050}
1051
1052fn path_y_bbox(path: &PsPath) -> Option<YBBox> {
1054 let mut y_min = f64::INFINITY;
1055 let mut y_max = f64::NEG_INFINITY;
1056 for seg in &path.segments {
1057 match seg {
1058 PathSegment::MoveTo(_, y) | PathSegment::LineTo(_, y) => {
1059 y_min = y_min.min(*y);
1060 y_max = y_max.max(*y);
1061 }
1062 PathSegment::CurveTo { y1, y2, y3, .. } => {
1063 y_min = y_min.min(*y1).min(*y2).min(*y3);
1064 y_max = y_max.max(*y1).max(*y2).max(*y3);
1065 }
1066 PathSegment::ClosePath => {}
1067 }
1068 }
1069 if y_min <= y_max {
1070 Some(YBBox { y_min, y_max })
1071 } else {
1072 None
1073 }
1074}
1075
1076fn image_y_bbox(params: &ImageParams) -> Option<YBBox> {
1078 let image_inv = params.image_matrix.invert()?;
1079 let combined = params.ctm.concat(&image_inv);
1080 let corners = [
1081 (0.0, 0.0),
1082 (params.width as f64, 0.0),
1083 (params.width as f64, params.height as f64),
1084 (0.0, params.height as f64),
1085 ];
1086 let mut y_min = f64::INFINITY;
1087 let mut y_max = f64::NEG_INFINITY;
1088 for (x, y) in &corners {
1089 let (_, dy) = combined.transform_point(*x, *y);
1090 y_min = y_min.min(dy);
1091 y_max = y_max.max(dy);
1092 }
1093 Some(YBBox { y_min, y_max })
1094}
1095
1096fn precompute_clip_seen(list: &DisplayList) -> HashSet<u64> {
1099 let mut counts: HashMap<u64, u32> = HashMap::new();
1100 for elem in list.elements() {
1101 if let DisplayElement::Clip { path, params } = elem {
1102 let hash = hash_clip_path(path, ¶ms.fill_rule);
1103 *counts.entry(hash).or_insert(0) += 1;
1104 }
1105 }
1106 counts
1107 .into_iter()
1108 .filter(|(_, c)| *c > 1)
1109 .map(|(h, _)| h)
1110 .collect()
1111}
1112
1113fn build_clip_epochs(list: &DisplayList, bboxes: &[Option<YBBox>]) -> Vec<ClipEpoch> {
1116 let elements = list.elements();
1117 let mut epochs = Vec::new();
1118 let mut epoch_start = 0;
1119 let mut y_min = f64::INFINITY;
1120 let mut y_max = f64::NEG_INFINITY;
1121 let mut has_erase = false;
1122
1123 for (i, element) in elements.iter().enumerate() {
1124 if matches!(element, DisplayElement::InitClip) && i > epoch_start {
1126 epochs.push(ClipEpoch {
1127 start_idx: epoch_start,
1128 end_idx: i,
1129 paint_bbox: if y_min <= y_max {
1130 Some(YBBox { y_min, y_max })
1131 } else {
1132 None
1133 },
1134 has_erase_page: has_erase,
1135 });
1136 epoch_start = i;
1137 y_min = f64::INFINITY;
1138 y_max = f64::NEG_INFINITY;
1139 has_erase = false;
1140 }
1141 if matches!(element, DisplayElement::ErasePage) {
1142 has_erase = true;
1143 }
1144 if let Some(ref bbox) = bboxes[i] {
1145 y_min = y_min.min(bbox.y_min);
1146 y_max = y_max.max(bbox.y_max);
1147 }
1148 }
1149 if epoch_start < elements.len() {
1151 epochs.push(ClipEpoch {
1152 start_idx: epoch_start,
1153 end_idx: elements.len(),
1154 paint_bbox: if y_min <= y_max {
1155 Some(YBBox { y_min, y_max })
1156 } else {
1157 None
1158 },
1159 has_erase_page: has_erase,
1160 });
1161 }
1162 epochs
1163}
1164
1165fn composite_onto_white(data: &mut [u8]) {
1172 for pixel in data.chunks_exact_mut(4) {
1173 let a = pixel[3] as u16;
1174 if a == 255 {
1175 continue; }
1177 let inv_a = 255 - a;
1178 pixel[0] = (pixel[0] as u16 + inv_a).min(255) as u8;
1179 pixel[1] = (pixel[1] as u16 + inv_a).min(255) as u8;
1180 pixel[2] = (pixel[2] as u16 + inv_a).min(255) as u8;
1181 pixel[3] = 255;
1182 }
1183}
1184
1185fn composite_non_isolated_group_cropped(
1194 target: &mut Pixmap,
1195 source: &Pixmap,
1196 backdrop: &[u8],
1197 params: &stet_graphics::display_list::GroupParams,
1198 clip_mask: Option<&stet_tiny_skia::Mask>,
1199 crop_x: i32,
1200 crop_y: i32,
1201) {
1202 let cw = source.width();
1203 let ch = source.height();
1204
1205 let Some(mut contribution) = Pixmap::new(cw, ch) else {
1207 return;
1208 };
1209 let src_data = source.data();
1210 let contrib_data = contribution.data_mut();
1211
1212 for (i, chunk) in contrib_data.chunks_exact_mut(4).enumerate() {
1213 let off = i * 4;
1214 if src_data[off] != backdrop[off]
1215 || src_data[off + 1] != backdrop[off + 1]
1216 || src_data[off + 2] != backdrop[off + 2]
1217 || src_data[off + 3] != backdrop[off + 3]
1218 {
1219 chunk.copy_from_slice(&src_data[off..off + 4]);
1220 }
1221 }
1222
1223 let paint = stet_tiny_skia::PixmapPaint {
1224 opacity: params.alpha as f32,
1225 blend_mode: u8_to_blend_mode(params.blend_mode),
1226 quality: stet_tiny_skia::FilterQuality::Nearest,
1227 };
1228 target.draw_pixmap(
1229 crop_x,
1230 crop_y,
1231 contribution.as_ref(),
1232 &paint,
1233 Transform::identity(),
1234 clip_mask,
1235 );
1236}
1237
1238fn composite_non_isolated_extracted(
1251 target: &mut Pixmap,
1252 source: &Pixmap,
1253 isolated: &Pixmap,
1254 backdrop: &[u8],
1255 params: &stet_graphics::display_list::GroupParams,
1256 clip_mask: Option<&stet_tiny_skia::Mask>,
1257 crop_x: i32,
1258 crop_y: i32,
1259) {
1260 let cw = source.width();
1261 let ch = source.height();
1262
1263 let Some(mut contribution) = Pixmap::new(cw, ch) else {
1264 return;
1265 };
1266 let src_data = source.data();
1267 let iso_data = isolated.data();
1268 let contrib_data = contribution.data_mut();
1269
1270 for i in 0..(cw as usize * ch as usize) {
1271 let off = i * 4;
1272 let alpha_g = iso_data[off + 3];
1273 if alpha_g == 0 {
1274 continue; }
1276
1277 let inv_alpha = 255 - alpha_g as i32;
1279 for c in 0..3 {
1280 let r = src_data[off + c] as i32;
1281 let b = backdrop[off + c] as i32;
1282 let raw = r - (b * inv_alpha + 127) / 255;
1283 contrib_data[off + c] = raw.clamp(0, 255) as u8;
1284 }
1285 contrib_data[off + 3] = alpha_g;
1286 }
1287
1288 let paint = stet_tiny_skia::PixmapPaint {
1289 opacity: params.alpha as f32,
1290 blend_mode: u8_to_blend_mode(params.blend_mode),
1291 quality: stet_tiny_skia::FilterQuality::Nearest,
1292 };
1293 target.draw_pixmap(
1294 crop_x,
1295 crop_y,
1296 contribution.as_ref(),
1297 &paint,
1298 Transform::identity(),
1299 clip_mask,
1300 );
1301}
1302
1303fn viewport_transform(t: Transform, vp_x: f32, vp_y: f32, scale_x: f32, scale_y: f32) -> Transform {
1308 Transform::from_row(
1310 t.sx * scale_x,
1311 t.ky * scale_y,
1312 t.kx * scale_x,
1313 t.sy * scale_y,
1314 (t.tx - vp_x) * scale_x,
1315 (t.ty - vp_y) * scale_y,
1316 )
1317}
1318
1319fn box_resample(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
1326 if dw == 0 || dh == 0 {
1327 return Vec::new();
1328 }
1329 let (sw, sh, dw, dh) = (sw as usize, sh as usize, dw as usize, dh as usize);
1330
1331 let ratio_x = sw as f32 / dw as f32;
1335 let mut tmp = vec![0.0f32; dw * sh * 4];
1336 let tmp_stride = dw * 4;
1337
1338 for y in 0..sh {
1339 let row_off = y * sw * 4;
1340 let dst_row = y * tmp_stride;
1341 for dx in 0..dw {
1342 let left_f = dx as f32 * ratio_x;
1343 let right_f = (dx + 1) as f32 * ratio_x;
1344 let left = (left_f as usize).min(sw - 1);
1345 let right = (right_f.ceil() as usize).min(sw);
1346 let inv_area = 1.0 / (right_f - left_f);
1347 let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0, 0.0, 0.0);
1348 for sx in left..right {
1349 let pixel_left = sx as f32;
1351 let pixel_right = (sx + 1) as f32;
1352 let w = pixel_right.min(right_f) - pixel_left.max(left_f);
1353 let i = row_off + sx * 4;
1354 r += src[i] as f32 * w;
1355 g += src[i + 1] as f32 * w;
1356 b += src[i + 2] as f32 * w;
1357 a += src[i + 3] as f32 * w;
1358 }
1359 let di = dst_row + dx * 4;
1360 tmp[di] = r * inv_area;
1361 tmp[di + 1] = g * inv_area;
1362 tmp[di + 2] = b * inv_area;
1363 tmp[di + 3] = a * inv_area;
1364 }
1365 }
1366
1367 let ratio_y = sh as f32 / dh as f32;
1369 let mut out = vec![0u8; dw * dh * 4];
1370 let out_stride = dw * 4;
1371
1372 for dy in 0..dh {
1373 let top_f = dy as f32 * ratio_y;
1374 let bottom_f = (dy + 1) as f32 * ratio_y;
1375 let top = (top_f as usize).min(sh - 1);
1376 let bottom = (bottom_f.ceil() as usize).min(sh);
1377 let inv_area = 1.0 / (bottom_f - top_f);
1378
1379 let n_rows = bottom - top;
1381 let mut row_weights_buf: [(usize, f32); 8] = [(0, 0.0); 8];
1382 let row_weights_vec: Vec<(usize, f32)>;
1383 let row_weights: &[(usize, f32)] = if n_rows <= 8 {
1384 for (i, sy) in (top..bottom).enumerate() {
1385 let pixel_top = sy as f32;
1386 let pixel_bottom = (sy + 1) as f32;
1387 let w = pixel_bottom.min(bottom_f) - pixel_top.max(top_f);
1388 row_weights_buf[i] = (sy, w);
1389 }
1390 &row_weights_buf[..n_rows]
1391 } else {
1392 row_weights_vec = (top..bottom)
1393 .map(|sy| {
1394 let pixel_top = sy as f32;
1395 let pixel_bottom = (sy + 1) as f32;
1396 let w = pixel_bottom.min(bottom_f) - pixel_top.max(top_f);
1397 (sy, w)
1398 })
1399 .collect();
1400 &row_weights_vec
1401 };
1402
1403 let dst_row = dy * out_stride;
1404 for dx in 0..dw {
1405 let col = dx * 4;
1406 let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0, 0.0, 0.0);
1407 for &(sy, w) in row_weights {
1408 let i = sy * tmp_stride + col;
1409 r += tmp[i] * w;
1410 g += tmp[i + 1] * w;
1411 b += tmp[i + 2] * w;
1412 a += tmp[i + 3] * w;
1413 }
1414 let di = dst_row + col;
1415 out[di] = (r * inv_area + 0.5).clamp(0.0, 255.0) as u8;
1416 out[di + 1] = (g * inv_area + 0.5).clamp(0.0, 255.0) as u8;
1417 out[di + 2] = (b * inv_area + 0.5).clamp(0.0, 255.0) as u8;
1418 out[di + 3] = (a * inv_area + 0.5).clamp(0.0, 255.0) as u8;
1419 }
1420 }
1421
1422 out
1423}
1424
1425fn bicubic_resample(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
1432 if dw == 0 || dh == 0 {
1433 return Vec::new();
1434 }
1435
1436 let (sw, sh, dw, dh) = (sw as usize, sh as usize, dw as usize, dh as usize);
1437 let ratio_x = sw as f32 / dw as f32;
1438 let ratio_y = sh as f32 / dh as f32;
1439
1440 let mut tmp = vec![0.0f32; dw * sh * 4];
1442 for y in 0..sh {
1443 let src_row = y * sw * 4;
1444 let dst_row = y * dw * 4;
1445 for dx in 0..dw {
1446 let sx = (dx as f32 + 0.5) * ratio_x - 0.5;
1447 let sx_floor = sx.floor() as i32;
1448 let fx = sx - sx_floor as f32;
1449 let w0 = catmull_rom(fx + 1.0);
1450 let w1 = catmull_rom(fx);
1451 let w2 = catmull_rom(1.0 - fx);
1452 let w3 = catmull_rom(2.0 - fx);
1453 let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0, 0.0, 0.0);
1454 for (k, w) in [
1455 (sx_floor - 1, w0),
1456 (sx_floor, w1),
1457 (sx_floor + 1, w2),
1458 (sx_floor + 2, w3),
1459 ] {
1460 let px = k.clamp(0, sw as i32 - 1) as usize;
1461 let i = src_row + px * 4;
1462 r += src[i] as f32 * w;
1463 g += src[i + 1] as f32 * w;
1464 b += src[i + 2] as f32 * w;
1465 a += src[i + 3] as f32 * w;
1466 }
1467 let di = dst_row + dx * 4;
1468 tmp[di] = r;
1469 tmp[di + 1] = g;
1470 tmp[di + 2] = b;
1471 tmp[di + 3] = a;
1472 }
1473 }
1474
1475 let mut out = vec![0u8; dw * dh * 4];
1478 let tmp_stride = dw * 4;
1479 let out_stride = dw * 4;
1480 for dy in 0..dh {
1481 let sy = (dy as f32 + 0.5) * ratio_y - 0.5;
1482 let sy_floor = sy.floor() as i32;
1483 let fy = sy - sy_floor as f32;
1484 let w0 = catmull_rom(fy + 1.0);
1485 let w1 = catmull_rom(fy);
1486 let w2 = catmull_rom(1.0 - fy);
1487 let w3 = catmull_rom(2.0 - fy);
1488 let py0 = (sy_floor - 1).clamp(0, sh as i32 - 1) as usize * tmp_stride;
1489 let py1 = sy_floor.clamp(0, sh as i32 - 1) as usize * tmp_stride;
1490 let py2 = (sy_floor + 1).clamp(0, sh as i32 - 1) as usize * tmp_stride;
1491 let py3 = (sy_floor + 2).clamp(0, sh as i32 - 1) as usize * tmp_stride;
1492 let dst_row = dy * out_stride;
1493 for dx in 0..dw {
1494 let col = dx * 4;
1495 let r = tmp[py0 + col] * w0
1496 + tmp[py1 + col] * w1
1497 + tmp[py2 + col] * w2
1498 + tmp[py3 + col] * w3;
1499 let g = tmp[py0 + col + 1] * w0
1500 + tmp[py1 + col + 1] * w1
1501 + tmp[py2 + col + 1] * w2
1502 + tmp[py3 + col + 1] * w3;
1503 let b = tmp[py0 + col + 2] * w0
1504 + tmp[py1 + col + 2] * w1
1505 + tmp[py2 + col + 2] * w2
1506 + tmp[py3 + col + 2] * w3;
1507 let a = tmp[py0 + col + 3] * w0
1508 + tmp[py1 + col + 3] * w1
1509 + tmp[py2 + col + 3] * w2
1510 + tmp[py3 + col + 3] * w3;
1511 let di = dst_row + col;
1512 out[di] = r.round().clamp(0.0, 255.0) as u8;
1513 out[di + 1] = g.round().clamp(0.0, 255.0) as u8;
1514 out[di + 2] = b.round().clamp(0.0, 255.0) as u8;
1515 out[di + 3] = a.round().clamp(0.0, 255.0) as u8;
1516 }
1517 }
1518
1519 out
1520}
1521
1522#[inline]
1524fn catmull_rom(t: f32) -> f32 {
1525 let t = t.abs();
1526 if t < 1.0 {
1527 (1.5 * t - 2.5) * t * t + 1.0
1528 } else if t < 2.0 {
1529 ((-0.5 * t + 2.5) * t - 4.0) * t + 2.0
1530 } else {
1531 0.0
1532 }
1533}
1534
1535pub fn build_icc_cache_for_list(
1547 list: &DisplayList,
1548 system_cmyk_bytes: Option<&std::sync::Arc<Vec<u8>>>,
1549) -> IccCache {
1550 let mut cache = IccCache::new();
1551 let mut seen = HashSet::new();
1552
1553 if let Some(cmyk_bytes) = system_cmyk_bytes
1555 && let Some(hash) = cache.register_profile(cmyk_bytes)
1556 {
1557 seen.insert(hash);
1558 cache.set_default_cmyk_hash(hash);
1560 cache.prepare_reverse_cmyk();
1564 }
1565
1566 fn scan_elements(
1568 elements: &[DisplayElement],
1569 seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
1570 cache: &mut IccCache,
1571 ) {
1572 for element in elements {
1573 if let DisplayElement::Group { elements: sub, .. } = element {
1575 scan_elements(sub.elements(), seen, cache);
1576 }
1577 if let DisplayElement::SoftMasked { content, mask, .. } = element {
1578 scan_elements(content.elements(), seen, cache);
1579 scan_elements(mask.elements(), seen, cache);
1580 }
1581 if let DisplayElement::OcgGroup { elements: sub, .. } = element {
1582 scan_elements(sub.elements(), seen, cache);
1583 }
1584 let shading_cs = match element {
1586 DisplayElement::AxialShading { params } => Some(¶ms.color_space),
1587 DisplayElement::RadialShading { params } => Some(¶ms.color_space),
1588 DisplayElement::MeshShading { params } => Some(¶ms.color_space),
1589 DisplayElement::PatchShading { params } => Some(¶ms.color_space),
1590 _ => None,
1591 };
1592 if let Some(stet_graphics::device::ShadingColorSpace::ICCBased {
1593 n,
1594 profile_hash,
1595 profile_data,
1596 }) = shading_cs
1597 {
1598 if seen.insert(*profile_hash) {
1599 cache.register_profile_with_n(profile_data, Some(*n));
1600 }
1601 }
1602 if let DisplayElement::Image { params, .. } = element {
1604 match ¶ms.color_space {
1605 ImageColorSpace::ICCBased {
1606 n,
1607 profile_hash,
1608 profile_data,
1609 } if seen.insert(*profile_hash) => {
1610 cache.register_profile_with_n(profile_data, Some(*n));
1611 }
1612 ImageColorSpace::Indexed { base, .. }
1613 if matches!(base.as_ref(), ImageColorSpace::ICCBased { .. }) =>
1614 {
1615 if let ImageColorSpace::ICCBased {
1616 n,
1617 profile_hash,
1618 profile_data,
1619 } = base.as_ref()
1620 {
1621 if seen.insert(*profile_hash) {
1622 cache.register_profile_with_n(profile_data, Some(*n));
1623 }
1624 }
1625 }
1626 _ => {}
1627 }
1628 }
1629 }
1630 }
1631 scan_elements(list.elements(), &mut seen, &mut cache);
1632
1633 cache
1634}
1635
1636fn register_shading_icc_profiles(list: &DisplayList, cache: &mut IccCache) {
1641 fn register_image_iccs(
1642 cs: &ImageColorSpace,
1643 seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
1644 cache: &mut IccCache,
1645 ) {
1646 match cs {
1647 ImageColorSpace::ICCBased {
1648 n,
1649 profile_hash,
1650 profile_data,
1651 } => {
1652 if seen.insert(*profile_hash) {
1653 cache.register_profile_with_n(profile_data, Some(*n));
1654 }
1655 }
1656 ImageColorSpace::Indexed { base, .. } => register_image_iccs(base, seen, cache),
1657 ImageColorSpace::Separation { alt_space, .. }
1658 | ImageColorSpace::DeviceN { alt_space, .. } => {
1659 register_image_iccs(alt_space, seen, cache)
1660 }
1661 _ => {}
1662 }
1663 }
1664 fn scan(
1665 elements: &[DisplayElement],
1666 seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
1667 cache: &mut IccCache,
1668 ) {
1669 for element in elements {
1670 if let DisplayElement::Group { elements: sub, .. } = element {
1671 scan(sub.elements(), seen, cache);
1672 }
1673 if let DisplayElement::SoftMasked { content, mask, .. } = element {
1674 scan(content.elements(), seen, cache);
1675 scan(mask.elements(), seen, cache);
1676 }
1677 if let DisplayElement::OcgGroup { elements: sub, .. } = element {
1678 scan(sub.elements(), seen, cache);
1679 }
1680 let shading_cs = match element {
1681 DisplayElement::AxialShading { params } => Some(¶ms.color_space),
1682 DisplayElement::RadialShading { params } => Some(¶ms.color_space),
1683 DisplayElement::MeshShading { params } => Some(¶ms.color_space),
1684 DisplayElement::PatchShading { params } => Some(¶ms.color_space),
1685 _ => None,
1686 };
1687 if let Some(stet_graphics::device::ShadingColorSpace::ICCBased {
1688 n,
1689 profile_hash,
1690 profile_data,
1691 }) = shading_cs
1692 && seen.insert(*profile_hash)
1693 {
1694 cache.register_profile_with_n(profile_data, Some(*n));
1695 }
1696 if let DisplayElement::Image { params, .. } = element {
1697 register_image_iccs(¶ms.color_space, seen, cache);
1698 }
1699 }
1700 }
1701 let mut seen = HashSet::new();
1702 scan(list.elements(), &mut seen, cache);
1703}
1704
1705fn samples_to_rgba(
1709 data: &[u8],
1710 params: &ImageParams,
1711 icc: Option<&IccCache>,
1712 opm_zero_transparent: bool,
1713) -> Vec<u8> {
1714 let w = params.width as usize;
1715 let h = params.height as usize;
1716 let npixels = w * h;
1717 let bpc = params.bits_per_component;
1718 match ¶ms.color_space {
1719 ImageColorSpace::PreconvertedRGBA => {
1720 data.to_vec()
1722 }
1723 ImageColorSpace::DeviceGray => {
1724 let mut rgba = vec![255u8; npixels * 4];
1725 if bpc == 16 {
1726 for i in 0..npixels {
1727 let g = data.get(i * 2).copied().unwrap_or(0);
1728 let pi = i * 4;
1729 rgba[pi] = g;
1730 rgba[pi + 1] = g;
1731 rgba[pi + 2] = g;
1732 }
1733 } else {
1734 for i in 0..npixels {
1735 let g = data.get(i).copied().unwrap_or(0);
1736 let pi = i * 4;
1737 rgba[pi] = g;
1738 rgba[pi + 1] = g;
1739 rgba[pi + 2] = g;
1740 }
1741 }
1742 rgba
1743 }
1744 ImageColorSpace::DeviceRGB => {
1745 let mut rgba = vec![255u8; npixels * 4];
1746 if bpc == 16 {
1747 for i in 0..npixels {
1750 let si = i * 6;
1751 let pi = i * 4;
1752 rgba[pi] = data.get(si).copied().unwrap_or(0);
1753 rgba[pi + 1] = data.get(si + 2).copied().unwrap_or(0);
1754 rgba[pi + 2] = data.get(si + 4).copied().unwrap_or(0);
1755 }
1756 } else {
1757 for i in 0..npixels {
1758 let si = i * 3;
1759 let pi = i * 4;
1760 rgba[pi] = data.get(si).copied().unwrap_or(0);
1761 rgba[pi + 1] = data.get(si + 1).copied().unwrap_or(0);
1762 rgba[pi + 2] = data.get(si + 2).copied().unwrap_or(0);
1763 }
1764 }
1765 rgba
1766 }
1767 ImageColorSpace::DeviceCMYK => {
1768 if let Some(cache) = icc
1772 && let Some(cmyk_hash) = cache.default_cmyk_hash()
1773 {
1774 let avail_pixels = data.len() / 4;
1775 let icc_pixels = avail_pixels.min(npixels);
1776 if icc_pixels > 0
1777 && let Some(rgb) = cache.convert_image_8bit(cmyk_hash, data, icc_pixels)
1778 {
1779 let mut rgba = vec![255u8; npixels * 4];
1780 for i in 0..icc_pixels {
1781 rgba[i * 4] = rgb[i * 3];
1782 rgba[i * 4 + 1] = rgb[i * 3 + 1];
1783 rgba[i * 4 + 2] = rgb[i * 3 + 2];
1784 if opm_zero_transparent {
1786 let si = i * 4;
1787 if data[si] == 0
1788 && data[si + 1] == 0
1789 && data[si + 2] == 0
1790 && data[si + 3] == 0
1791 {
1792 rgba[i * 4 + 3] = 0;
1793 }
1794 }
1795 }
1796 return rgba;
1798 }
1799 }
1800 let mut rgba = vec![255u8; npixels * 4];
1802 for i in 0..npixels {
1803 let si = i * 4;
1804 let c = data.get(si).copied().unwrap_or(0) as f64 / 255.0;
1805 let m = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0;
1806 let y = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0;
1807 let k = data.get(si + 3).copied().unwrap_or(0) as f64 / 255.0;
1808 let r = (1.0 - c.min(1.0)) * (1.0 - k.min(1.0));
1809 let g = (1.0 - m.min(1.0)) * (1.0 - k.min(1.0));
1810 let b = (1.0 - y.min(1.0)) * (1.0 - k.min(1.0));
1811 let pi = i * 4;
1812 rgba[pi] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
1813 rgba[pi + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
1814 rgba[pi + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
1815 if opm_zero_transparent
1817 && data.get(si).copied().unwrap_or(0) == 0
1818 && data.get(si + 1).copied().unwrap_or(0) == 0
1819 && data.get(si + 2).copied().unwrap_or(0) == 0
1820 && data.get(si + 3).copied().unwrap_or(0) == 0
1821 {
1822 rgba[pi + 3] = 0;
1823 }
1824 }
1825 rgba
1826 }
1827 ImageColorSpace::ICCBased {
1828 n,
1829 profile_hash,
1830 profile_data,
1831 } => {
1832 if let Some(cache) = icc
1834 && cache.has_profile(profile_hash)
1835 && let Some(rgb) = cache.convert_image_8bit(profile_hash, data, npixels)
1836 {
1837 let mut rgba = vec![255u8; npixels * 4];
1838 for i in 0..npixels {
1839 rgba[i * 4] = rgb[i * 3];
1840 rgba[i * 4 + 1] = rgb[i * 3 + 1];
1841 rgba[i * 4 + 2] = rgb[i * 3 + 2];
1842 if opm_zero_transparent && *n == 4 {
1844 let si = i * *n as usize;
1845 if si + 3 < data.len()
1846 && data[si] == 0
1847 && data[si + 1] == 0
1848 && data[si + 2] == 0
1849 && data[si + 3] == 0
1850 {
1851 rgba[i * 4 + 3] = 0;
1852 }
1853 }
1854 }
1855 return rgba;
1856 }
1857 let _ = (profile_hash, profile_data);
1859 let fallback = match n {
1860 1 => ImageColorSpace::DeviceGray,
1861 4 => ImageColorSpace::DeviceCMYK,
1862 _ => ImageColorSpace::DeviceRGB,
1863 };
1864 let p = ImageParams {
1865 color_space: fallback,
1866 bits_per_component: 8,
1867 ..params.clone()
1868 };
1869 samples_to_rgba(data, &p, icc, opm_zero_transparent)
1870 }
1871 ImageColorSpace::Indexed {
1872 base,
1873 hival,
1874 lookup,
1875 } => {
1876 let base_ncomp = base.num_components() as usize;
1877 let mut expanded = Vec::with_capacity(npixels * base_ncomp);
1879 for i in 0..npixels {
1880 let idx = data.get(i).copied().unwrap_or(0) as usize;
1881 let idx = idx.min(*hival as usize);
1882 let offset = idx * base_ncomp;
1883 for c in 0..base_ncomp {
1884 expanded.push(lookup.get(offset + c).copied().unwrap_or(0));
1885 }
1886 }
1887 let p = ImageParams {
1888 color_space: *base.clone(),
1889 bits_per_component: 8,
1890 ..params.clone()
1891 };
1892 samples_to_rgba(&expanded, &p, icc, opm_zero_transparent)
1893 }
1894 ImageColorSpace::CIEBasedABC { params: cie_params } => {
1895 let mut rgba = vec![255u8; npixels * 4];
1896 for i in 0..npixels {
1897 let si = i * 3;
1898 let a = data.get(si).copied().unwrap_or(0) as f64 / 255.0;
1899 let b = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0;
1900 let c = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0;
1901 let color = DeviceColor::from_cie_abc(a, b, c, cie_params);
1902 let pi = i * 4;
1903 rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
1904 rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
1905 rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
1906 }
1907 rgba
1908 }
1909 ImageColorSpace::CIEBasedA { params: cie_params } => {
1910 let mut rgba = vec![255u8; npixels * 4];
1911 for i in 0..npixels {
1912 let val = data.get(i).copied().unwrap_or(0) as f64 / 255.0;
1913 let color = DeviceColor::from_cie_a(val, cie_params);
1914 let pi = i * 4;
1915 rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
1916 rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
1917 rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
1918 }
1919 rgba
1920 }
1921 ImageColorSpace::Lab { range, .. } => {
1922 let mut rgba = vec![255u8; npixels * 4];
1923 let a_span = range[1] - range[0];
1924 let b_span = range[3] - range[2];
1925 for i in 0..npixels {
1926 let si = i * 3;
1927 let l = data.get(si).copied().unwrap_or(0) as f64 / 255.0 * 100.0;
1928 let a = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0 * a_span + range[0];
1929 let b = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0 * b_span + range[2];
1930 let color = DeviceColor::from_lab(l, a, b, range);
1931 let pi = i * 4;
1932 rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
1933 rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
1934 rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
1935 }
1936 rgba
1937 }
1938 ImageColorSpace::Separation {
1939 alt_space,
1940 tint_table,
1941 ..
1942 } => {
1943 if matches!(alt_space.as_ref(), ImageColorSpace::DeviceCMYK)
1946 && let Some(rgba) = tint_separation_via_icc(data, npixels, tint_table, icc)
1947 {
1948 return rgba;
1949 }
1950 let mut rgba = vec![255u8; npixels * 4];
1951 let no = tint_table.num_outputs as usize;
1952 let mut alt_comps = vec![0.0f32; no];
1953 for i in 0..npixels {
1954 let tint = data.get(i).copied().unwrap_or(0) as f32 / 255.0;
1955 tint_table.lookup_1d(tint, &mut alt_comps);
1956 let (r, g, b) = alt_comps_to_rgb(&alt_comps, alt_space);
1957 let pi = i * 4;
1958 rgba[pi] = r;
1959 rgba[pi + 1] = g;
1960 rgba[pi + 2] = b;
1961 }
1962 rgba
1963 }
1964 ImageColorSpace::DeviceN {
1965 alt_space,
1966 tint_table,
1967 ..
1968 } => {
1969 let ni = tint_table.num_inputs as usize;
1970 let no = tint_table.num_outputs as usize;
1971 if matches!(alt_space.as_ref(), ImageColorSpace::DeviceCMYK)
1973 && let Some(rgba) = tint_devicen_via_icc(data, npixels, ni, tint_table, icc)
1974 {
1975 return rgba;
1976 }
1977 let mut rgba = vec![255u8; npixels * 4];
1978 let mut inputs = vec![0.0f32; ni];
1979 let mut alt_comps = vec![0.0f32; no];
1980 for i in 0..npixels {
1981 let si = i * ni;
1982 for (c, inp) in inputs.iter_mut().enumerate() {
1983 *inp = data.get(si + c).copied().unwrap_or(0) as f32 / 255.0;
1984 }
1985 tint_table.lookup_nd(&inputs, &mut alt_comps);
1986 let (r, g, b) = alt_comps_to_rgb(&alt_comps, alt_space);
1987 let pi = i * 4;
1988 rgba[pi] = r;
1989 rgba[pi + 1] = g;
1990 rgba[pi + 2] = b;
1991 }
1992 rgba
1993 }
1994 ImageColorSpace::Mask { color, polarity } => {
1995 let mut rgba = vec![0u8; npixels * 4];
1996 let r = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
1997 let g = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
1998 let b = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
1999 let bytes_per_row = (w).div_ceil(8);
2000 for row in 0..h {
2001 for col in 0..w {
2002 let byte_idx = row * bytes_per_row + col / 8;
2003 let bit_offset = 7 - (col % 8);
2004 let bit = if byte_idx < data.len() {
2005 (data[byte_idx] >> bit_offset) & 1
2006 } else {
2007 0
2008 };
2009 let paint = if *polarity { bit == 1 } else { bit == 0 };
2010 if paint {
2011 let pi = (row * w + col) * 4;
2012 rgba[pi] = r;
2013 rgba[pi + 1] = g;
2014 rgba[pi + 2] = b;
2015 rgba[pi + 3] = 255;
2016 }
2017 }
2018 }
2019 rgba
2020 }
2021 }
2022}
2023
2024fn tint_separation_via_icc(
2027 data: &[u8],
2028 npixels: usize,
2029 tint_table: &TintLookupTable,
2030 icc: Option<&IccCache>,
2031) -> Option<Vec<u8>> {
2032 let cache = icc?;
2033 let cmyk_hash = cache.default_cmyk_hash()?;
2034 let mut cmyk_data = vec![0u8; npixels * 4];
2036 let mut alt_comps = [0.0f32; 4];
2037 for i in 0..npixels {
2038 let tint = data.get(i).copied().unwrap_or(0) as f32 / 255.0;
2039 tint_table.lookup_1d(tint, &mut alt_comps);
2040 let si = i * 4;
2041 cmyk_data[si] = (alt_comps[0].clamp(0.0, 1.0) * 255.0).round() as u8;
2042 cmyk_data[si + 1] = (alt_comps[1].clamp(0.0, 1.0) * 255.0).round() as u8;
2043 cmyk_data[si + 2] = (alt_comps[2].clamp(0.0, 1.0) * 255.0).round() as u8;
2044 cmyk_data[si + 3] = (alt_comps[3].clamp(0.0, 1.0) * 255.0).round() as u8;
2045 }
2046 let rgb = cache.convert_image_8bit(cmyk_hash, &cmyk_data, npixels)?;
2047 let mut rgba = vec![255u8; npixels * 4];
2048 for i in 0..npixels {
2049 rgba[i * 4] = rgb[i * 3];
2050 rgba[i * 4 + 1] = rgb[i * 3 + 1];
2051 rgba[i * 4 + 2] = rgb[i * 3 + 2];
2052 }
2053 Some(rgba)
2054}
2055
2056fn tint_devicen_via_icc(
2058 data: &[u8],
2059 npixels: usize,
2060 ni: usize,
2061 tint_table: &TintLookupTable,
2062 icc: Option<&IccCache>,
2063) -> Option<Vec<u8>> {
2064 let cache = icc?;
2065 let cmyk_hash = cache.default_cmyk_hash()?;
2066 let mut cmyk_data = vec![0u8; npixels * 4];
2067 let mut inputs = vec![0.0f32; ni];
2068 let mut alt_comps = [0.0f32; 4];
2069 for i in 0..npixels {
2070 let si = i * ni;
2071 for (c, inp) in inputs.iter_mut().enumerate() {
2072 *inp = data.get(si + c).copied().unwrap_or(0) as f32 / 255.0;
2073 }
2074 tint_table.lookup_nd(&inputs, &mut alt_comps);
2075 let di = i * 4;
2076 cmyk_data[di] = (alt_comps[0].clamp(0.0, 1.0) * 255.0).round() as u8;
2077 cmyk_data[di + 1] = (alt_comps[1].clamp(0.0, 1.0) * 255.0).round() as u8;
2078 cmyk_data[di + 2] = (alt_comps[2].clamp(0.0, 1.0) * 255.0).round() as u8;
2079 cmyk_data[di + 3] = (alt_comps[3].clamp(0.0, 1.0) * 255.0).round() as u8;
2080 }
2081 let rgb = cache.convert_image_8bit(cmyk_hash, &cmyk_data, npixels)?;
2082 let mut rgba = vec![255u8; npixels * 4];
2083 for i in 0..npixels {
2084 rgba[i * 4] = rgb[i * 3];
2085 rgba[i * 4 + 1] = rgb[i * 3 + 1];
2086 rgba[i * 4 + 2] = rgb[i * 3 + 2];
2087 }
2088 Some(rgba)
2089}
2090
2091fn alt_comps_to_rgb(comps: &[f32], alt_space: &ImageColorSpace) -> (u8, u8, u8) {
2093 match alt_space {
2094 ImageColorSpace::DeviceGray => {
2095 let g = (comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2096 (g, g, g)
2097 }
2098 ImageColorSpace::DeviceRGB => {
2099 let r = (comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2100 let g = (comps.get(1).copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2101 let b = (comps.get(2).copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2102 (r, g, b)
2103 }
2104 ImageColorSpace::DeviceCMYK => {
2105 let c = comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0);
2106 let m = comps.get(1).copied().unwrap_or(0.0).clamp(0.0, 1.0);
2107 let y = comps.get(2).copied().unwrap_or(0.0).clamp(0.0, 1.0);
2108 let k = comps.get(3).copied().unwrap_or(0.0).clamp(0.0, 1.0);
2109 let r = ((1.0 - (c + k).min(1.0)) * 255.0).round() as u8;
2110 let g = ((1.0 - (m + k).min(1.0)) * 255.0).round() as u8;
2111 let b = ((1.0 - (y + k).min(1.0)) * 255.0).round() as u8;
2112 (r, g, b)
2113 }
2114 _ => (0, 0, 0),
2115 }
2116}
2117
2118fn apply_mask_color_rgba(rgba: &mut [u8], sample_data: &[u8], params: &ImageParams) {
2120 let mask_color = match ¶ms.mask_color {
2121 Some(mc) => mc,
2122 None => return,
2123 };
2124 let ncomp = params.color_space.num_components() as usize;
2125 let npixels = params.width as usize * params.height as usize;
2126 let is_range = mask_color.len() == 2 * ncomp;
2127
2128 for i in 0..npixels {
2129 let si = i * ncomp;
2130 let matched = if is_range {
2131 (0..ncomp).all(|c| {
2132 let sample = sample_data.get(si + c).copied().unwrap_or(0);
2133 let min_val = mask_color.get(c * 2).copied().unwrap_or(0);
2134 let max_val = mask_color.get(c * 2 + 1).copied().unwrap_or(0);
2135 sample >= min_val && sample <= max_val
2136 })
2137 } else {
2138 (0..ncomp).all(|c| {
2139 let sample = sample_data.get(si + c).copied().unwrap_or(0);
2140 let target = mask_color.get(c).copied().unwrap_or(0);
2141 sample == target
2142 })
2143 };
2144 if matched {
2145 let pi = i * 4;
2146 if pi + 3 < rgba.len() {
2147 rgba[pi] = 0;
2148 rgba[pi + 1] = 0;
2149 rgba[pi + 2] = 0;
2150 rgba[pi + 3] = 0;
2151 }
2152 }
2153 }
2154}
2155
2156fn image_filter_quality(transform: Transform, interpolate: bool) -> stet_tiny_skia::FilterQuality {
2162 let eff_sx = (transform.sx * transform.sx + transform.ky * transform.ky).sqrt();
2163 let eff_sy = (transform.kx * transform.kx + transform.sy * transform.sy).sqrt();
2164 let min_scale = eff_sx.min(eff_sy);
2165 if (eff_sx - 1.0).abs() < 0.01 && (eff_sy - 1.0).abs() < 0.01 {
2167 stet_tiny_skia::FilterQuality::Nearest
2168 } else if !interpolate && min_scale >= 0.95 {
2169 stet_tiny_skia::FilterQuality::Nearest
2171 } else {
2172 stet_tiny_skia::FilterQuality::Bilinear
2173 }
2174}
2175
2176fn prescale_image(
2181 rgba_data: &[u8],
2182 w: u32,
2183 h: u32,
2184 transform: Transform,
2185 interpolate: bool,
2186) -> Option<(Vec<u8>, u32, u32, Transform)> {
2187 let scale_x = (transform.sx * transform.sx + transform.ky * transform.ky).sqrt();
2189 let scale_y = (transform.kx * transform.kx + transform.sy * transform.sy).sqrt();
2190 let min_scale = scale_x.min(scale_y);
2191
2192 if min_scale > 1.05 {
2196 if interpolate {
2197 let is_axis_aligned = transform.kx.abs() < 1e-4 && transform.ky.abs() < 1e-4;
2198 if is_axis_aligned && w >= 2 && h >= 2 {
2199 let dw = (w as f32 * transform.sx.abs()).round().max(1.0) as u32;
2200 let dh = (h as f32 * transform.sy.abs()).round().max(1.0) as u32;
2201 if dw > w || dh > h {
2202 let resampled = bicubic_resample(rgba_data, w, h, dw, dh);
2203 let new_sx = transform.sx * w as f32 / dw as f32;
2204 let new_sy = transform.sy * h as f32 / dh as f32;
2205 let adjusted = Transform::from_row(
2206 new_sx,
2207 transform.ky,
2208 transform.kx,
2209 new_sy,
2210 transform.tx,
2211 transform.ty,
2212 );
2213 return Some((resampled, dw, dh, adjusted));
2214 }
2215 }
2216 }
2217 return None;
2218 }
2219
2220 if min_scale >= 0.95 {
2222 return None;
2223 }
2224
2225 let is_axis_aligned = transform.kx.abs() < 1e-4 && transform.ky.abs() < 1e-4;
2228 if is_axis_aligned && w >= 2 && h >= 2 {
2229 let dw = (w as f32 * transform.sx.abs()).ceil().max(1.0) as u32;
2230 let dh = (h as f32 * transform.sy.abs()).ceil().max(1.0) as u32;
2231 if dw < w || dh < h {
2232 let resampled = box_resample(rgba_data, w, h, dw, dh);
2233 let new_sx = transform.sx * w as f32 / dw as f32;
2235 let new_sy = transform.sy * h as f32 / dh as f32;
2236 let adjusted = Transform::from_row(
2237 new_sx,
2238 transform.ky,
2239 transform.kx,
2240 new_sy,
2241 transform.tx,
2242 transform.ty,
2243 );
2244 return Some((resampled, dw, dh, adjusted));
2245 }
2246 }
2247
2248 let factor = (1.0 / min_scale) as u32;
2250 if factor < 2 || w < factor || h < factor {
2251 return None;
2252 }
2253 let nw = w / factor;
2254 let nh = h / factor;
2255 if nw == 0 || nh == 0 {
2256 return None;
2257 }
2258 let area = factor * factor;
2259 let half = area / 2;
2260 let stride = w as usize * 4;
2261 let mut out = vec![0u8; (nw * nh * 4) as usize];
2262 for dy in 0..nh {
2263 for dx in 0..nw {
2264 let (mut r, mut g, mut b, mut a) = (0u32, 0u32, 0u32, 0u32);
2265 let sy0 = (dy * factor) as usize;
2266 let sx0 = (dx * factor) as usize;
2267 for iy in 0..factor as usize {
2268 let row = (sy0 + iy) * stride + sx0 * 4;
2269 for ix in 0..factor as usize {
2270 let i = row + ix * 4;
2271 r += rgba_data[i] as u32;
2272 g += rgba_data[i + 1] as u32;
2273 b += rgba_data[i + 2] as u32;
2274 a += rgba_data[i + 3] as u32;
2275 }
2276 }
2277 let di = (dy * nw + dx) as usize * 4;
2278 out[di] = ((r + half) / area) as u8;
2279 out[di + 1] = ((g + half) / area) as u8;
2280 out[di + 2] = ((b + half) / area) as u8;
2281 out[di + 3] = ((a + half) / area) as u8;
2282 }
2283 }
2284 let f = factor as f32;
2285 let adjusted = Transform::from_row(
2286 transform.sx * f,
2287 transform.ky * f,
2288 transform.kx * f,
2289 transform.sy * f,
2290 transform.tx,
2291 transform.ty,
2292 );
2293 Some((out, nw, nh, adjusted))
2294}
2295
2296fn translate_clip_rect(rect: &ClipRect, y_start: u32, band_h: u32) -> ClipRect {
2298 ClipRect {
2299 x0: rect.x0,
2300 y0: rect.y0.saturating_sub(y_start).min(band_h),
2301 x1: rect.x1,
2302 y1: rect.y1.saturating_sub(y_start).min(band_h),
2303 }
2304}
2305
2306fn enforce_min_image_size(transform: Transform, img_w: u32, img_h: u32) -> Transform {
2313 let eff_w =
2315 ((transform.sx * img_w as f32).powi(2) + (transform.ky * img_w as f32).powi(2)).sqrt();
2316 let eff_h =
2317 ((transform.kx * img_h as f32).powi(2) + (transform.sy * img_h as f32).powi(2)).sqrt();
2318
2319 if eff_w >= 1.0 && eff_h >= 1.0 {
2320 return transform;
2321 }
2322
2323 let ratio = eff_w.max(eff_h) / eff_w.min(eff_h).max(0.001);
2327 if ratio < 3.0 {
2328 return transform;
2329 }
2330
2331 let mut t = transform;
2332 if eff_w < 1.0 && eff_w > 0.001 {
2333 let boost = 1.0 / eff_w;
2334 t.sx *= boost;
2335 t.ky *= boost;
2336 }
2337 if eff_h < 1.0 && eff_h > 0.001 {
2338 let boost = 1.0 / eff_h;
2339 t.kx *= boost;
2340 t.sy *= boost;
2341 }
2342 t
2343}
2344
2345fn hairline_min_width(ctm: &Matrix, dpi: f64) -> f64 {
2349 let (a, b, c, d) = (ctm.a, ctm.b, ctm.c, ctm.d);
2350 let sum_sq = a * a + b * b + c * c + d * d;
2351 let diff = ((a * a + b * b - c * c - d * d).powi(2) + 4.0 * (a * c + b * d).powi(2)).sqrt();
2352 let s_max = (0.5 * (sum_sq + diff)).max(0.0).sqrt();
2353 let min_px = if dpi <= 150.0 { 0.5 } else { 1.0 };
2354 if s_max > 1e-10 {
2355 min_px / s_max
2356 } else {
2357 min_px
2358 }
2359}
2360
2361fn is_k_only_src(color: &DeviceColor) -> bool {
2367 if let Some((c, m, y, _k)) = color.native_cmyk {
2368 c == 0.0 && m == 0.0 && y == 0.0
2369 } else {
2370 false
2371 }
2372}
2373
2374fn needs_gray_promotion(
2383 overprint: bool,
2384 painted_channels: u8,
2385 is_device_cmyk: bool,
2386 color: &DeviceColor,
2387) -> Option<f64> {
2388 if !overprint
2389 || painted_channels != 0
2390 || is_device_cmyk
2391 || color.native_cmyk.is_some()
2392 || color.process_cmyk.is_some()
2393 {
2394 return None;
2395 }
2396 let r = color.r;
2397 if (r - color.g).abs() > f64::EPSILON || (r - color.b).abs() > f64::EPSILON {
2398 return None;
2399 }
2400 Some(r.clamp(0.0, 1.0))
2401}
2402
2403fn maybe_promote_gray_fill<'a>(
2406 params: &'a FillParams,
2407 buf: &'a mut Option<FillParams>,
2408) -> &'a FillParams {
2409 if let Some(gray) = needs_gray_promotion(
2410 params.overprint,
2411 params.painted_channels,
2412 params.is_device_cmyk,
2413 ¶ms.color,
2414 ) {
2415 let mut promoted = params.clone();
2416 promoted.is_device_cmyk = true;
2417 promoted.painted_channels = stet_graphics::device::CMYK_K;
2418 promoted.color.native_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2419 promoted.color.process_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2420 *buf = Some(promoted);
2421 return buf.as_ref().unwrap();
2422 }
2423 params
2424}
2425
2426fn maybe_promote_gray_stroke<'a>(
2428 params: &'a StrokeParams,
2429 buf: &'a mut Option<StrokeParams>,
2430) -> &'a StrokeParams {
2431 if let Some(gray) = needs_gray_promotion(
2432 params.overprint,
2433 params.painted_channels,
2434 params.is_device_cmyk,
2435 ¶ms.color,
2436 ) {
2437 let mut promoted = params.clone();
2438 promoted.is_device_cmyk = true;
2439 promoted.painted_channels = stet_graphics::device::CMYK_K;
2440 promoted.color.native_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2441 promoted.color.process_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2442 *buf = Some(promoted);
2443 return buf.as_ref().unwrap();
2444 }
2445 params
2446}
2447
2448fn build_stroke(params: &StrokeParams, dpi: f64) -> Stroke {
2452 let min_lw = hairline_min_width(¶ms.ctm, dpi);
2453 let mut stroke = Stroke {
2454 width: (params.line_width as f32).max(min_lw as f32),
2455 line_cap: to_line_cap(params.line_cap),
2456 line_join: to_line_join(params.line_join),
2457 miter_limit: params.miter_limit as f32,
2458 ..Stroke::default()
2459 };
2460 if !params.dash_pattern.array.is_empty() {
2461 let mut dash_array: Vec<f32> = params
2462 .dash_pattern
2463 .array
2464 .iter()
2465 .map(|&v| v as f32)
2466 .collect();
2467 if dash_array.len() % 2 == 1 {
2470 let clone = dash_array.clone();
2471 dash_array.extend_from_slice(&clone);
2472 }
2473 if let Some(dash) = StrokeDash::new(dash_array, params.dash_pattern.offset as f32) {
2474 stroke.dash = Some(dash);
2475 }
2476 }
2477 stroke
2478}
2479
2480fn ctm_is_device_space(ctm: &Matrix) -> bool {
2497 (ctm.a.abs() - 1.0).abs() < 0.01
2498 && ctm.b.abs() < 0.01
2499 && ctm.c.abs() < 0.01
2500 && (ctm.d.abs() - 1.0).abs() < 0.01
2501}
2502
2503fn stroke_adjust_path_viewport(
2509 path: &PsPath,
2510 device_width: f64,
2511 scale_x: f64,
2512 scale_y: f64,
2513 vp_x: f64,
2514 vp_y: f64,
2515) -> PsPath {
2516 let use_half_pixel = device_width < 1.5 || (device_width.round() as i32) % 2 == 1;
2517
2518 let snap_x = |v: f64| -> f64 {
2520 let out = (v - vp_x) * scale_x;
2521 let snapped = if use_half_pixel {
2522 out.floor() + 0.5
2523 } else {
2524 out.round()
2525 };
2526 snapped / scale_x + vp_x
2527 };
2528 let snap_y = |v: f64| -> f64 {
2529 let out = (v - vp_y) * scale_y;
2530 let snapped = if use_half_pixel {
2531 out.floor() + 0.5
2532 } else {
2533 out.round()
2534 };
2535 snapped / scale_y + vp_y
2536 };
2537
2538 let mut result = PsPath::new();
2539 let mut prev_x = 0.0_f64;
2540 let mut prev_y = 0.0_f64;
2541
2542 for seg in &path.segments {
2543 match *seg {
2544 PathSegment::MoveTo(x, y) => {
2545 prev_x = x;
2546 prev_y = y;
2547 result.segments.push(PathSegment::MoveTo(x, y));
2548 }
2549 PathSegment::LineTo(x, y) => {
2550 let is_horizontal = (y - prev_y).abs() < 1e-6;
2551 let is_vertical = (x - prev_x).abs() < 1e-6;
2552
2553 if is_horizontal {
2554 let snapped_y = snap_y(y);
2555 if let Some(PathSegment::MoveTo(_, ly) | PathSegment::LineTo(_, ly)) =
2556 result.segments.last_mut()
2557 {
2558 *ly = snapped_y;
2559 }
2560 result.segments.push(PathSegment::LineTo(x, snapped_y));
2561 prev_x = x;
2562 prev_y = snapped_y;
2563 } else if is_vertical {
2564 let snapped_x = snap_x(x);
2565 if let Some(PathSegment::MoveTo(lx, _) | PathSegment::LineTo(lx, _)) =
2566 result.segments.last_mut()
2567 {
2568 *lx = snapped_x;
2569 }
2570 result.segments.push(PathSegment::LineTo(snapped_x, y));
2571 prev_x = snapped_x;
2572 prev_y = y;
2573 } else {
2574 result.segments.push(PathSegment::LineTo(x, y));
2575 prev_x = x;
2576 prev_y = y;
2577 }
2578 }
2579 PathSegment::CurveTo {
2580 x1,
2581 y1,
2582 x2,
2583 y2,
2584 x3,
2585 y3,
2586 } => {
2587 result.segments.push(PathSegment::CurveTo {
2588 x1,
2589 y1,
2590 x2,
2591 y2,
2592 x3,
2593 y3,
2594 });
2595 prev_x = x3;
2596 prev_y = y3;
2597 }
2598 PathSegment::ClosePath => {
2599 result.segments.push(PathSegment::ClosePath);
2600 }
2601 }
2602 }
2603 result
2604}
2605
2606fn render_element(
2612 pixmap: &mut Pixmap,
2613 band_state: &mut BandState,
2614 element: &DisplayElement,
2615 ctx: &RenderContext<'_>,
2616) {
2617 match element {
2618 DisplayElement::Fill { path, params } => {
2619 let mut promoted_fill: Option<FillParams> = None;
2625 let params = maybe_promote_gray_fill(params, &mut promoted_fill);
2626 let painted = params.painted_channels;
2647 let subset_channels = painted != 0 && painted != stet_graphics::device::CMYK_ALL;
2648 let opm1_cmyk = params.is_device_cmyk && params.overprint_mode == 1;
2649 let custom_spot =
2650 painted == 0 && !params.is_device_cmyk && params.color.native_cmyk.is_some();
2651 let is_k_only_cmyk =
2657 params.is_device_cmyk && params.overprint_mode == 0 && is_k_only_src(¶ms.color);
2658 let needs_overprint = params.overprint
2659 && band_state.cmyk_buffer.is_some()
2660 && params.blend_mode == 0
2661 && (subset_channels || opm1_cmyk || custom_spot || is_k_only_cmyk);
2662
2663 if needs_overprint {
2664 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2665 let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
2666 let spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2667 render_overprint_fill(
2668 pixmap,
2669 &mut cmyk_buf,
2670 &mut op_bg,
2671 &mut op_touched,
2672 &spot_mask,
2673 band_state,
2674 path,
2675 params,
2676 ctx.vp_x,
2677 ctx.vp_y,
2678 ctx.scale_x,
2679 ctx.scale_y,
2680 ctx.out_w,
2681 ctx.out_h,
2682 ctx.icc,
2683 ctx.no_aa,
2684 );
2685 band_state.cmyk_buffer = Some(cmyk_buf);
2686 band_state.restore_op_buffers(op_bg, op_touched);
2687 band_state.restore_spot_mask(spot_mask);
2688 } else {
2689 let Some(skia_path) = build_skia_path(path) else {
2690 return;
2691 };
2692 let mut temp_mask = None;
2693 let Some(mask_ref) = resolve_clip_mask(
2694 &band_state.clip_region,
2695 &mut temp_mask,
2696 ctx.out_w,
2697 ctx.out_h,
2698 ) else {
2699 return;
2700 };
2701 let paint =
2702 to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, ctx.no_aa);
2703 let transform = ctx.transform(¶ms.ctm);
2704
2705 if is_degenerate_fill(path) {
2711 let stroke = Stroke {
2712 width: 1.0,
2713 ..Stroke::default()
2714 };
2715 pixmap.stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
2716 } else {
2717 let fill_rule = to_fill_rule(¶ms.fill_rule);
2718 pixmap.fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
2719 }
2720
2721 if band_state.cmyk_buffer.is_some() {
2723 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2724 let mut spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2725 update_cmyk_buffer_for_fill(
2726 &mut cmyk_buf,
2727 &mut spot_mask,
2728 path,
2729 params,
2730 ctx.vp_x,
2731 ctx.vp_y,
2732 ctx.scale_x,
2733 ctx.scale_y,
2734 ctx.out_w,
2735 ctx.out_h,
2736 &band_state.clip_region,
2737 ctx.no_aa,
2738 ctx.icc,
2739 );
2740 band_state.cmyk_buffer = Some(cmyk_buf);
2741 band_state.restore_spot_mask(spot_mask);
2742 }
2743 }
2744 }
2745 DisplayElement::Stroke { path, params } => {
2746 let mut promoted_stroke: Option<StrokeParams> = None;
2747 let params = maybe_promote_gray_stroke(params, &mut promoted_stroke);
2748 let transform = ctx.transform(¶ms.ctm);
2749 let vp_ctm = Matrix {
2752 a: transform.sx as f64,
2753 b: transform.ky as f64,
2754 c: transform.kx as f64,
2755 d: transform.sy as f64,
2756 tx: 0.0,
2757 ty: 0.0,
2758 };
2759 let vp_params = StrokeParams {
2760 ctm: vp_ctm,
2761 ..params.clone()
2762 };
2763 let stroke = build_stroke(&vp_params, ctx.effective_dpi);
2764
2765 let adjusted;
2767 let draw_path = if params.stroke_adjust
2768 && stroke.width <= 2.0
2769 && ctm_is_device_space(¶ms.ctm)
2770 {
2771 adjusted = stroke_adjust_path_viewport(
2772 path,
2773 stroke.width as f64,
2774 ctx.scale_x as f64,
2775 ctx.scale_y as f64,
2776 ctx.vp_x as f64,
2777 ctx.vp_y as f64,
2778 );
2779 &adjusted
2780 } else {
2781 path
2782 };
2783
2784 let painted = params.painted_channels;
2792 let subset_channels = painted != 0 && painted != stet_graphics::device::CMYK_ALL;
2793 let opm1_cmyk = params.is_device_cmyk && params.overprint_mode == 1;
2794 let custom_spot =
2795 painted == 0 && !params.is_device_cmyk && params.color.native_cmyk.is_some();
2796 let is_k_only_cmyk =
2797 params.is_device_cmyk && params.overprint_mode == 0 && is_k_only_src(¶ms.color);
2798 let needs_overprint = params.overprint
2799 && band_state.cmyk_buffer.is_some()
2800 && params.blend_mode == 0
2801 && (subset_channels || opm1_cmyk || custom_spot || is_k_only_cmyk);
2802
2803 let Some(skia_path) = build_skia_path(draw_path) else {
2804 return;
2805 };
2806 let mut temp_mask = None;
2807 let Some(mask_ref) = resolve_clip_mask(
2808 &band_state.clip_region,
2809 &mut temp_mask,
2810 ctx.out_w,
2811 ctx.out_h,
2812 ) else {
2813 return;
2814 };
2815
2816 if needs_overprint {
2817 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2822 let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
2823 let spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2824 render_overprint_stroke(
2825 pixmap,
2826 &mut cmyk_buf,
2827 &mut op_bg,
2828 &mut op_touched,
2829 &spot_mask,
2830 band_state,
2831 &skia_path,
2832 &stroke,
2833 transform,
2834 params,
2835 ctx.out_w,
2836 ctx.out_h,
2837 ctx.icc,
2838 ctx.no_aa,
2839 );
2840 band_state.cmyk_buffer = Some(cmyk_buf);
2841 band_state.restore_op_buffers(op_bg, op_touched);
2842 band_state.restore_spot_mask(spot_mask);
2843 } else {
2844 let paint =
2845 to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, ctx.no_aa);
2846 pixmap.stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
2847
2848 if band_state.cmyk_buffer.is_some() {
2849 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2850 let mut spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2851 update_cmyk_buffer_for_stroke(
2852 &mut cmyk_buf,
2853 &mut spot_mask,
2854 draw_path,
2855 params,
2856 &stroke,
2857 transform,
2858 ctx.out_w,
2859 ctx.out_h,
2860 &band_state.clip_region,
2861 ctx.no_aa,
2862 ctx.icc,
2863 );
2864 band_state.cmyk_buffer = Some(cmyk_buf);
2865 band_state.restore_spot_mask(spot_mask);
2866 }
2867 }
2868 }
2869 DisplayElement::Clip { path, params } => {
2870 clip_path_unified(band_state, path, params, ctx);
2871 }
2872 DisplayElement::InitClip => {
2873 if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
2874 band_state.recycle_mask(mask);
2875 }
2876 band_state.clip_region = None;
2877 }
2878 DisplayElement::ErasePage => {
2879 pixmap.fill(Color::TRANSPARENT);
2880 if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
2881 band_state.recycle_mask(mask);
2882 }
2883 band_state.clip_region = None;
2884 }
2885 DisplayElement::Image {
2886 sample_data,
2887 params,
2888 } => {
2889 let iw = params.width;
2890 let ih = params.height;
2891 if iw == 0 || ih == 0 {
2892 return;
2893 }
2894
2895 let needs_overprint = params.overprint
2896 && band_state.cmyk_buffer.is_some()
2897 && image_supports_overprint(¶ms.color_space);
2898
2899 if needs_overprint {
2900 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2901 let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
2902 render_overprint_image(
2903 pixmap,
2904 &mut cmyk_buf,
2905 &mut op_bg,
2906 &mut op_touched,
2907 band_state,
2908 sample_data,
2909 params,
2910 ctx.vp_x,
2911 ctx.vp_y,
2912 ctx.scale_x,
2913 ctx.scale_y,
2914 ctx.out_w,
2915 ctx.out_h,
2916 ctx.icc,
2917 );
2918 band_state.cmyk_buffer = Some(cmyk_buf);
2919 band_state.restore_op_buffers(op_bg, op_touched);
2920 } else if let Some(pp) = ctx
2921 .preprocessed
2922 .and_then(|pp| pp.get(ctx.elem_idx))
2923 .and_then(|e| e.as_ref())
2924 {
2925 let Some(image_inv) = params.image_matrix.invert() else {
2928 return;
2929 };
2930 let combined = params.ctm.concat(&image_inv);
2931 let raw_transform = ctx.transform(&combined);
2932 let transform = Transform::from_row(
2933 pp.adj_sx,
2934 pp.adj_ky,
2935 pp.adj_kx,
2936 pp.adj_sy,
2937 raw_transform.tx,
2938 raw_transform.ty,
2939 );
2940
2941 let Some(img_pixmap) =
2942 stet_tiny_skia::PixmapRef::from_bytes(&pp.data, pp.width, pp.height)
2943 else {
2944 return;
2945 };
2946 #[allow(unused_assignments)]
2947 let mut temp_mask = None;
2948 let mask_ref = match &band_state.clip_region {
2949 None => None,
2950 Some(ClipRegion::Mask(m)) => Some(m as &Mask),
2951 Some(ClipRegion::Rect(rect)) => {
2952 if rect.is_empty() {
2953 return;
2954 } else if rect.is_full_page(ctx.out_w, ctx.out_h) {
2955 None
2956 } else {
2957 temp_mask = rect.make_mask(ctx.out_w, ctx.out_h);
2958 temp_mask.as_ref()
2959 }
2960 }
2961 };
2962 let img_paint = stet_tiny_skia::PixmapPaint {
2963 quality: pp.quality,
2964 opacity: params.alpha as f32,
2965 blend_mode: u8_to_blend_mode(params.blend_mode),
2966 };
2967 pixmap.draw_pixmap(0, 0, img_pixmap, &img_paint, transform, mask_ref);
2968
2969 if let Some(ref mut cmyk_buf) = band_state.cmyk_buffer {
2975 update_cmyk_buffer_for_image(
2976 cmyk_buf,
2977 sample_data,
2978 pixmap.data(),
2979 params,
2980 ctx.vp_x,
2981 ctx.vp_y,
2982 ctx.scale_x,
2983 ctx.scale_y,
2984 ctx.out_w,
2985 ctx.out_h,
2986 &band_state.clip_region,
2987 ctx.icc,
2988 );
2989 }
2990 } else {
2991 let owned_rgba;
2993 let rgba_data: &[u8] = if let Some(cached) =
2994 ctx.image_cache.and_then(|c| c.get(ctx.elem_idx))
2995 {
2996 cached
2997 } else {
2998 owned_rgba = {
2999 let mut rgba =
3000 samples_to_rgba(sample_data, params, ctx.icc, ctx.opm_zero_transparent);
3001 if params.mask_color.is_some() {
3002 apply_mask_color_rgba(&mut rgba, sample_data, params);
3003 }
3004 rgba
3005 };
3006 &owned_rgba
3007 };
3008 let expected = (iw * ih * 4) as usize;
3009 if rgba_data.len() < expected {
3010 return;
3011 }
3012 let Some(image_inv) = params.image_matrix.invert() else {
3013 return;
3014 };
3015 let combined = params.ctm.concat(&image_inv);
3016 let raw_transform = enforce_min_image_size(ctx.transform(&combined), iw, ih);
3017
3018 let prescaled =
3023 prescale_image(rgba_data, iw, ih, raw_transform, params.interpolate);
3024 let (img_data, img_w, img_h, transform) = match &prescaled {
3025 Some((data, w, h, t)) => (data.as_slice(), *w, *h, *t),
3026 None => (rgba_data, iw, ih, raw_transform),
3027 };
3028
3029 let Some(img_pixmap) =
3030 stet_tiny_skia::PixmapRef::from_bytes(img_data, img_w, img_h)
3031 else {
3032 return;
3033 };
3034 #[allow(unused_assignments)]
3035 let mut temp_mask = None;
3036 let mask_ref = match &band_state.clip_region {
3037 None => None,
3038 Some(ClipRegion::Mask(m)) => Some(m as &Mask),
3039 Some(ClipRegion::Rect(rect)) => {
3040 if rect.is_empty() {
3041 return;
3042 } else if rect.is_full_page(ctx.out_w, ctx.out_h) {
3043 None
3044 } else {
3045 temp_mask = rect.make_mask(ctx.out_w, ctx.out_h);
3046 temp_mask.as_ref()
3047 }
3048 }
3049 };
3050 let img_paint = stet_tiny_skia::PixmapPaint {
3051 quality: image_filter_quality(transform, params.interpolate),
3052 opacity: params.alpha as f32,
3053 blend_mode: u8_to_blend_mode(params.blend_mode),
3054 };
3055 pixmap.draw_pixmap(0, 0, img_pixmap, &img_paint, transform, mask_ref);
3056
3057 if let Some(ref mut cmyk_buf) = band_state.cmyk_buffer {
3061 update_cmyk_buffer_for_image(
3062 cmyk_buf,
3063 sample_data,
3064 pixmap.data(),
3065 params,
3066 ctx.vp_x,
3067 ctx.vp_y,
3068 ctx.scale_x,
3069 ctx.scale_y,
3070 ctx.out_w,
3071 ctx.out_h,
3072 &band_state.clip_region,
3073 ctx.icc,
3074 );
3075 }
3076 }
3077 }
3078 DisplayElement::AxialShading { params } => {
3079 let mut temp_mask = None;
3080 let Some(mask_ref) = resolve_clip_mask(
3081 &band_state.clip_region,
3082 &mut temp_mask,
3083 ctx.out_w,
3084 ctx.out_h,
3085 ) else {
3086 return;
3087 };
3088 render_axial_shading(
3089 pixmap,
3090 params,
3091 ctx.vp_x,
3092 ctx.vp_y,
3093 ctx.scale_x,
3094 ctx.scale_y,
3095 mask_ref,
3096 ctx.no_aa,
3097 band_state.cmyk_buffer.as_deref_mut(),
3098 ctx.icc,
3099 );
3100 }
3101 DisplayElement::RadialShading { params } => {
3102 let mut temp_mask = None;
3103 let Some(mask_ref) = resolve_clip_mask(
3104 &band_state.clip_region,
3105 &mut temp_mask,
3106 ctx.out_w,
3107 ctx.out_h,
3108 ) else {
3109 return;
3110 };
3111 render_radial_shading(
3112 pixmap,
3113 params,
3114 ctx.vp_x,
3115 ctx.vp_y,
3116 ctx.scale_x,
3117 ctx.scale_y,
3118 mask_ref,
3119 ctx.no_aa,
3120 band_state.cmyk_buffer.as_deref_mut(),
3121 ctx.icc,
3122 );
3123 }
3124 DisplayElement::MeshShading { params } => {
3125 let mut temp_mask = None;
3126 let Some(mask_ref) = resolve_clip_mask(
3127 &band_state.clip_region,
3128 &mut temp_mask,
3129 ctx.out_w,
3130 ctx.out_h,
3131 ) else {
3132 return;
3133 };
3134 render_mesh_shading(
3135 pixmap,
3136 params,
3137 ctx.vp_x,
3138 ctx.vp_y,
3139 ctx.scale_x,
3140 ctx.scale_y,
3141 mask_ref,
3142 band_state.cmyk_buffer.as_deref_mut(),
3143 ctx.icc,
3144 );
3145 }
3146 DisplayElement::PatchShading { params } => {
3147 let mut temp_mask = None;
3148 let Some(mask_ref) = resolve_clip_mask(
3149 &band_state.clip_region,
3150 &mut temp_mask,
3151 ctx.out_w,
3152 ctx.out_h,
3153 ) else {
3154 return;
3155 };
3156 render_patch_shading(
3157 pixmap,
3158 params,
3159 ctx.vp_x,
3160 ctx.vp_y,
3161 ctx.scale_x,
3162 ctx.scale_y,
3163 mask_ref,
3164 band_state.cmyk_buffer.as_deref_mut(),
3165 ctx.icc,
3166 );
3167 }
3168 DisplayElement::PatternFill { params } => {
3169 render_pattern_fill(pixmap, band_state, params, ctx);
3170 }
3171 DisplayElement::Group { elements, params } => {
3172 render_group(pixmap, band_state, elements, params, ctx);
3173 }
3174 DisplayElement::SoftMasked {
3175 mask,
3176 content,
3177 params,
3178 mask_cache,
3179 } => {
3180 render_soft_masked(pixmap, band_state, mask, content, params, mask_cache, ctx);
3181 }
3182 DisplayElement::Text { .. } => {} DisplayElement::OcgGroup {
3184 elements,
3185 default_visible,
3186 ..
3187 } => {
3188 let visible = *default_visible;
3194 for (idx, elem) in elements.elements().iter().enumerate() {
3195 if !visible
3196 && !matches!(elem, DisplayElement::Clip { .. } | DisplayElement::InitClip)
3197 {
3198 continue;
3199 }
3200 let elem_ctx = RenderContext {
3201 elem_idx: idx,
3202 ..*ctx
3203 };
3204 render_element(pixmap, band_state, elem, &elem_ctx);
3205 }
3206 }
3207 }
3208}
3209
3210fn compute_group_crop(bbox: &[f64; 4], ctx: &RenderContext<'_>) -> Option<(i32, i32, u32, u32)> {
3215 let px_min = ((bbox[0] as f32 - ctx.vp_x) * ctx.scale_x).floor() as i32;
3217 let py_min = ((bbox[1] as f32 - ctx.vp_y) * ctx.scale_y).floor() as i32;
3218 let px_max = ((bbox[2] as f32 - ctx.vp_x) * ctx.scale_x).ceil() as i32;
3219 let py_max = ((bbox[3] as f32 - ctx.vp_y) * ctx.scale_y).ceil() as i32;
3220
3221 let x0 = px_min.max(0);
3223 let y0 = py_min.max(0);
3224 let x1 = px_max.min(ctx.out_w as i32);
3225 let y1 = py_max.min(ctx.out_h as i32);
3226
3227 if x0 >= x1 || y0 >= y1 {
3228 return None;
3229 }
3230
3231 let crop_w = (x1 - x0) as u32;
3232 let crop_h = (y1 - y0) as u32;
3233
3234 let crop_pixels = crop_w as u64 * crop_h as u64;
3236 let full_pixels = ctx.out_w as u64 * ctx.out_h as u64;
3237 if crop_pixels * 4 >= full_pixels * 3 {
3238 return None;
3239 }
3240
3241 Some((x0, y0, crop_w, crop_h))
3242}
3243
3244fn blend_cmyk_separable_channel(cb: f64, cs: f64, mode: u8) -> f64 {
3248 let cbi = 1.0 - cb;
3249 let csi = 1.0 - cs;
3250 let result_inv = match mode {
3251 1 => cbi * csi, 2 => cbi + csi - cbi * csi, 3 => {
3254 if cbi <= 0.5 {
3256 2.0 * cbi * csi
3257 } else {
3258 1.0 - 2.0 * (1.0 - cbi) * (1.0 - csi)
3259 }
3260 }
3261 4 => cbi.min(csi), 5 => cbi.max(csi), 6 => {
3264 if csi >= 1.0 {
3266 1.0
3267 } else {
3268 (cbi / (1.0 - csi)).min(1.0)
3269 }
3270 }
3271 7 => {
3272 if csi <= 0.0 {
3274 0.0
3275 } else {
3276 1.0 - ((1.0 - cbi) / csi).min(1.0)
3277 }
3278 }
3279 8 => {
3280 if csi <= 0.5 {
3282 2.0 * cbi * csi
3283 } else {
3284 1.0 - 2.0 * (1.0 - cbi) * (1.0 - csi)
3285 }
3286 }
3287 9 => {
3288 let d = if cbi <= 0.25 {
3290 ((16.0 * cbi - 12.0) * cbi + 4.0) * cbi
3291 } else {
3292 cbi.sqrt()
3293 };
3294 if csi <= 0.5 {
3295 cbi - (1.0 - 2.0 * csi) * cbi * (1.0 - cbi)
3296 } else {
3297 cbi + (2.0 * csi - 1.0) * (d - cbi)
3298 }
3299 }
3300 10 => (cbi - csi).abs(), 11 => cbi + csi - 2.0 * cbi * csi, _ => csi, };
3304 1.0 - result_inv.clamp(0.0, 1.0)
3305}
3306
3307fn blend_cmyk_nonseparable(cb: [f64; 4], cs: [f64; 4], mode: u8) -> [f64; 4] {
3313 fn lum(c: [f64; 3]) -> f64 {
3314 0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
3315 }
3316 fn clip_color(mut c: [f64; 3]) -> [f64; 3] {
3317 let l = lum(c);
3318 let n = c[0].min(c[1]).min(c[2]);
3319 let x = c[0].max(c[1]).max(c[2]);
3320 if n < 0.0 {
3321 for ci in c.iter_mut() {
3322 *ci = l + (*ci - l) * l / (l - n);
3323 }
3324 }
3325 if x > 1.0 {
3326 for ci in c.iter_mut() {
3327 *ci = l + (*ci - l) * (1.0 - l) / (x - l);
3328 }
3329 }
3330 c
3331 }
3332 fn set_lum(c: [f64; 3], l: f64) -> [f64; 3] {
3333 let d = l - lum(c);
3334 clip_color([c[0] + d, c[1] + d, c[2] + d])
3335 }
3336 fn sat(c: [f64; 3]) -> f64 {
3337 c[0].max(c[1]).max(c[2]) - c[0].min(c[1]).min(c[2])
3338 }
3339 fn set_sat(c: [f64; 3], s: f64) -> [f64; 3] {
3340 let mut idx = [0usize, 1, 2];
3342 idx.sort_by(|a, b| {
3343 c[*a]
3344 .partial_cmp(&c[*b])
3345 .unwrap_or(std::cmp::Ordering::Equal)
3346 });
3347 let (i_min, i_mid, i_max) = (idx[0], idx[1], idx[2]);
3348 let mut out = c;
3349 if c[i_max] > c[i_min] {
3350 out[i_mid] = (c[i_mid] - c[i_min]) * s / (c[i_max] - c[i_min]);
3351 out[i_max] = s;
3352 } else {
3353 out[i_mid] = 0.0;
3354 out[i_max] = 0.0;
3355 }
3356 out[i_min] = 0.0;
3357 out
3358 }
3359
3360 let cb_rgb = [1.0 - cb[0], 1.0 - cb[1], 1.0 - cb[2]];
3361 let cs_rgb = [1.0 - cs[0], 1.0 - cs[1], 1.0 - cs[2]];
3362 let result_rgb = match mode {
3363 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,
3368 };
3369 let result_k = if mode == 15 { cs[3] } else { cb[3] };
3373 [
3374 (1.0 - result_rgb[0]).clamp(0.0, 1.0),
3375 (1.0 - result_rgb[1]).clamp(0.0, 1.0),
3376 (1.0 - result_rgb[2]).clamp(0.0, 1.0),
3377 result_k,
3378 ]
3379}
3380
3381fn ps_path_bbox(path: &PsPath) -> Option<(f64, f64, f64, f64)> {
3386 let mut it = path.segments.iter().filter_map(|seg| match *seg {
3387 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => Some(vec![(x, y)]),
3388 PathSegment::CurveTo {
3389 x1,
3390 y1,
3391 x2,
3392 y2,
3393 x3,
3394 y3,
3395 } => Some(vec![(x1, y1), (x2, y2), (x3, y3)]),
3396 PathSegment::ClosePath => None,
3397 });
3398 let first = it.next()?.into_iter().next()?;
3399 let (mut x0, mut y0) = first;
3400 let (mut x1, mut y1) = first;
3401 for seg_points in std::iter::once(vec![first]).chain(it) {
3402 for (x, y) in seg_points {
3403 x0 = x0.min(x);
3404 y0 = y0.min(y);
3405 x1 = x1.max(x);
3406 y1 = y1.max(y);
3407 }
3408 }
3409 Some((x0, y0, x1, y1))
3410}
3411
3412fn bbox_contains(outer: (f64, f64, f64, f64), inner: (f64, f64, f64, f64), tolerance: f64) -> bool {
3415 inner.0 >= outer.0 - tolerance
3416 && inner.1 >= outer.1 - tolerance
3417 && inner.2 <= outer.2 + tolerance
3418 && inner.3 <= outer.3 + tolerance
3419}
3420
3421fn compute_obscured_fill_skips(elements: &DisplayList) -> Vec<usize> {
3447 let mut skips = Vec::new();
3448 let els = elements.elements();
3449 for i in 0..els.len() {
3450 let DisplayElement::Fill {
3451 path: parent_path,
3452 params: parent_params,
3453 } = &els[i]
3454 else {
3455 continue;
3456 };
3457 if (parent_params.alpha - 1.0).abs() > 1e-6 || parent_params.blend_mode != 0 {
3458 continue;
3459 }
3460 let Some(parent_bbox) = ps_path_bbox(parent_path) else {
3461 continue;
3462 };
3463 let mut j = i + 1;
3467 let mut clips_ok = true;
3468 while j < els.len() {
3469 match &els[j] {
3470 DisplayElement::InitClip => {}
3471 DisplayElement::Clip {
3472 path: clip_path, ..
3473 } => match ps_path_bbox(clip_path) {
3474 Some(cb) if bbox_contains(cb, parent_bbox, 0.5) => {}
3475 _ => {
3476 clips_ok = false;
3477 break;
3478 }
3479 },
3480 _ => break,
3481 }
3482 j += 1;
3483 }
3484 if !clips_ok {
3485 continue;
3486 }
3487 let Some(DisplayElement::Group {
3488 elements: group_elements,
3489 params: group_params,
3490 }) = els.get(j)
3491 else {
3492 continue;
3493 };
3494 if !group_params.isolated
3495 || (group_params.alpha - 1.0).abs() > 1e-6
3496 || group_params.blend_mode != 0
3497 {
3498 continue;
3499 }
3500 let group_bbox = (
3504 group_params.bbox[0],
3505 group_params.bbox[1],
3506 group_params.bbox[2],
3507 group_params.bbox[3],
3508 );
3509 if !bbox_contains(group_bbox, parent_bbox, 0.5) {
3510 continue;
3511 }
3512 let inner_els = group_elements.elements();
3515 let mut k = 0;
3516 let mut inner_clips_ok = true;
3517 while k < inner_els.len() {
3518 match &inner_els[k] {
3519 DisplayElement::InitClip => {}
3520 DisplayElement::Clip {
3521 path: clip_path, ..
3522 } => match ps_path_bbox(clip_path) {
3523 Some(cb) if bbox_contains(cb, parent_bbox, 0.5) => {}
3524 _ => {
3525 inner_clips_ok = false;
3526 break;
3527 }
3528 },
3529 _ => break,
3530 }
3531 k += 1;
3532 }
3533 if !inner_clips_ok {
3534 continue;
3535 }
3536 let Some(DisplayElement::Fill {
3537 path: group_path,
3538 params: group_fill_params,
3539 }) = inner_els.get(k)
3540 else {
3541 continue;
3542 };
3543 if (group_fill_params.alpha - 1.0).abs() > 1e-6 || group_fill_params.blend_mode != 0 {
3544 continue;
3545 }
3546 if paths_approximately_equal(parent_path, group_path, 0.5) {
3547 skips.push(i);
3548 }
3549 }
3550 skips
3551}
3552
3553fn paths_approximately_equal(a: &PsPath, b: &PsPath, tolerance: f64) -> bool {
3559 if a.segments.len() != b.segments.len() {
3560 return false;
3561 }
3562 for (sa, sb) in a.segments.iter().zip(b.segments.iter()) {
3563 let close_pair = |(x1, y1): (f64, f64), (x2, y2): (f64, f64)| -> bool {
3564 (x1 - x2).abs() <= tolerance && (y1 - y2).abs() <= tolerance
3565 };
3566 match (sa, sb) {
3567 (PathSegment::MoveTo(x1, y1), PathSegment::MoveTo(x2, y2)) => {
3568 if !close_pair((*x1, *y1), (*x2, *y2)) {
3569 return false;
3570 }
3571 }
3572 (PathSegment::LineTo(x1, y1), PathSegment::LineTo(x2, y2)) => {
3573 if !close_pair((*x1, *y1), (*x2, *y2)) {
3574 return false;
3575 }
3576 }
3577 (
3578 PathSegment::CurveTo {
3579 x1: ax1,
3580 y1: ay1,
3581 x2: ax2,
3582 y2: ay2,
3583 x3: ax3,
3584 y3: ay3,
3585 },
3586 PathSegment::CurveTo {
3587 x1: bx1,
3588 y1: by1,
3589 x2: bx2,
3590 y2: by2,
3591 x3: bx3,
3592 y3: by3,
3593 },
3594 ) => {
3595 if !close_pair((*ax1, *ay1), (*bx1, *by1))
3596 || !close_pair((*ax2, *ay2), (*bx2, *by2))
3597 || !close_pair((*ax3, *ay3), (*bx3, *by3))
3598 {
3599 return false;
3600 }
3601 }
3602 (PathSegment::ClosePath, PathSegment::ClosePath) => {}
3603 _ => return false,
3604 }
3605 }
3606 true
3607}
3608
3609fn render_group(
3613 pixmap: &mut Pixmap,
3614 band_state: &mut BandState,
3615 elements: &DisplayList,
3616 params: &stet_graphics::display_list::GroupParams,
3617 ctx: &RenderContext<'_>,
3618) {
3619 if params.knockout {
3620 render_knockout_group(pixmap, band_state, elements, params, ctx);
3621 return;
3622 }
3623
3624 let crop = compute_group_crop(¶ms.bbox, ctx);
3625
3626 let (eff_w, eff_h, crop_x, crop_y, eff_vp_x, eff_vp_y) = match crop {
3627 Some((cx, cy, cw, ch)) => (
3628 cw,
3629 ch,
3630 cx,
3631 cy,
3632 ctx.vp_x + cx as f32 / ctx.scale_x,
3633 ctx.vp_y + cy as f32 / ctx.scale_y,
3634 ),
3635 None => (ctx.out_w, ctx.out_h, 0, 0, ctx.vp_x, ctx.vp_y),
3636 };
3637
3638 let Some(mut offscreen) = Pixmap::new(eff_w, eff_h) else {
3639 return;
3640 };
3641
3642 use stet_graphics::display_list::GroupColorSpace;
3650
3651 let needs_group_cmyk = has_overprint_elements(elements)
3657 || band_state.cmyk_buffer.is_some()
3658 || params.color_space == GroupColorSpace::DeviceCMYK
3659 || has_cmyk_group(elements);
3660
3661 let force_cmyk_compose =
3668 std::env::var_os("STET_FORCE_CMYK_COMPOSITE_BACK").as_deref() == Some("1".as_ref());
3669 let plan_cmyk_compose = match ctx.knockout_painter_pass {
3680 KnockoutPainterPass::CoveragePass => false,
3681 KnockoutPainterPass::ColorPass => {
3682 !params.isolated
3683 && params.blend_mode != 0
3684 && needs_group_cmyk
3685 && band_state.cmyk_buffer.is_some()
3686 && group_content_is_native_cmyk(elements)
3687 }
3688 KnockoutPainterPass::None if force_cmyk_compose => {
3689 !params.isolated
3707 && matches!(params.blend_mode, 10..=15)
3708 && needs_group_cmyk
3709 && band_state.cmyk_buffer.is_some()
3710 && group_content_is_native_cmyk(elements)
3711 }
3712 KnockoutPainterPass::None => {
3713 let inversion_sensitive = !params.isolated
3718 && matches!(params.blend_mode, 10..=15)
3719 && group_only_native_cmyk_fills(elements);
3720 let cmyk_group_blend = !params.isolated
3735 && ctx.parent_group_isolated
3736 && params.blend_mode != 0
3737 && params.color_space == GroupColorSpace::DeviceCMYK
3738 && needs_group_cmyk
3739 && band_state.cmyk_buffer.is_some()
3740 && group_content_is_native_cmyk(elements);
3741 inversion_sensitive || cmyk_group_blend
3742 }
3743 };
3744 let needs_alpha_extraction = !params.isolated
3749 && params.blend_mode != 0
3750 && !plan_cmyk_compose
3751 && !ctx.alpha_extraction_pass;
3752 let needs_backdrop_preload =
3753 !params.isolated && (params.blend_mode == 0 || plan_cmyk_compose || needs_alpha_extraction);
3754 let backdrop = if needs_backdrop_preload {
3755 let data = if crop.is_some() {
3756 copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h)
3757 } else {
3758 pixmap.data().to_vec()
3759 };
3760 offscreen.data_mut().copy_from_slice(&data);
3761 Some(data)
3762 } else {
3763 None
3764 };
3765 let group_cmyk = if needs_group_cmyk {
3766 let buf_size = eff_w as usize * eff_h as usize * 4;
3767 let mut buf = vec![0.0f32; buf_size];
3768 if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
3769 let parent_stride = ctx.out_w as usize * 4;
3770 let group_stride = eff_w as usize * 4;
3771 for gy in 0..eff_h as usize {
3772 let py = crop_y as usize + gy;
3773 if py < ctx.out_h as usize {
3774 let p_start = py * parent_stride + crop_x as usize * 4;
3775 let g_start = gy * group_stride;
3776 let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
3777 buf[g_start..g_start + copy_len]
3778 .copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
3779 }
3780 }
3781 }
3782 Some(buf)
3783 } else {
3784 None
3785 };
3786
3787 let backdrop_cmyk: Option<Vec<f32>> = if !params.isolated {
3792 group_cmyk.clone()
3793 } else {
3794 None
3795 };
3796
3797 let mut group_band = BandState {
3798 clip_region: None,
3799 spare_mask: None,
3800 clip_mask_cache: HashMap::new(),
3801 clip_mask_seen: HashSet::new(),
3802 mask_pool: Vec::new(),
3803 cmyk_buffer: group_cmyk,
3804 op_bg_snapshot: None,
3805 op_touched: None,
3806 spot_mask: None,
3807 };
3808
3809 let group_ctx = RenderContext {
3810 vp_x: eff_vp_x,
3811 vp_y: eff_vp_y,
3812 scale_x: ctx.scale_x,
3813 scale_y: ctx.scale_y,
3814 out_w: eff_w,
3815 out_h: eff_h,
3816 effective_dpi: ctx.effective_dpi,
3817 icc: ctx.icc,
3818 image_cache: None, preprocessed: None,
3820 elem_idx: 0,
3821 no_aa: ctx.no_aa,
3822 opm_zero_transparent: ctx.opm_zero_transparent,
3823 knockout_painter_pass: ctx.knockout_painter_pass,
3824 parent_group_isolated: params.isolated,
3826 alpha_extraction_pass: ctx.alpha_extraction_pass,
3827 };
3828
3829 let skip_indices = compute_obscured_fill_skips(elements);
3830 for (idx, elem) in elements.elements().iter().enumerate() {
3831 if skip_indices.contains(&idx) {
3832 continue;
3833 }
3834 let elem_ctx = RenderContext {
3835 elem_idx: idx,
3836 ..group_ctx
3837 };
3838 render_element(&mut offscreen, &mut group_band, elem, &elem_ctx);
3839 }
3840
3841 let alpha_offscreen = if needs_alpha_extraction {
3845 let mut iso = Pixmap::new(eff_w, eff_h);
3846 if let Some(ref mut iso_pm) = iso {
3847 let mut iso_band = BandState {
3848 clip_region: None,
3849 spare_mask: None,
3850 clip_mask_cache: HashMap::new(),
3851 clip_mask_seen: HashSet::new(),
3852 mask_pool: Vec::new(),
3853 cmyk_buffer: None,
3854 op_bg_snapshot: None,
3855 op_touched: None,
3856 spot_mask: None,
3857 };
3858 let iso_ctx = RenderContext {
3859 parent_group_isolated: true,
3860 alpha_extraction_pass: true,
3861 ..group_ctx
3862 };
3863 for (idx, elem) in elements.elements().iter().enumerate() {
3864 let elem_ctx = RenderContext {
3865 elem_idx: idx,
3866 ..iso_ctx
3867 };
3868 render_element(iso_pm, &mut iso_band, elem, &elem_ctx);
3869 }
3870 }
3871 iso
3872 } else {
3873 None
3874 };
3875
3876 let mut temp_mask = None;
3877 let mask_ref = match resolve_clip_mask(
3878 &band_state.clip_region,
3879 &mut temp_mask,
3880 ctx.out_w,
3881 ctx.out_h,
3882 ) {
3883 None => return, Some(m) => m,
3885 };
3886
3887 let coverage_params;
3892 let effective_params: &stet_graphics::display_list::GroupParams =
3893 if ctx.knockout_painter_pass == KnockoutPainterPass::CoveragePass {
3894 coverage_params = stet_graphics::display_list::GroupParams {
3895 alpha: 1.0,
3896 blend_mode: 0,
3897 ..params.clone()
3898 };
3899 &coverage_params
3900 } else {
3901 params
3902 };
3903
3904 let mut cmyk_compose_done = false;
3905 if let Some(backdrop) = &backdrop {
3906 let inner_cmyk = group_band.cmyk_buffer.as_deref();
3924 let pre_cmyk = backdrop_cmyk.as_deref();
3925 if plan_cmyk_compose && let (Some(inner), Some(pre)) = (inner_cmyk, pre_cmyk) {
3926 composite_non_isolated_cmyk(
3927 pixmap,
3928 band_state.cmyk_buffer.as_deref_mut(),
3929 &offscreen,
3930 inner,
3931 pre,
3932 backdrop,
3933 effective_params,
3934 mask_ref,
3935 crop_x,
3936 crop_y,
3937 ctx.icc,
3938 );
3939 cmyk_compose_done = true;
3940 } else if let Some(ref alpha_os) = alpha_offscreen {
3941 composite_non_isolated_extracted(
3942 pixmap,
3943 &offscreen,
3944 alpha_os,
3945 backdrop,
3946 effective_params,
3947 mask_ref,
3948 crop_x,
3949 crop_y,
3950 );
3951 } else {
3952 composite_non_isolated_group_cropped(
3953 pixmap,
3954 &offscreen,
3955 backdrop,
3956 effective_params,
3957 mask_ref,
3958 crop_x,
3959 crop_y,
3960 );
3961 }
3962 } else {
3963 let paint = stet_tiny_skia::PixmapPaint {
3964 opacity: effective_params.alpha as f32,
3965 blend_mode: u8_to_blend_mode(effective_params.blend_mode),
3966 quality: stet_tiny_skia::FilterQuality::Nearest,
3967 };
3968 pixmap.draw_pixmap(
3969 crop_x,
3970 crop_y,
3971 offscreen.as_ref(),
3972 &paint,
3973 Transform::identity(),
3974 mask_ref,
3975 );
3976 }
3977
3978 if !cmyk_compose_done
3984 && let (Some(group_cmyk), Some(parent_cmyk)) =
3985 (&group_band.cmyk_buffer, &mut band_state.cmyk_buffer)
3986 {
3987 copy_cmyk_buffer_to_parent(
3988 parent_cmyk,
3989 group_cmyk,
3990 offscreen.data(),
3991 crop_x as usize,
3992 crop_y as usize,
3993 eff_w as usize,
3994 eff_h as usize,
3995 ctx.out_w as usize,
3996 ctx.out_h as usize,
3997 );
3998 }
3999}
4000
4001#[allow(clippy::too_many_arguments)]
4013fn composite_non_isolated_cmyk(
4014 target: &mut Pixmap,
4015 parent_cmyk: Option<&mut [f32]>,
4016 source: &Pixmap,
4017 source_cmyk: &[f32],
4018 backdrop_cmyk: &[f32],
4019 backdrop_pixels: &[u8],
4020 params: &stet_graphics::display_list::GroupParams,
4021 clip_mask: Option<&stet_tiny_skia::Mask>,
4022 crop_x: i32,
4023 crop_y: i32,
4024 icc: Option<&IccCache>,
4025) {
4026 let cw = source.width() as usize;
4027 let ch = source.height() as usize;
4028 let target_w = target.width() as usize;
4029 let target_h = target.height() as usize;
4030
4031 let opacity = params.alpha.clamp(0.0, 1.0);
4032 let blend_mode = params.blend_mode;
4033 let is_nonseparable = matches!(blend_mode, 12..=15);
4034
4035 let target_data = target.data_mut();
4036 let target_stride = target_w * 4;
4037 let group_stride = cw * 4;
4038
4039 let clip_data = clip_mask.map(|m| m.data());
4040
4041 for gy in 0..ch {
4042 let ty = crop_y + gy as i32;
4043 if ty < 0 || ty as usize >= target_h {
4044 continue;
4045 }
4046 let ty = ty as usize;
4047 let group_row = gy * group_stride;
4048 let target_row = ty * target_stride;
4049
4050 for gx in 0..cw {
4051 let tx = crop_x + gx as i32;
4052 if tx < 0 || tx as usize >= target_w {
4053 continue;
4054 }
4055 let tx = tx as usize;
4056 let gi = group_row + gx * 4;
4057 let ti = target_row + tx * 4;
4058
4059 let bc = backdrop_cmyk[gi] as f64;
4061 let bm = backdrop_cmyk[gi + 1] as f64;
4062 let by_ = backdrop_cmyk[gi + 2] as f64;
4063 let bk = backdrop_cmyk[gi + 3] as f64;
4064 let sc = source_cmyk[gi] as f64;
4065 let sm = source_cmyk[gi + 1] as f64;
4066 let sy_ = source_cmyk[gi + 2] as f64;
4067 let sk = source_cmyk[gi + 3] as f64;
4068 if (sc - bc).abs() < 1.0 / 255.0
4069 && (sm - bm).abs() < 1.0 / 255.0
4070 && (sy_ - by_).abs() < 1.0 / 255.0
4071 && (sk - bk).abs() < 1.0 / 255.0
4072 {
4073 continue;
4074 }
4075
4076 let cov = if let Some(cd) = clip_data {
4078 cd[ty * target_w + tx] as f64 / 255.0
4079 } else {
4080 1.0
4081 };
4082 if cov <= 0.0 {
4083 continue;
4084 }
4085
4086 let backdrop_alpha = backdrop_pixels[gi + 3];
4105 let backdrop_transparent = backdrop_alpha == 0;
4106
4107 let mix = cov * opacity;
4108 let dst_a = target_data[ti + 3] as f64 / 255.0;
4109
4110 if backdrop_transparent {
4111 let src_data = source.data();
4124 let src_a_pm = src_data[gi + 3] as f64 / 255.0;
4125 if src_a_pm <= 0.0 {
4126 continue;
4127 }
4128 let (full_r, full_g, full_b) = icc
4133 .and_then(|i| i.convert_cmyk_readonly(sc, sm, sy_, sk))
4134 .unwrap_or_else(|| cmyk_to_rgb_plrm(sc, sm, sy_, sk));
4135 let alpha_s = mix;
4136 let inv_sa = 1.0 - alpha_s;
4137 let dst_r_pm = target_data[ti] as f64 / 255.0;
4138 let dst_g_pm = target_data[ti + 1] as f64 / 255.0;
4139 let dst_b_pm = target_data[ti + 2] as f64 / 255.0;
4140 let out_r = full_r * alpha_s + dst_r_pm * inv_sa;
4141 let out_g = full_g * alpha_s + dst_g_pm * inv_sa;
4142 let out_b = full_b * alpha_s + dst_b_pm * inv_sa;
4143 let out_a = alpha_s + dst_a * inv_sa;
4144 target_data[ti] = (out_r * 255.0).round().clamp(0.0, 255.0) as u8;
4145 target_data[ti + 1] = (out_g * 255.0).round().clamp(0.0, 255.0) as u8;
4146 target_data[ti + 2] = (out_b * 255.0).round().clamp(0.0, 255.0) as u8;
4147 target_data[ti + 3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4148 continue;
4149 }
4150
4151 let (rc, rm, ry, rk) = if is_nonseparable {
4153 let r = blend_cmyk_nonseparable([bc, bm, by_, bk], [sc, sm, sy_, sk], blend_mode);
4154 (r[0], r[1], r[2], r[3])
4155 } else {
4156 (
4157 blend_cmyk_separable_channel(bc, sc, blend_mode),
4158 blend_cmyk_separable_channel(bm, sm, blend_mode),
4159 blend_cmyk_separable_channel(by_, sy_, blend_mode),
4160 blend_cmyk_separable_channel(bk, sk, blend_mode),
4161 )
4162 };
4163
4164 let (new_r, new_g, new_b) = icc
4165 .and_then(|i| i.convert_cmyk_readonly(rc, rm, ry, rk))
4166 .unwrap_or_else(|| cmyk_to_rgb_plrm(rc, rm, ry, rk));
4167
4168 let alpha_s = mix;
4181 let alpha_b = dst_a;
4182 let out_a = alpha_s + alpha_b * (1.0 - alpha_s);
4183 if out_a <= 0.0 {
4184 continue;
4185 }
4186 let (dst_r, dst_g, dst_b) = if alpha_b > 0.0 {
4187 let inv_a = 1.0 / alpha_b;
4188 (
4189 (target_data[ti] as f64 / 255.0) * inv_a,
4190 (target_data[ti + 1] as f64 / 255.0) * inv_a,
4191 (target_data[ti + 2] as f64 / 255.0) * inv_a,
4192 )
4193 } else {
4194 (0.0, 0.0, 0.0)
4195 };
4196 let coef_b = alpha_s * alpha_b;
4204 let coef_s = alpha_s * (1.0 - alpha_b);
4205 let coef_d = (1.0 - alpha_s) * alpha_b;
4206 let out_r = (coef_s * new_r + coef_b * new_r + coef_d * dst_r) / out_a;
4207 let out_g = (coef_s * new_g + coef_b * new_g + coef_d * dst_g) / out_a;
4208 let out_b = (coef_s * new_b + coef_b * new_b + coef_d * dst_b) / out_a;
4209
4210 target_data[ti] = (out_r * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4211 target_data[ti + 1] = (out_g * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4212 target_data[ti + 2] = (out_b * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4213 target_data[ti + 3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4214 }
4215 }
4216
4217 if let Some(parent_cmyk) = parent_cmyk {
4222 for gy in 0..ch {
4223 let ty = crop_y + gy as i32;
4224 if ty < 0 || ty as usize >= target_h {
4225 continue;
4226 }
4227 let ty = ty as usize;
4228 let group_row = gy * group_stride;
4229 let parent_row = ty * target_stride;
4230
4231 for gx in 0..cw {
4232 let tx = crop_x + gx as i32;
4233 if tx < 0 || tx as usize >= target_w {
4234 continue;
4235 }
4236 let tx = tx as usize;
4237 let gi = group_row + gx * 4;
4238 let pi = parent_row + tx * 4;
4239
4240 let bc = backdrop_cmyk[gi] as f64;
4241 let bm = backdrop_cmyk[gi + 1] as f64;
4242 let by_ = backdrop_cmyk[gi + 2] as f64;
4243 let bk = backdrop_cmyk[gi + 3] as f64;
4244 let sc = source_cmyk[gi] as f64;
4245 let sm = source_cmyk[gi + 1] as f64;
4246 let sy_ = source_cmyk[gi + 2] as f64;
4247 let sk = source_cmyk[gi + 3] as f64;
4248 if (sc - bc).abs() < 1.0 / 255.0
4249 && (sm - bm).abs() < 1.0 / 255.0
4250 && (sy_ - by_).abs() < 1.0 / 255.0
4251 && (sk - bk).abs() < 1.0 / 255.0
4252 {
4253 continue;
4254 }
4255
4256 let backdrop_transparent = backdrop_pixels[gi + 3] == 0;
4262 let (rc, rm, ry, rk) = if backdrop_transparent {
4263 (sc, sm, sy_, sk)
4264 } else if is_nonseparable {
4265 let r =
4266 blend_cmyk_nonseparable([bc, bm, by_, bk], [sc, sm, sy_, sk], blend_mode);
4267 (r[0], r[1], r[2], r[3])
4268 } else {
4269 (
4270 blend_cmyk_separable_channel(bc, sc, blend_mode),
4271 blend_cmyk_separable_channel(bm, sm, blend_mode),
4272 blend_cmyk_separable_channel(by_, sy_, blend_mode),
4273 blend_cmyk_separable_channel(bk, sk, blend_mode),
4274 )
4275 };
4276 parent_cmyk[pi] = rc as f32;
4277 parent_cmyk[pi + 1] = rm as f32;
4278 parent_cmyk[pi + 2] = ry as f32;
4279 parent_cmyk[pi + 3] = rk as f32;
4280 }
4281 }
4282 }
4283}
4284
4285fn render_knockout_group(
4290 pixmap: &mut Pixmap,
4291 band_state: &mut BandState,
4292 elements: &DisplayList,
4293 params: &stet_graphics::display_list::GroupParams,
4294 ctx: &RenderContext<'_>,
4295) {
4296 let crop = compute_group_crop(¶ms.bbox, ctx);
4297
4298 let (eff_w, eff_h, crop_x, crop_y, eff_vp_x, eff_vp_y) = match crop {
4299 Some((cx, cy, cw, ch)) => (
4300 cw,
4301 ch,
4302 cx,
4303 cy,
4304 ctx.vp_x + cx as f32 / ctx.scale_x,
4305 ctx.vp_y + cy as f32 / ctx.scale_y,
4306 ),
4307 None => (ctx.out_w, ctx.out_h, 0, 0, ctx.vp_x, ctx.vp_y),
4308 };
4309
4310 let Some(mut offscreen) = Pixmap::new(eff_w, eff_h) else {
4311 return;
4312 };
4313
4314 let initial_backdrop = if !params.isolated {
4315 if crop.is_some() {
4316 copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h)
4317 } else {
4318 pixmap.data().to_vec()
4319 }
4320 } else {
4321 vec![0u8; (eff_w * eff_h * 4) as usize]
4322 };
4323
4324 let Some(mut accumulated) = Pixmap::new(eff_w, eff_h) else {
4325 return;
4326 };
4327 accumulated.data_mut().copy_from_slice(&initial_backdrop);
4328
4329 let needs_cmyk = has_overprint_elements(elements) || band_state.cmyk_buffer.is_some();
4331 let initial_cmyk = if needs_cmyk {
4332 let buf_size = eff_w as usize * eff_h as usize * 4;
4333 let mut buf = vec![0.0f32; buf_size];
4334 if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
4335 let parent_stride = ctx.out_w as usize * 4;
4336 let group_stride = eff_w as usize * 4;
4337 for gy in 0..eff_h as usize {
4338 let py = crop_y as usize + gy;
4339 if py < ctx.out_h as usize {
4340 let p_start = py * parent_stride + crop_x as usize * 4;
4341 let g_start = gy * group_stride;
4342 let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
4343 buf[g_start..g_start + copy_len]
4344 .copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
4345 }
4346 }
4347 }
4348 Some(buf)
4349 } else {
4350 None
4351 };
4352
4353 let mut accumulated_cmyk = initial_cmyk.clone();
4354
4355 let group_ctx = RenderContext {
4360 vp_x: eff_vp_x,
4361 vp_y: eff_vp_y,
4362 scale_x: ctx.scale_x,
4363 scale_y: ctx.scale_y,
4364 out_w: eff_w,
4365 out_h: eff_h,
4366 effective_dpi: ctx.effective_dpi,
4367 icc: ctx.icc,
4368 image_cache: None,
4369 preprocessed: None,
4370 elem_idx: 0,
4371 no_aa: true,
4372 opm_zero_transparent: ctx.opm_zero_transparent,
4373 knockout_painter_pass: ctx.knockout_painter_pass,
4374 parent_group_isolated: true,
4378 alpha_extraction_pass: false,
4379 };
4380
4381 let mut ko_band = BandState {
4385 clip_region: None,
4386 spare_mask: None,
4387 clip_mask_cache: HashMap::new(),
4388 clip_mask_seen: HashSet::new(),
4389 mask_pool: Vec::new(),
4390 cmyk_buffer: None,
4391 op_bg_snapshot: None,
4392 op_touched: None,
4393 spot_mask: None,
4394 };
4395
4396 let mut coverage_offscreen: Option<Pixmap> = None;
4399
4400 for elem in elements.elements() {
4401 match elem {
4402 DisplayElement::Clip { .. } | DisplayElement::InitClip => {
4404 render_element(&mut offscreen, &mut ko_band, elem, &group_ctx);
4405 }
4406 DisplayElement::Group { .. } => {
4416 let pass1_ctx = RenderContext {
4423 knockout_painter_pass: KnockoutPainterPass::ColorPass,
4424 ..group_ctx
4425 };
4426 offscreen.data_mut().copy_from_slice(&initial_backdrop);
4427 ko_band.cmyk_buffer = initial_cmyk.clone();
4428 render_element(&mut offscreen, &mut ko_band, elem, &pass1_ctx);
4429 let pass1_cmyk = ko_band.cmyk_buffer.take();
4430
4431 let cov = match coverage_offscreen.as_mut() {
4436 Some(p) => {
4437 p.data_mut().fill(0);
4438 p
4439 }
4440 None => {
4441 let Some(p) = Pixmap::new(eff_w, eff_h) else {
4442 replace_changed_pixels(
4446 accumulated.data_mut(),
4447 offscreen.data(),
4448 &initial_backdrop,
4449 );
4450 if let (Some(p1), Some(acc)) = (&pass1_cmyk, &mut accumulated_cmyk) {
4451 replace_changed_cmyk(acc, p1, offscreen.data(), &initial_backdrop);
4452 }
4453 continue;
4454 };
4455 coverage_offscreen = Some(p);
4456 coverage_offscreen.as_mut().unwrap()
4457 }
4458 };
4459 ko_band.cmyk_buffer = None;
4460 let coverage_ctx = RenderContext {
4465 knockout_painter_pass: KnockoutPainterPass::CoveragePass,
4466 ..group_ctx
4467 };
4468 render_element(cov, &mut ko_band, elem, &coverage_ctx);
4469
4470 replace_with_coverage_mask(accumulated.data_mut(), offscreen.data(), cov.data());
4474
4475 if let (Some(p1_cmyk), Some(acc_cmyk)) = (&pass1_cmyk, &mut accumulated_cmyk) {
4476 replace_cmyk_with_coverage_mask(acc_cmyk, p1_cmyk, cov.data());
4477 }
4478 ko_band.cmyk_buffer = None;
4479 }
4480 _ => {
4484 offscreen.data_mut().copy_from_slice(&initial_backdrop);
4485
4486 ko_band.cmyk_buffer = initial_cmyk.clone();
4487
4488 render_element(&mut offscreen, &mut ko_band, elem, &group_ctx);
4489
4490 if let (Some(elem_cmyk), Some(acc_cmyk)) =
4491 (&ko_band.cmyk_buffer, &mut accumulated_cmyk)
4492 {
4493 replace_changed_cmyk(acc_cmyk, elem_cmyk, offscreen.data(), &initial_backdrop);
4494 }
4495 ko_band.cmyk_buffer = None;
4496
4497 replace_changed_pixels(accumulated.data_mut(), offscreen.data(), &initial_backdrop);
4498 }
4499 }
4500 }
4501
4502 let mut temp_mask = None;
4503 let mask_ref = resolve_clip_mask(
4504 &band_state.clip_region,
4505 &mut temp_mask,
4506 ctx.out_w,
4507 ctx.out_h,
4508 );
4509 let mask_ref = match mask_ref {
4510 None => return,
4511 Some(m) => m,
4512 };
4513
4514 composite_non_isolated_group_cropped(
4515 pixmap,
4516 &accumulated,
4517 &initial_backdrop,
4518 params,
4519 mask_ref,
4520 crop_x,
4521 crop_y,
4522 );
4523
4524 if let (Some(acc_cmyk), Some(parent_cmyk)) = (&accumulated_cmyk, &mut band_state.cmyk_buffer) {
4525 copy_cmyk_buffer_to_parent(
4526 parent_cmyk,
4527 acc_cmyk,
4528 accumulated.data(),
4529 crop_x as usize,
4530 crop_y as usize,
4531 eff_w as usize,
4532 eff_h as usize,
4533 ctx.out_w as usize,
4534 ctx.out_h as usize,
4535 );
4536 }
4537}
4538fn replace_with_coverage_mask(target: &mut [u8], source: &[u8], coverage: &[u8]) {
4546 for i in (0..target.len()).step_by(4) {
4547 let cov_a = coverage[i + 3];
4548 if cov_a == 0 {
4549 continue;
4550 }
4551 if cov_a == 255 {
4552 target[i..i + 4].copy_from_slice(&source[i..i + 4]);
4553 continue;
4554 }
4555 let a = cov_a as u32;
4556 let inv = 255 - a;
4557 for c in 0..4 {
4558 let s = source[i + c] as u32;
4559 let t = target[i + c] as u32;
4560 target[i + c] = ((s * a + t * inv + 127) / 255) as u8;
4561 }
4562 }
4563}
4564
4565fn replace_cmyk_with_coverage_mask(target: &mut [f32], source: &[f32], coverage: &[u8]) {
4569 let pixel_count = target.len() / 4;
4570 for i in 0..pixel_count {
4571 let pi = i * 4;
4572 let cov_a = coverage[pi + 3];
4573 if cov_a == 0 {
4574 continue;
4575 }
4576 if cov_a == 255 {
4577 target[pi..pi + 4].copy_from_slice(&source[pi..pi + 4]);
4578 continue;
4579 }
4580 let a = cov_a as f32 / 255.0;
4581 let inv = 1.0 - a;
4582 for c in 0..4 {
4583 target[pi + c] = source[pi + c] * a + target[pi + c] * inv;
4584 }
4585 }
4586}
4587
4588fn replace_changed_pixels(target: &mut [u8], source: &[u8], backdrop: &[u8]) {
4592 for i in (0..target.len()).step_by(4) {
4593 if source[i] != backdrop[i]
4594 || source[i + 1] != backdrop[i + 1]
4595 || source[i + 2] != backdrop[i + 2]
4596 || source[i + 3] != backdrop[i + 3]
4597 {
4598 target[i..i + 4].copy_from_slice(&source[i..i + 4]);
4599 }
4600 }
4601}
4602
4603#[allow(clippy::too_many_arguments)]
4607fn copy_cmyk_buffer_to_parent(
4608 parent_cmyk: &mut [f32],
4609 group_cmyk: &[f32],
4610 group_pixels: &[u8],
4611 crop_x: usize,
4612 crop_y: usize,
4613 group_w: usize,
4614 group_h: usize,
4615 parent_w: usize,
4616 parent_h: usize,
4617) {
4618 let parent_stride = parent_w * 4;
4619 let group_stride = group_w * 4;
4620 for gy in 0..group_h {
4621 let py = crop_y + gy;
4622 if py >= parent_h {
4623 break;
4624 }
4625 for gx in 0..group_w {
4626 let px = crop_x + gx;
4627 if px >= parent_w {
4628 break;
4629 }
4630 let g_pixel_idx = (gy * group_w + gx) * 4;
4635 let g_cmyk_idx = gy * group_stride + gx * 4;
4636 if group_pixels[g_pixel_idx + 3] > 0
4637 && (group_cmyk[g_cmyk_idx] != 0.0
4638 || group_cmyk[g_cmyk_idx + 1] != 0.0
4639 || group_cmyk[g_cmyk_idx + 2] != 0.0
4640 || group_cmyk[g_cmyk_idx + 3] != 0.0)
4641 {
4642 let p_cmyk_idx = py * parent_stride + px * 4;
4643 parent_cmyk[p_cmyk_idx..p_cmyk_idx + 4]
4644 .copy_from_slice(&group_cmyk[g_cmyk_idx..g_cmyk_idx + 4]);
4645 }
4646 }
4647 }
4648}
4649
4650fn replace_changed_cmyk(
4653 target_cmyk: &mut [f32],
4654 source_cmyk: &[f32],
4655 source_pixels: &[u8],
4656 backdrop_pixels: &[u8],
4657) {
4658 let pixel_count = target_cmyk.len() / 4;
4659 for i in 0..pixel_count {
4660 let pi = i * 4;
4661 if source_pixels[pi] != backdrop_pixels[pi]
4662 || source_pixels[pi + 1] != backdrop_pixels[pi + 1]
4663 || source_pixels[pi + 2] != backdrop_pixels[pi + 2]
4664 || source_pixels[pi + 3] != backdrop_pixels[pi + 3]
4665 {
4666 target_cmyk[pi..pi + 4].copy_from_slice(&source_cmyk[pi..pi + 4]);
4667 }
4668 }
4669}
4670
4671#[allow(clippy::too_many_arguments)]
4679fn render_soft_masked(
4680 pixmap: &mut Pixmap,
4681 band_state: &mut BandState,
4682 mask_list: &DisplayList,
4683 content_list: &DisplayList,
4684 params: &stet_graphics::display_list::SoftMaskParams,
4685 mask_cache: &Arc<Mutex<Option<Option<stet_graphics::display_list::MaskRaster>>>>,
4686 ctx: &RenderContext<'_>,
4687) {
4688 let use_inline_mask = ctx.vp_x != 0.0;
4705 let bbox = ¶ms.bbox;
4706 let smask_px_x0 = ((bbox[0] as f32 - ctx.vp_x) * ctx.scale_x).floor() as i32;
4707 let smask_px_y0 = ((bbox[1] as f32 - ctx.vp_y) * ctx.scale_y).floor() as i32;
4708 let smask_px_x1 = ((bbox[2] as f32 - ctx.vp_x) * ctx.scale_x).ceil() as i32;
4709 let smask_px_y1 = ((bbox[3] as f32 - ctx.vp_y) * ctx.scale_y).ceil() as i32;
4710
4711 let crop_x = smask_px_x0.max(0);
4713 let crop_y = smask_px_y0.max(0);
4714 let crop_x1 = smask_px_x1.min(ctx.out_w as i32);
4715 let crop_y1 = smask_px_y1.min(ctx.out_h as i32);
4716 if crop_x >= crop_x1 || crop_y >= crop_y1 {
4717 return;
4718 }
4719 let eff_w = (crop_x1 - crop_x) as u32;
4720 let eff_h = (crop_y1 - crop_y) as u32;
4721
4722 let eff_vp_x = ctx.vp_x + crop_x as f32 / ctx.scale_x;
4727 let eff_vp_y = ctx.vp_y + crop_y as f32 / ctx.scale_y;
4728
4729 let sub_ctx = RenderContext {
4730 vp_x: eff_vp_x,
4731 vp_y: eff_vp_y,
4732 scale_x: ctx.scale_x,
4733 scale_y: ctx.scale_y,
4734 out_w: eff_w,
4735 out_h: eff_h,
4736 effective_dpi: ctx.effective_dpi,
4737 icc: ctx.icc,
4738 image_cache: None,
4739 preprocessed: None,
4740 elem_idx: 0,
4741 no_aa: ctx.no_aa,
4742 opm_zero_transparent: ctx.opm_zero_transparent,
4743 knockout_painter_pass: ctx.knockout_painter_pass,
4744 parent_group_isolated: ctx.parent_group_isolated,
4745 alpha_extraction_pass: false,
4749 };
4750
4751 let mut mask_values_inline: Vec<u8> = Vec::new();
4755 if use_inline_mask {
4756 let Some(mut mask_pixmap) = Pixmap::new(eff_w, eff_h) else {
4757 return;
4758 };
4759 let mut mask_band = BandState {
4760 clip_region: None,
4761 spare_mask: None,
4762 clip_mask_cache: HashMap::new(),
4763 clip_mask_seen: HashSet::new(),
4764 mask_pool: Vec::new(),
4765 cmyk_buffer: None,
4766 op_bg_snapshot: None,
4767 op_touched: None,
4768 spot_mask: None,
4769 };
4770 for (idx, elem) in mask_list.elements().iter().enumerate() {
4771 let elem_ctx = RenderContext {
4772 elem_idx: idx,
4773 ..sub_ctx
4774 };
4775 render_element(&mut mask_pixmap, &mut mask_band, elem, &elem_ctx);
4776 }
4777 if params.has_nested_mask_scope
4778 && params.subtype == stet_graphics::display_list::SoftMaskSubtype::Luminosity
4779 {
4780 let bc = params.backdrop_color.as_ref();
4781 let bd_r = bc.map_or(0u8, |c| (c[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
4782 let bd_g = bc.map_or(0u8, |c| (c[1].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
4783 let bd_b = bc.map_or(0u8, |c| (c[2].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
4784 for chunk in mask_pixmap.data_mut().chunks_exact_mut(4) {
4785 let a = chunk[3] as u16;
4786 if a == 255 {
4787 continue;
4788 }
4789 let inv_a = 255 - a;
4790 chunk[0] = ((chunk[0] as u16 * 255 + bd_r as u16 * inv_a + 127) / 255) as u8;
4791 chunk[1] = ((chunk[1] as u16 * 255 + bd_g as u16 * inv_a + 127) / 255) as u8;
4792 chunk[2] = ((chunk[2] as u16 * 255 + bd_b as u16 * inv_a + 127) / 255) as u8;
4793 chunk[3] = 255;
4794 }
4795 }
4796 mask_values_inline = vec![0u8; (eff_w * eff_h) as usize];
4797 extract_soft_mask_values(mask_pixmap.data(), &mut mask_values_inline, params);
4798 }
4799
4800 let raster_owned: Option<stet_graphics::display_list::MaskRaster> = if use_inline_mask {
4802 None
4803 } else {
4804 let mut guard = mask_cache.lock().unwrap();
4805 let needs_build = match guard.as_ref() {
4806 None => true,
4807 Some(None) => false, Some(Some(r)) => {
4809 (r.scale_x - ctx.scale_x).abs() > 1e-4 || (r.scale_y - ctx.scale_y).abs() > 1e-4
4810 }
4811 };
4812 if needs_build {
4813 let built = rasterize_mask(
4814 mask_list,
4815 params,
4816 ctx.icc,
4817 ctx.no_aa,
4818 ctx.effective_dpi,
4819 ctx.scale_x,
4820 ctx.scale_y,
4821 );
4822 *guard = Some(built);
4823 }
4824 guard.as_ref().and_then(|inner| inner.clone())
4825 };
4826
4827 let fallback_mask = out_of_bounds_mask_value(params) as i32;
4831
4832 let Some(mut content_pixmap) = Pixmap::new(eff_w, eff_h) else {
4836 return;
4837 };
4838 let backdrop = copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h);
4839 content_pixmap.data_mut().copy_from_slice(&backdrop);
4840
4841 let content_cmyk = if has_overprint_elements(content_list) || band_state.cmyk_buffer.is_some() {
4842 let buf_size = eff_w as usize * eff_h as usize * 4;
4843 let mut buf = vec![0.0f32; buf_size];
4844 if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
4845 let parent_stride = ctx.out_w as usize * 4;
4846 let group_stride = eff_w as usize * 4;
4847 for gy in 0..eff_h as usize {
4848 let py = crop_y as usize + gy;
4849 if py < ctx.out_h as usize {
4850 let p_start = py * parent_stride + crop_x as usize * 4;
4851 let g_start = gy * group_stride;
4852 let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
4853 buf[g_start..g_start + copy_len]
4854 .copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
4855 }
4856 }
4857 }
4858 Some(buf)
4859 } else {
4860 None
4861 };
4862 let backdrop_cmyk: Option<Vec<f32>> = content_cmyk.clone();
4869 let mut content_band = BandState {
4870 clip_region: None,
4871 spare_mask: None,
4872 clip_mask_cache: HashMap::new(),
4873 clip_mask_seen: HashSet::new(),
4874 mask_pool: Vec::new(),
4875 cmyk_buffer: content_cmyk,
4876 op_bg_snapshot: None,
4877 op_touched: None,
4878 spot_mask: None,
4879 };
4880 for (idx, elem) in content_list.elements().iter().enumerate() {
4881 let elem_ctx = RenderContext {
4882 elem_idx: idx,
4883 ..sub_ctx
4884 };
4885 render_element(&mut content_pixmap, &mut content_band, elem, &elem_ctx);
4886 }
4887
4888 let vp_x_pixels = (ctx.vp_x * ctx.scale_x).round() as i32;
4908 let vp_y_pixels = (ctx.vp_y * ctx.scale_y).round() as i32;
4909
4910 let mut temp_mask = None;
4911 let clip_ref = resolve_clip_mask(
4912 &band_state.clip_region,
4913 &mut temp_mask,
4914 ctx.out_w,
4915 ctx.out_h,
4916 );
4917 let clip_ref = match clip_ref {
4918 None => return,
4919 Some(m) => m,
4920 };
4921
4922 let use_cmyk_blend = ctx.icc.is_some()
4938 && backdrop_cmyk.is_some()
4939 && content_band.cmyk_buffer.is_some()
4940 && content_list_is_simple_native_cmyk(content_list);
4941
4942 let content_data = content_pixmap.data();
4943 let parent_data = pixmap.data_mut();
4944 let parent_stride = ctx.out_w as usize * 4;
4945 let content_stride = eff_w as usize * 4;
4946
4947 for y in 0..eff_h as usize {
4948 let py = crop_y as usize + y;
4949 if py >= ctx.out_h as usize {
4950 break;
4951 }
4952 let ci_row = y * content_stride;
4953 let pi_row = py * parent_stride;
4954 let page_y = vp_y_pixels + crop_y + y as i32;
4955
4956 for x in 0..eff_w as usize {
4957 let px = crop_x as usize + x;
4958 if px >= ctx.out_w as usize {
4959 break;
4960 }
4961
4962 if let Some(clip) = clip_ref {
4964 if clip.data()[py * ctx.out_w as usize + px] == 0 {
4965 continue;
4966 }
4967 }
4968
4969 let m = if use_inline_mask {
4972 mask_values_inline[y * eff_w as usize + x] as i32
4973 } else if let Some(ref raster) = raster_owned {
4974 let page_x = vp_x_pixels + crop_x + x as i32;
4975 let mx = page_x - raster.origin_x;
4976 let my = page_y - raster.origin_y;
4977 if mx >= 0 && (mx as u32) < raster.width && my >= 0 && (my as u32) < raster.height {
4978 raster.data[my as usize * raster.width as usize + mx as usize] as i32
4979 } else {
4980 fallback_mask
4981 }
4982 } else {
4983 fallback_mask
4984 };
4985 if m == 0 {
4986 continue;
4987 }
4988
4989 let ci = ci_row + x * 4;
4990 let pi = pi_row + px * 4;
4991
4992 let ci_cmyk = (y * eff_w as usize + x) * 4;
5002 let cmyk_path_ok = use_cmyk_blend && {
5003 let bc_cmyk = &backdrop_cmyk.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5004 let cc_cmyk = &content_band.cmyk_buffer.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5005 let icc_match = |cmyk: &[f32], rgb: &[u8]| -> bool {
5006 let (r, g, b) = ctx
5007 .icc
5008 .and_then(|i| {
5009 i.convert_cmyk_readonly(
5010 cmyk[0] as f64,
5011 cmyk[1] as f64,
5012 cmyk[2] as f64,
5013 cmyk[3] as f64,
5014 )
5015 })
5016 .unwrap_or_else(|| {
5017 cmyk_to_rgb_plrm(
5018 cmyk[0] as f64,
5019 cmyk[1] as f64,
5020 cmyk[2] as f64,
5021 cmyk[3] as f64,
5022 )
5023 });
5024 let r = (r * 255.0).round() as i32;
5025 let g = (g * 255.0).round() as i32;
5026 let b = (b * 255.0).round() as i32;
5027 (r - rgb[0] as i32).abs() <= 3
5028 && (g - rgb[1] as i32).abs() <= 3
5029 && (b - rgb[2] as i32).abs() <= 3
5030 };
5031 icc_match(bc_cmyk, &backdrop[ci..ci + 3])
5032 && icc_match(cc_cmyk, &content_data[ci..ci + 3])
5033 };
5034
5035 if cmyk_path_ok {
5036 let bc_cmyk = &backdrop_cmyk.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5038 let cc_cmyk = &content_band.cmyk_buffer.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5039 let mf = m as f64 / 255.0;
5040 let rc = bc_cmyk[0] as f64 + mf * (cc_cmyk[0] as f64 - bc_cmyk[0] as f64);
5041 let rm = bc_cmyk[1] as f64 + mf * (cc_cmyk[1] as f64 - bc_cmyk[1] as f64);
5042 let ry = bc_cmyk[2] as f64 + mf * (cc_cmyk[2] as f64 - bc_cmyk[2] as f64);
5043 let rk = bc_cmyk[3] as f64 + mf * (cc_cmyk[3] as f64 - bc_cmyk[3] as f64);
5044 let (fr, fg, fb) = ctx
5045 .icc
5046 .and_then(|i| i.convert_cmyk_readonly(rc, rm, ry, rk))
5047 .unwrap_or_else(|| cmyk_to_rgb_plrm(rc, rm, ry, rk));
5048 parent_data[pi] = (fr * 255.0).round().clamp(0.0, 255.0) as u8;
5049 parent_data[pi + 1] = (fg * 255.0).round().clamp(0.0, 255.0) as u8;
5050 parent_data[pi + 2] = (fb * 255.0).round().clamp(0.0, 255.0) as u8;
5051 let content_a = content_data[ci + 3] as i32;
5053 let backdrop_a = backdrop[ci + 3] as i32;
5054 let delta = content_a - backdrop_a;
5055 if delta != 0 {
5056 let masked_delta = if delta > 0 {
5057 (delta * m + 128) / 255
5058 } else {
5059 (delta * m - 128) / 255
5060 };
5061 let result = (parent_data[pi + 3] as i32 + masked_delta).clamp(0, 255);
5062 parent_data[pi + 3] = result as u8;
5063 }
5064 } else {
5073 for c in 0..4 {
5074 let content_val = content_data[ci + c] as i32;
5075 let backdrop_val = backdrop[ci + c] as i32;
5076 let delta = content_val - backdrop_val;
5077 if delta != 0 {
5078 let masked_delta = if delta > 0 {
5079 (delta * m + 128) / 255
5080 } else {
5081 (delta * m - 128) / 255
5082 };
5083 let result = (parent_data[pi + c] as i32 + masked_delta).clamp(0, 255);
5084 parent_data[pi + c] = result as u8;
5085 }
5086 }
5087 }
5088 }
5089 }
5090
5091 if !use_cmyk_blend {
5095 if let (Some(content_cmyk), Some(parent_cmyk)) =
5096 (&content_band.cmyk_buffer, &mut band_state.cmyk_buffer)
5097 {
5098 copy_cmyk_buffer_to_parent(
5099 parent_cmyk,
5100 content_cmyk,
5101 content_pixmap.data(),
5102 crop_x as usize,
5103 crop_y as usize,
5104 eff_w as usize,
5105 eff_h as usize,
5106 ctx.out_w as usize,
5107 ctx.out_h as usize,
5108 );
5109 }
5110 }
5111}
5112fn extract_soft_mask_values(
5114 rgba: &[u8],
5115 out: &mut [u8],
5116 params: &stet_graphics::display_list::SoftMaskParams,
5117) {
5118 use stet_graphics::display_list::SoftMaskSubtype;
5119 let pixel_count = out.len();
5120
5121 match params.subtype {
5122 SoftMaskSubtype::Alpha => {
5123 for i in 0..pixel_count {
5124 let a = rgba[i * 4 + 3]; out[i] = if params.transfer_invert { 255 - a } else { a };
5126 }
5127 }
5128 SoftMaskSubtype::Luminosity => {
5129 let backdrop_lum = if let Some(bc) = ¶ms.backdrop_color {
5131 (0.2126 * bc[0] + 0.7152 * bc[1] + 0.0722 * bc[2]).clamp(0.0, 1.0)
5132 } else {
5133 0.0 };
5135 let backdrop_byte = (backdrop_lum * 255.0 + 0.5) as u8;
5136
5137 #[allow(clippy::needless_range_loop)]
5138 for i in 0..pixel_count {
5139 let off = i * 4;
5140 let a = rgba[off + 3];
5141 let lum_byte = if a == 0 {
5142 backdrop_byte
5143 } else if a < 255 {
5144 let af = a as f64;
5147 let bd = backdrop_lum * 255.0;
5148 let r = rgba[off] as f64 + bd * (255.0 - af) / 255.0;
5149 let g = rgba[off + 1] as f64 + bd * (255.0 - af) / 255.0;
5150 let b = rgba[off + 2] as f64 + bd * (255.0 - af) / 255.0;
5151 let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
5152 (lum + 0.5).clamp(0.0, 255.0) as u8
5153 } else {
5154 let lum = 0.2126 * rgba[off] as f64
5156 + 0.7152 * rgba[off + 1] as f64
5157 + 0.0722 * rgba[off + 2] as f64;
5158 (lum + 0.5).clamp(0.0, 255.0) as u8
5159 };
5160 out[i] = if params.transfer_invert {
5162 255 - lum_byte
5163 } else {
5164 lum_byte
5165 };
5166 }
5167 }
5168 }
5169}
5170
5171fn out_of_bounds_mask_value(params: &stet_graphics::display_list::SoftMaskParams) -> u8 {
5179 use stet_graphics::display_list::SoftMaskSubtype;
5180 let raw = match params.subtype {
5181 SoftMaskSubtype::Alpha => 0u8,
5182 SoftMaskSubtype::Luminosity => {
5183 let lum = if let Some(bc) = ¶ms.backdrop_color {
5184 (0.2126 * bc[0] + 0.7152 * bc[1] + 0.0722 * bc[2]).clamp(0.0, 1.0)
5185 } else {
5186 0.0
5187 };
5188 (lum * 255.0 + 0.5) as u8
5189 }
5190 };
5191 if params.transfer_invert {
5192 255 - raw
5193 } else {
5194 raw
5195 }
5196}
5197
5198const MAX_MASK_RASTER_PIXELS: u64 = 64 * 1024 * 1024;
5205
5206fn rasterize_mask(
5218 mask_list: &DisplayList,
5219 params: &stet_graphics::display_list::SoftMaskParams,
5220 icc: Option<&IccCache>,
5221 no_aa: bool,
5222 effective_dpi: f64,
5223 scale_x: f32,
5224 scale_y: f32,
5225) -> Option<stet_graphics::display_list::MaskRaster> {
5226 let mut bounds = compute_paint_bounds(mask_list, effective_dpi)?;
5233 if let Some(cap) = params.parent_clip_bbox {
5234 let cap_bbox = BBox2D {
5235 x_min: cap[0],
5236 y_min: cap[1],
5237 x_max: cap[2],
5238 y_max: cap[3],
5239 };
5240 bounds = intersect_bbox(&bounds, &cap_bbox)?;
5241 }
5242
5243 let px_x_min = (bounds.x_min as f32 * scale_x).floor() as i32 - 1;
5246 let px_y_min = (bounds.y_min as f32 * scale_y).floor() as i32 - 1;
5247 let px_x_max = (bounds.x_max as f32 * scale_x).ceil() as i32 + 1;
5248 let px_y_max = (bounds.y_max as f32 * scale_y).ceil() as i32 + 1;
5249 if px_x_min >= px_x_max || px_y_min >= px_y_max {
5250 return None;
5251 }
5252 let raster_w = (px_x_max - px_x_min) as u32;
5253 let raster_h = (px_y_max - px_y_min) as u32;
5254 if raster_w == 0 || raster_h == 0 {
5255 return None;
5256 }
5257 if (raster_w as u64) * (raster_h as u64) > MAX_MASK_RASTER_PIXELS {
5258 return None;
5259 }
5260
5261 let mut mask_pixmap = Pixmap::new(raster_w, raster_h)?;
5263
5264 let sub_ctx = RenderContext {
5268 vp_x: px_x_min as f32 / scale_x,
5269 vp_y: px_y_min as f32 / scale_y,
5270 scale_x,
5271 scale_y,
5272 out_w: raster_w,
5273 out_h: raster_h,
5274 effective_dpi,
5275 icc,
5276 image_cache: None,
5277 preprocessed: None,
5278 elem_idx: 0,
5279 no_aa,
5280 opm_zero_transparent: false,
5281 knockout_painter_pass: KnockoutPainterPass::None,
5282 parent_group_isolated: false,
5283 alpha_extraction_pass: false,
5284 };
5285
5286 let mut mask_band = BandState {
5288 clip_region: None,
5289 spare_mask: None,
5290 clip_mask_cache: HashMap::new(),
5291 clip_mask_seen: HashSet::new(),
5292 mask_pool: Vec::new(),
5293 cmyk_buffer: None,
5294 op_bg_snapshot: None,
5295 op_touched: None,
5296 spot_mask: None,
5297 };
5298
5299 for (idx, elem) in mask_list.elements().iter().enumerate() {
5301 let elem_ctx = RenderContext {
5302 elem_idx: idx,
5303 ..sub_ctx
5304 };
5305 render_element(&mut mask_pixmap, &mut mask_band, elem, &elem_ctx);
5306 }
5307
5308 if params.has_nested_mask_scope
5316 && params.subtype == stet_graphics::display_list::SoftMaskSubtype::Luminosity
5317 {
5318 let bc = params.backdrop_color.as_ref();
5319 let bd_r = bc.map_or(0u8, |c| (c[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
5320 let bd_g = bc.map_or(0u8, |c| (c[1].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
5321 let bd_b = bc.map_or(0u8, |c| (c[2].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
5322 for chunk in mask_pixmap.data_mut().chunks_exact_mut(4) {
5323 let a = chunk[3] as u16;
5324 if a == 255 {
5325 continue;
5326 }
5327 let inv_a = 255 - a;
5328 chunk[0] = ((chunk[0] as u16 * 255 + bd_r as u16 * inv_a + 127) / 255) as u8;
5329 chunk[1] = ((chunk[1] as u16 * 255 + bd_g as u16 * inv_a + 127) / 255) as u8;
5330 chunk[2] = ((chunk[2] as u16 * 255 + bd_b as u16 * inv_a + 127) / 255) as u8;
5331 chunk[3] = 255;
5332 }
5333 }
5334
5335 let pixel_count = (raster_w * raster_h) as usize;
5337 let mut data = vec![0u8; pixel_count];
5338 extract_soft_mask_values(mask_pixmap.data(), &mut data, params);
5339
5340 Some(stet_graphics::display_list::MaskRaster {
5341 data,
5342 width: raster_w,
5343 height: raster_h,
5344 origin_x: px_x_min,
5345 origin_y: px_y_min,
5346 scale_x,
5347 scale_y,
5348 })
5349}
5350
5351fn transform_element_ctm(elem: &DisplayElement, pm: &Matrix) -> DisplayElement {
5355 match elem {
5356 DisplayElement::Fill { path, params } => {
5357 let mut p = params.clone();
5358 p.ctm = pm.concat(&p.ctm);
5359 DisplayElement::Fill {
5360 path: path.clone(),
5361 params: p,
5362 }
5363 }
5364 DisplayElement::Stroke { path, params } => {
5365 let mut p = params.clone();
5366 p.ctm = pm.concat(&p.ctm);
5367 DisplayElement::Stroke {
5368 path: path.clone(),
5369 params: p,
5370 }
5371 }
5372 DisplayElement::Clip { path, params } => {
5373 let mut p = params.clone();
5374 p.ctm = pm.concat(&p.ctm);
5375 if let Some(ref mut sp) = p.stroke_params {
5376 sp.ctm = pm.concat(&sp.ctm);
5377 }
5378 DisplayElement::Clip {
5379 path: path.clone(),
5380 params: p,
5381 }
5382 }
5383 DisplayElement::Image {
5384 sample_data,
5385 params,
5386 } => {
5387 let mut p = params.clone();
5388 p.ctm = pm.concat(&p.ctm);
5389 DisplayElement::Image {
5390 sample_data: sample_data.clone(),
5391 params: p,
5392 }
5393 }
5394 DisplayElement::MeshShading { params } => {
5395 let mut p = params.clone();
5396 p.ctm = pm.concat(&p.ctm);
5397 DisplayElement::MeshShading { params: p }
5398 }
5399 DisplayElement::PatchShading { params } => {
5400 let mut p = params.clone();
5401 p.ctm = pm.concat(&p.ctm);
5402 DisplayElement::PatchShading { params: p }
5403 }
5404 DisplayElement::AxialShading { params } => {
5405 let mut p = params.clone();
5406 p.ctm = pm.concat(&p.ctm);
5407 DisplayElement::AxialShading { params: p }
5408 }
5409 DisplayElement::RadialShading { params } => {
5410 let mut p = params.clone();
5411 p.ctm = pm.concat(&p.ctm);
5412 DisplayElement::RadialShading { params: p }
5413 }
5414 DisplayElement::Group { elements, params } => {
5415 let mut t = DisplayList::new();
5416 for child in elements.elements() {
5417 t.push(transform_element_ctm(child, pm));
5418 }
5419 let mut p = params.clone();
5420 let corners = [
5421 pm.transform_point(p.bbox[0], p.bbox[1]),
5422 pm.transform_point(p.bbox[2], p.bbox[1]),
5423 pm.transform_point(p.bbox[0], p.bbox[3]),
5424 pm.transform_point(p.bbox[2], p.bbox[3]),
5425 ];
5426 p.bbox = [
5427 corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min),
5428 corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min),
5429 corners
5430 .iter()
5431 .map(|c| c.0)
5432 .fold(f64::NEG_INFINITY, f64::max),
5433 corners
5434 .iter()
5435 .map(|c| c.1)
5436 .fold(f64::NEG_INFINITY, f64::max),
5437 ];
5438 DisplayElement::Group {
5439 elements: t,
5440 params: p,
5441 }
5442 }
5443 DisplayElement::SoftMasked {
5444 mask,
5445 content,
5446 params,
5447 ..
5448 } => {
5449 let mut t_mask = DisplayList::new();
5450 for child in mask.elements() {
5451 t_mask.push(transform_element_ctm(child, pm));
5452 }
5453 let mut t_content = DisplayList::new();
5454 for child in content.elements() {
5455 t_content.push(transform_element_ctm(child, pm));
5456 }
5457 let mut p = params.clone();
5458 let corners = [
5459 pm.transform_point(p.bbox[0], p.bbox[1]),
5460 pm.transform_point(p.bbox[2], p.bbox[1]),
5461 pm.transform_point(p.bbox[0], p.bbox[3]),
5462 pm.transform_point(p.bbox[2], p.bbox[3]),
5463 ];
5464 p.bbox = [
5465 corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min),
5466 corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min),
5467 corners
5468 .iter()
5469 .map(|c| c.0)
5470 .fold(f64::NEG_INFINITY, f64::max),
5471 corners
5472 .iter()
5473 .map(|c| c.1)
5474 .fold(f64::NEG_INFINITY, f64::max),
5475 ];
5476 if let Some(pcb) = p.parent_clip_bbox {
5483 let pcb_corners = [
5484 pm.transform_point(pcb[0], pcb[1]),
5485 pm.transform_point(pcb[2], pcb[1]),
5486 pm.transform_point(pcb[0], pcb[3]),
5487 pm.transform_point(pcb[2], pcb[3]),
5488 ];
5489 p.parent_clip_bbox = Some([
5490 pcb_corners
5491 .iter()
5492 .map(|c| c.0)
5493 .fold(f64::INFINITY, f64::min),
5494 pcb_corners
5495 .iter()
5496 .map(|c| c.1)
5497 .fold(f64::INFINITY, f64::min),
5498 pcb_corners
5499 .iter()
5500 .map(|c| c.0)
5501 .fold(f64::NEG_INFINITY, f64::max),
5502 pcb_corners
5503 .iter()
5504 .map(|c| c.1)
5505 .fold(f64::NEG_INFINITY, f64::max),
5506 ]);
5507 }
5508 DisplayElement::SoftMasked {
5512 mask: t_mask,
5513 content: t_content,
5514 params: p,
5515 mask_cache: Arc::new(Mutex::new(None)),
5516 }
5517 }
5518 DisplayElement::PatternFill { params } => {
5519 let mut p = params.clone();
5520 p.pattern_matrix = pm.concat(&p.pattern_matrix);
5521 p.path = transform_path_by_matrix(&p.path, pm);
5523 if let Some(ref mut sp) = p.stroke_params {
5524 sp.ctm = pm.concat(&sp.ctm);
5525 }
5526 DisplayElement::PatternFill { params: p }
5527 }
5528 DisplayElement::OcgGroup {
5529 elements,
5530 ocg_id,
5531 default_visible,
5532 } => {
5533 let mut t = DisplayList::new();
5534 for child in elements.elements() {
5535 t.push(transform_element_ctm(child, pm));
5536 }
5537 DisplayElement::OcgGroup {
5538 elements: t,
5539 ocg_id: *ocg_id,
5540 default_visible: *default_visible,
5541 }
5542 }
5543 other => other.clone(),
5544 }
5545}
5546
5547fn transform_path_by_matrix(path: &PsPath, m: &Matrix) -> PsPath {
5549 use stet_fonts::geometry::PathSegment;
5550 let mut out = PsPath::new();
5551 for seg in &path.segments {
5552 out.segments.push(match *seg {
5553 PathSegment::MoveTo(x, y) => {
5554 let (nx, ny) = m.transform_point(x, y);
5555 PathSegment::MoveTo(nx, ny)
5556 }
5557 PathSegment::LineTo(x, y) => {
5558 let (nx, ny) = m.transform_point(x, y);
5559 PathSegment::LineTo(nx, ny)
5560 }
5561 PathSegment::CurveTo {
5562 x1,
5563 y1,
5564 x2,
5565 y2,
5566 x3,
5567 y3,
5568 } => {
5569 let (nx1, ny1) = m.transform_point(x1, y1);
5570 let (nx2, ny2) = m.transform_point(x2, y2);
5571 let (nx3, ny3) = m.transform_point(x3, y3);
5572 PathSegment::CurveTo {
5573 x1: nx1,
5574 y1: ny1,
5575 x2: nx2,
5576 y2: ny2,
5577 x3: nx3,
5578 y3: ny3,
5579 }
5580 }
5581 PathSegment::ClosePath => PathSegment::ClosePath,
5582 });
5583 }
5584 out
5585}
5586
5587fn bilinear_prescale(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
5594 let mut dst = vec![0u8; (dw * dh * 4) as usize];
5595 for dy in 0..dh {
5596 let sy_f = (dy as f64 + 0.5) * sh as f64 / dh as f64 - 0.5;
5597 let sy0 = sy_f.floor().max(0.0) as u32;
5598 let sy1 = (sy0 + 1).min(sh - 1);
5599 let fy = (sy_f - sy0 as f64) as f32;
5600 let ify = 1.0 - fy;
5601 for dx in 0..dw {
5602 let sx_f = (dx as f64 + 0.5) * sw as f64 / dw as f64 - 0.5;
5603 let sx0 = sx_f.floor().max(0.0) as u32;
5604 let sx1 = (sx0 + 1).min(sw - 1);
5605 let fx = (sx_f - sx0 as f64) as f32;
5606 let ifx = 1.0 - fx;
5607
5608 let i00 = (sy0 * sw + sx0) as usize * 4;
5609 let i10 = (sy0 * sw + sx1) as usize * 4;
5610 let i01 = (sy1 * sw + sx0) as usize * 4;
5611 let i11 = (sy1 * sw + sx1) as usize * 4;
5612 let di = (dy * dw + dx) as usize * 4;
5613 for c in 0..4 {
5614 dst[di + c] = (src[i00 + c] as f32 * ifx * ify
5615 + src[i10 + c] as f32 * fx * ify
5616 + src[i01 + c] as f32 * ifx * fy
5617 + src[i11 + c] as f32 * fx * fy)
5618 .round() as u8;
5619 }
5620 }
5621 }
5622 dst
5623}
5624
5625fn render_pattern_fill(
5626 pixmap: &mut Pixmap,
5627 band_state: &mut BandState,
5628 params: &stet_graphics::device::PatternFillParams,
5629 ctx: &RenderContext<'_>,
5630) {
5631 let mut temp_mask = None;
5632 let Some(mask_ref) = resolve_clip_mask(
5633 &band_state.clip_region,
5634 &mut temp_mask,
5635 ctx.out_w,
5636 ctx.out_h,
5637 ) else {
5638 return;
5639 };
5640
5641 let pm = ¶ms.pattern_matrix;
5642
5643 let (step_ux, step_uy) = pm.transform_delta(params.xstep, 0.0);
5645 let (step_vx, step_vy) = pm.transform_delta(0.0, params.ystep);
5646
5647 let step_u_len = (step_ux * step_ux + step_uy * step_uy).sqrt();
5648 let step_v_len = (step_vx * step_vx + step_vy * step_vy).sqrt();
5649 if step_u_len < 0.01 || step_v_len < 0.01 {
5650 return;
5651 }
5652
5653 let origin_x = pm.tx;
5654 let origin_y = pm.ty;
5655
5656 let dev_vp_x = ctx.vp_x as f64;
5658 let dev_vp_y = ctx.vp_y as f64;
5659 let dev_vp_w = ctx.out_w as f64 / ctx.scale_x as f64;
5660 let dev_vp_h = ctx.out_h as f64 / ctx.scale_y as f64;
5661
5662 let (mut min_x, mut min_y, mut max_x, mut max_y) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
5663 for seg in ¶ms.path.segments {
5664 let (x, y) = match seg {
5665 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => (*x, *y),
5666 PathSegment::CurveTo { x3, y3, .. } => (*x3, *y3),
5667 PathSegment::ClosePath => continue,
5668 };
5669 min_x = min_x.min(x);
5670 min_y = min_y.min(y);
5671 max_x = max_x.max(x);
5672 max_y = max_y.max(y);
5673 }
5674
5675 if let Some(ref sp) = params.stroke_params {
5679 let ctm = &sp.ctm;
5681 let corners = [
5682 ctm.transform_point(min_x, min_y),
5683 ctm.transform_point(max_x, min_y),
5684 ctm.transform_point(min_x, max_y),
5685 ctm.transform_point(max_x, max_y),
5686 ];
5687 min_x = f64::MAX;
5688 min_y = f64::MAX;
5689 max_x = f64::MIN;
5690 max_y = f64::MIN;
5691 for (cx, cy) in &corners {
5692 min_x = min_x.min(*cx);
5693 min_y = min_y.min(*cy);
5694 max_x = max_x.max(*cx);
5695 max_y = max_y.max(*cy);
5696 }
5697 let half_w = sp.line_width
5699 * 0.5
5700 * (ctm.a * ctm.a + ctm.b * ctm.b)
5701 .sqrt()
5702 .max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt());
5703 min_x -= half_w;
5704 min_y -= half_w;
5705 max_x += half_w;
5706 max_y += half_w;
5707 }
5708
5709 min_x = min_x.max(dev_vp_x);
5711 min_y = min_y.max(dev_vp_y);
5712 max_x = max_x.min(dev_vp_x + dev_vp_w);
5713 max_y = max_y.min(dev_vp_y + dev_vp_h);
5714 if min_x >= max_x || min_y >= max_y {
5715 return;
5716 }
5717
5718 let det = step_ux * step_vy - step_uy * step_vx;
5719 if det.abs() < 1e-10 {
5720 return;
5721 }
5722 let inv_det = 1.0 / det;
5723
5724 let mut tu_min = f64::MAX;
5725 let mut tu_max = f64::MIN;
5726 let mut tv_min = f64::MAX;
5727 let mut tv_max = f64::MIN;
5728 for &(cx, cy) in &[
5729 (min_x, min_y),
5730 (max_x, min_y),
5731 (min_x, max_y),
5732 (max_x, max_y),
5733 ] {
5734 let dx = cx - origin_x;
5735 let dy = cy - origin_y;
5736 let tu = (dx * step_vy - dy * step_vx) * inv_det;
5737 let tv = (-dx * step_uy + dy * step_ux) * inv_det;
5738 tu_min = tu_min.min(tu);
5739 tu_max = tu_max.max(tu);
5740 tv_min = tv_min.min(tv);
5741 tv_max = tv_max.max(tv);
5742 }
5743
5744 let tile_x_start = tu_min.floor() as i32 - 1;
5745 let tile_x_end = tu_max.ceil() as i32 + 1;
5746 let tile_y_start = tv_min.floor() as i32 - 1;
5747 let tile_y_end = tv_max.ceil() as i32 + 1;
5748
5749 let tile_count = (tile_x_end - tile_x_start) as i64 * (tile_y_end - tile_y_start) as i64;
5750 if tile_count > 10000 {
5751 return;
5752 }
5753
5754 let Some(mut tile_buf) = Pixmap::new(ctx.out_w, ctx.out_h) else {
5755 return;
5756 };
5757
5758 let sx_f = ctx.scale_x as f64;
5759 let sy_f = ctx.scale_y as f64;
5760
5761 if params.device_space_tile {
5762 for tv in tile_y_start..tile_y_end {
5768 for tu in tile_x_start..tile_x_end {
5769 let offset_x = tu as f64 * step_ux + tv as f64 * step_vx;
5770 let offset_y = tu as f64 * step_uy + tv as f64 * step_vy;
5771
5772 let tile_ctx = RenderContext {
5773 vp_x: ctx.vp_x - offset_x as f32,
5774 vp_y: ctx.vp_y - offset_y as f32,
5775 scale_x: ctx.scale_x,
5776 scale_y: ctx.scale_y,
5777 out_w: ctx.out_w,
5778 out_h: ctx.out_h,
5779 effective_dpi: ctx.effective_dpi,
5780 icc: ctx.icc,
5781 image_cache: None,
5782 preprocessed: None,
5783 elem_idx: 0,
5784 no_aa: ctx.no_aa,
5785 opm_zero_transparent: params.overprint_mode == 1,
5786 knockout_painter_pass: ctx.knockout_painter_pass,
5787 parent_group_isolated: ctx.parent_group_isolated,
5788 alpha_extraction_pass: ctx.alpha_extraction_pass,
5789 };
5790
5791 let mut tile_band = BandState {
5792 clip_region: None,
5793 spare_mask: None,
5794 clip_mask_cache: HashMap::new(),
5795 clip_mask_seen: HashSet::new(),
5796 mask_pool: Vec::new(),
5797 cmyk_buffer: None,
5798 op_bg_snapshot: None,
5799 op_touched: None,
5800 spot_mask: None,
5801 };
5802
5803 for (idx, elem) in params.tile.elements().iter().enumerate() {
5804 let elem_ctx = RenderContext {
5805 elem_idx: idx,
5806 ..tile_ctx
5807 };
5808 render_element(&mut tile_buf, &mut tile_band, elem, &elem_ctx);
5809 }
5810 }
5811 }
5812 } else if params.tile.elements().iter().any(|e| {
5813 !matches!(
5814 e,
5815 DisplayElement::Fill { .. }
5816 | DisplayElement::Stroke { .. }
5817 | DisplayElement::Image { .. }
5818 | DisplayElement::Clip { .. }
5819 | DisplayElement::InitClip
5820 )
5821 }) {
5822 let bbox = ¶ms.bbox;
5826 let corners_dev = [
5827 pm.transform_point(bbox[0], bbox[1]),
5828 pm.transform_point(bbox[2], bbox[1]),
5829 pm.transform_point(bbox[0], bbox[3]),
5830 pm.transform_point(bbox[2], bbox[3]),
5831 ];
5832 let (mut td_x0, mut td_y0) = (f64::MAX, f64::MAX);
5833 let (mut td_x1, mut td_y1) = (f64::MIN, f64::MIN);
5834 for (x, y) in &corners_dev {
5835 td_x0 = td_x0.min(*x);
5836 td_y0 = td_y0.min(*y);
5837 td_x1 = td_x1.max(*x);
5838 td_y1 = td_y1.max(*y);
5839 }
5840 let tile_pw = ((td_x1 - td_x0) * sx_f).ceil().max(1.0) as u32;
5841 let tile_ph = ((td_y1 - td_y0) * sy_f).ceil().max(1.0) as u32;
5842 let tile_pw = tile_pw.min(8192);
5843 let tile_ph = tile_ph.min(8192);
5844
5845 if let Some(mut one_tile) = Pixmap::new(tile_pw, tile_ph) {
5846 let tile_render_ctx = RenderContext {
5847 vp_x: td_x0 as f32,
5848 vp_y: td_y0 as f32,
5849 scale_x: ctx.scale_x,
5850 scale_y: ctx.scale_y,
5851 out_w: tile_pw,
5852 out_h: tile_ph,
5853 effective_dpi: ctx.effective_dpi,
5854 icc: ctx.icc,
5855 image_cache: None,
5856 preprocessed: None,
5857 elem_idx: 0,
5858 no_aa: ctx.no_aa,
5859 opm_zero_transparent: params.overprint_mode == 1,
5860 knockout_painter_pass: ctx.knockout_painter_pass,
5861 parent_group_isolated: ctx.parent_group_isolated,
5862 alpha_extraction_pass: ctx.alpha_extraction_pass,
5863 };
5864 let mut tile_bs = BandState {
5865 clip_region: None,
5866 spare_mask: None,
5867 clip_mask_cache: HashMap::new(),
5868 clip_mask_seen: HashSet::new(),
5869 mask_pool: Vec::new(),
5870 cmyk_buffer: None,
5871 op_bg_snapshot: None,
5872 op_touched: None,
5873 spot_mask: None,
5874 };
5875 for (idx, elem) in params.tile.elements().iter().enumerate() {
5876 let transformed = transform_element_ctm(elem, pm);
5877 let elem_ctx = RenderContext {
5878 elem_idx: idx,
5879 ..tile_render_ctx
5880 };
5881 render_element(&mut one_tile, &mut tile_bs, &transformed, &elem_ctx);
5882 }
5883 for tv in tile_y_start..tile_y_end {
5885 for tu in tile_x_start..tile_x_end {
5886 let offset_x = tu as f64 * step_ux + tv as f64 * step_vx;
5887 let offset_y = tu as f64 * step_uy + tv as f64 * step_vy;
5888 let px = ((td_x0 + offset_x - dev_vp_x) * sx_f) as i32;
5889 let py = ((td_y0 + offset_y - dev_vp_y) * sy_f) as i32;
5890 let paint = stet_tiny_skia::PixmapPaint {
5891 opacity: 1.0,
5892 blend_mode: BlendMode::SourceOver,
5893 quality: stet_tiny_skia::FilterQuality::Nearest,
5894 };
5895 tile_buf.draw_pixmap(
5896 px,
5897 py,
5898 one_tile.as_ref(),
5899 &paint,
5900 Transform::identity(),
5901 None,
5902 );
5903 }
5904 }
5905 }
5906 } else {
5907 struct PreprocessedImage {
5915 rgba: Vec<u8>,
5916 width: u32,
5917 height: u32,
5918 img_transform: Transform,
5921 }
5922 let tile_elements = params.tile.elements();
5923 let mut preprocessed: Vec<Option<PreprocessedImage>> =
5924 Vec::with_capacity(tile_elements.len());
5925 let tt_sx = (pm.a * sx_f) as f32;
5927 let tt_sy = (pm.d * sy_f) as f32;
5928 let tt_kx = (pm.c * sx_f) as f32;
5929 let tt_ky = (pm.b * sy_f) as f32;
5930 for elem in tile_elements {
5931 if let DisplayElement::Image {
5932 sample_data,
5933 params: ip,
5934 } = elem
5935 {
5936 let iw = ip.width;
5937 let ih = ip.height;
5938 if iw > 0 && ih > 0 {
5939 let mut rgba =
5940 samples_to_rgba(sample_data, ip, ctx.icc, ctx.opm_zero_transparent);
5941 if ip.mask_color.is_some() {
5942 apply_mask_color_rgba(&mut rgba, sample_data, ip);
5943 }
5944 let expected = (iw * ih * 4) as usize;
5945 if rgba.len() >= expected {
5946 if let Some(inv) = ip.image_matrix.invert() {
5947 let combined_mat = ip.ctm.concat(&inv);
5948 let t = to_transform(&combined_mat);
5949 let test = t.post_concat(Transform::from_row(
5952 tt_sx, tt_ky, tt_kx, tt_sy, 0.0, 0.0,
5953 ));
5954 let eff_sx = (test.sx * test.sx + test.ky * test.ky).sqrt();
5955 let eff_sy = (test.kx * test.kx + test.sy * test.sy).sqrt();
5956 if eff_sx < 0.99 || eff_sy < 0.99 {
5957 let tw = (iw as f32 * eff_sx).floor().max(1.0) as u32;
5961 let th = (ih as f32 * eff_sy).floor().max(1.0) as u32;
5962 let scaled = bilinear_prescale(&rgba, iw, ih, tw, th);
5963 let adj = Transform::from_scale(
5966 iw as f32 / tw as f32,
5967 ih as f32 / th as f32,
5968 );
5969 preprocessed.push(Some(PreprocessedImage {
5970 rgba: scaled,
5971 width: tw,
5972 height: th,
5973 img_transform: t.pre_concat(adj),
5974 }));
5975 } else {
5976 preprocessed.push(Some(PreprocessedImage {
5977 rgba,
5978 width: iw,
5979 height: ih,
5980 img_transform: t,
5981 }));
5982 }
5983 } else {
5984 preprocessed.push(None);
5985 }
5986 } else {
5987 preprocessed.push(None);
5988 }
5989 } else {
5990 preprocessed.push(None);
5991 }
5992 }
5995 }
5996
5997 for tv in tile_y_start..tile_y_end {
5998 for tu in tile_x_start..tile_x_end {
5999 let pat_offset_x = tu as f64 * params.xstep;
6000 let pat_offset_y = tv as f64 * params.ystep;
6001
6002 let tile_transform = Transform::from_row(
6003 tt_sx,
6004 tt_ky,
6005 tt_kx,
6006 tt_sy,
6007 ((pm.a * pat_offset_x + pm.c * pat_offset_y + pm.tx - dev_vp_x) * sx_f) as f32,
6008 ((pm.b * pat_offset_x + pm.d * pat_offset_y + pm.ty - dev_vp_y) * sy_f) as f32,
6009 );
6010
6011 let bbox_clip = {
6013 let bb = ¶ms.bbox;
6014 let mut bp = stet_tiny_skia::PathBuilder::new();
6015 bp.move_to(bb[0] as f32, bb[1] as f32);
6016 bp.line_to(bb[2] as f32, bb[1] as f32);
6017 bp.line_to(bb[2] as f32, bb[3] as f32);
6018 bp.line_to(bb[0] as f32, bb[3] as f32);
6019 bp.close();
6020 bp.finish().and_then(|sp| {
6021 let mut m = Mask::new(ctx.out_w, ctx.out_h)?;
6022 m.fill_path(
6023 &sp,
6024 stet_tiny_skia::FillRule::Winding,
6025 false,
6026 tile_transform,
6027 );
6028 Some(m)
6029 })
6030 };
6031 let mut tile_clip: Option<Mask> = bbox_clip;
6032 let mut img_idx = 0usize;
6033 for elem in tile_elements {
6034 let clip_ref = tile_clip.as_ref();
6035 match elem {
6036 DisplayElement::Clip { path, params: cp } => {
6037 if let Some(sp) = build_skia_path(path) {
6038 let t = to_transform(&cp.ctm);
6039 let combined = t.post_concat(tile_transform);
6040 let mut mask = Mask::new(ctx.out_w, ctx.out_h).expect("mask");
6041 mask.fill_path(&sp, to_fill_rule(&cp.fill_rule), false, combined);
6042 if let Some(prev) = tile_clip.take() {
6043 intersect_masks(&mut mask, &prev);
6044 }
6045 tile_clip = Some(mask);
6046 }
6047 }
6048 DisplayElement::InitClip => {
6049 tile_clip = None;
6050 }
6051 DisplayElement::Fill { path, params: fp } => {
6052 if let Some(sp) = build_skia_path(path) {
6053 let mut paint = if params.paint_type == 1 {
6054 to_paint(&fp.color)
6055 } else {
6056 to_paint(
6057 params
6058 .underlying_color
6059 .as_ref()
6060 .unwrap_or(&DeviceColor::black()),
6061 )
6062 };
6063 paint.anti_alias = false;
6064 let t = to_transform(&fp.ctm);
6065 let combined = t.post_concat(tile_transform);
6066 let fr = to_fill_rule(&fp.fill_rule);
6067 tile_buf.fill_path(&sp, &paint, fr, combined, clip_ref);
6068 }
6069 }
6070 DisplayElement::Stroke { path, params: sp } => {
6071 if let Some(skp) = build_skia_path(path) {
6072 let effective_ctm = pm.concat(&sp.ctm);
6076 let mut sp_adj = sp.clone();
6077 sp_adj.ctm = effective_ctm;
6078 let stroke = build_stroke(&sp_adj, ctx.effective_dpi);
6079 let paint = if params.paint_type == 1 {
6080 to_paint(&sp.color)
6081 } else {
6082 to_paint(
6083 params
6084 .underlying_color
6085 .as_ref()
6086 .unwrap_or(&DeviceColor::black()),
6087 )
6088 };
6089 let t = to_transform(&sp.ctm);
6090 let combined = t.post_concat(tile_transform);
6091 tile_buf.stroke_path(&skp, &paint, &stroke, combined, clip_ref);
6092 }
6093 }
6094 DisplayElement::Image { .. } => {
6095 if let Some(ref pi) = preprocessed[img_idx] {
6096 let combined = pi.img_transform.post_concat(tile_transform);
6097 if let Some(img_ref) = stet_tiny_skia::PixmapRef::from_bytes(
6098 &pi.rgba, pi.width, pi.height,
6099 ) {
6100 let paint = stet_tiny_skia::PixmapPaint {
6101 opacity: 1.0,
6102 blend_mode: BlendMode::SourceOver,
6103 quality: stet_tiny_skia::FilterQuality::Nearest,
6104 };
6105 tile_buf.draw_pixmap(0, 0, img_ref, &paint, combined, clip_ref);
6106 }
6107 }
6108 img_idx += 1;
6109 }
6110 _ => {}
6111 }
6112 }
6113 }
6114 }
6115 }
6116
6117 let Some(fill_skia_path) = build_skia_path(¶ms.path) else {
6119 return;
6120 };
6121 let fill_rule = to_fill_rule(¶ms.fill_rule);
6122 let mut fill_mask = Mask::new(ctx.out_w, ctx.out_h).expect("mask");
6123 let path_transform = viewport_transform(
6124 Transform::identity(),
6125 ctx.vp_x,
6126 ctx.vp_y,
6127 ctx.scale_x,
6128 ctx.scale_y,
6129 );
6130 if let Some(ref sp) = params.stroke_params {
6131 let stroke = build_stroke(sp, ctx.effective_dpi);
6135 let ctm_transform = to_transform(&sp.ctm);
6136 let combined = ctm_transform.post_concat(path_transform);
6137 let res_scale = stet_tiny_skia::PathStroker::compute_resolution_scale(&combined);
6138 let dashed;
6139 let stroke_path = if let Some(ref dash) = stroke.dash {
6140 dashed = fill_skia_path.dash(dash, res_scale);
6141 match dashed.as_ref() {
6142 Some(p) => p,
6143 None => &fill_skia_path,
6144 }
6145 } else {
6146 &fill_skia_path
6147 };
6148 if let Some(outline) = stroke_path.stroke(&stroke, res_scale) {
6149 fill_mask.fill_path(
6150 &outline,
6151 stet_tiny_skia::FillRule::Winding,
6152 !ctx.no_aa,
6153 combined,
6154 );
6155 }
6156 } else {
6157 fill_mask.fill_path(&fill_skia_path, fill_rule, !ctx.no_aa, path_transform);
6158 }
6159
6160 if let Some(clip_mask) = mask_ref {
6161 intersect_masks(&mut fill_mask, clip_mask);
6162 }
6163
6164 let img_paint = stet_tiny_skia::PixmapPaint::default();
6165 pixmap.draw_pixmap(
6166 0,
6167 0,
6168 tile_buf.as_ref(),
6169 &img_paint,
6170 Transform::identity(),
6171 Some(&fill_mask),
6172 );
6173}
6174
6175fn clip_path_unified(
6180 band_state: &mut BandState,
6181 path: &PsPath,
6182 params: &ClipParams,
6183 ctx: &RenderContext<'_>,
6184) {
6185 let is_unit_scale = ctx.scale_x == 1.0 && ctx.scale_y == 1.0;
6186
6187 if is_unit_scale {
6189 let y_start = ctx.vp_y as u32;
6190 let x_start = ctx.vp_x as u32;
6191
6192 if x_start == 0
6197 && params.stroke_params.is_none()
6198 && params.ctm.a == 1.0
6199 && params.ctm.d == 1.0
6200 && params.ctm.tx == 0.0
6201 && params.ctm.ty == 0.0
6202 && let Some(bbox) = path_y_bbox(path)
6203 && (bbox.y_max <= y_start as f64 || bbox.y_min >= (y_start + ctx.out_h) as f64)
6204 {
6205 if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
6206 band_state.recycle_mask(mask);
6207 }
6208 band_state.clip_region = Some(ClipRegion::Rect(ClipRect {
6209 x0: 0,
6210 y0: 0,
6211 x1: 0,
6212 y1: 0,
6213 }));
6214 return;
6215 }
6216
6217 let ctm_is_identity = params.ctm.a == 1.0
6221 && params.ctm.b == 0.0
6222 && params.ctm.c == 0.0
6223 && params.ctm.d == 1.0
6224 && params.ctm.tx == 0.0
6225 && params.ctm.ty == 0.0;
6226 if x_start == 0
6227 && ctm_is_identity
6228 && params.stroke_params.is_none()
6229 && let Some(dev_rect) = detect_rect(path, ctx.out_w, u32::MAX)
6230 {
6231 let new_rect = translate_clip_rect(&dev_rect, y_start, ctx.out_h);
6232 match band_state.clip_region.take() {
6233 None => {
6234 band_state.clip_region = Some(ClipRegion::Rect(new_rect));
6235 }
6236 Some(ClipRegion::Rect(existing)) => {
6237 band_state.clip_region = Some(ClipRegion::Rect(existing.intersect(&new_rect)));
6238 }
6239 Some(ClipRegion::Mask(mut mask)) => {
6240 intersect_mask_with_rect(&mut mask, &new_rect, ctx.out_w, ctx.out_h);
6241 band_state.clip_region = Some(ClipRegion::Mask(mask));
6242 }
6243 }
6244 return;
6245 }
6246 }
6247
6248 let fill_rule = to_fill_rule(¶ms.fill_rule);
6250 let path_hash = hash_clip_path(path, ¶ms.fill_rule);
6251 let prev_region = band_state.clip_region.take();
6252
6253 let mut mask = band_state.take_mask(ctx.out_w, ctx.out_h);
6254
6255 let path_mask = if let Some(cached) = band_state.clip_mask_cache.get(&path_hash) {
6256 mask.data_mut().copy_from_slice(cached.data());
6257 mask
6258 } else {
6259 let Some(skia_path) = build_skia_path(path) else {
6260 band_state.recycle_mask(mask);
6261 band_state.clip_region = prev_region;
6262 return;
6263 };
6264 mask.data_mut().fill(0);
6265 if let Some(ref sp) = params.stroke_params {
6266 let stroke = build_stroke(sp, ctx.effective_dpi);
6269 let transform = ctx.transform(&sp.ctm);
6270 let res_scale = stet_tiny_skia::PathStroker::compute_resolution_scale(&transform);
6271 let dashed;
6272 let stroke_path = if let Some(ref dash) = stroke.dash {
6273 dashed = skia_path.dash(dash, res_scale);
6274 match dashed.as_ref() {
6275 Some(p) => p,
6276 None => &skia_path,
6277 }
6278 } else {
6279 &skia_path
6280 };
6281 if let Some(outline) = stroke_path.stroke(&stroke, res_scale) {
6282 mask.fill_path(
6283 &outline,
6284 stet_tiny_skia::FillRule::Winding,
6285 false,
6286 transform,
6287 );
6288 }
6289 } else {
6290 let transform = ctx.transform(¶ms.ctm);
6291 mask.fill_path(&skia_path, fill_rule, false, transform);
6292 }
6293 if !band_state.clip_mask_seen.insert(path_hash) {
6294 band_state.clip_mask_cache.insert(path_hash, mask.clone());
6295 }
6296 mask
6297 };
6298
6299 match prev_region {
6300 None => {
6301 band_state.clip_region = Some(ClipRegion::Mask(path_mask));
6302 }
6303 Some(ClipRegion::Rect(rect)) => {
6304 if rect.is_empty() {
6305 band_state.recycle_mask(path_mask);
6306 band_state.clip_region = Some(ClipRegion::Rect(rect));
6309 } else {
6310 let mut mask = path_mask;
6311 intersect_mask_with_rect(&mut mask, &rect, ctx.out_w, ctx.out_h);
6312 band_state.clip_region = Some(ClipRegion::Mask(mask));
6313 }
6314 }
6315 Some(ClipRegion::Mask(mut existing)) => {
6316 intersect_masks(&mut existing, &path_mask);
6317 band_state.recycle_mask(path_mask);
6318 band_state.clip_region = Some(ClipRegion::Mask(existing));
6319 }
6320 }
6321}
6322impl OutputDevice for SkiaDevice {
6323 fn fill_path(&mut self, path: &PsPath, params: &FillParams) {
6324 self.ensure_full_pixmap();
6325 let Some(skia_path) = build_skia_path(path) else {
6326 return;
6327 };
6328 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6329 let mut temp_mask = None;
6330 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6331 return; };
6333
6334 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, self.no_aa);
6335 let transform = to_transform(¶ms.ctm);
6336 let fill_rule = to_fill_rule(¶ms.fill_rule);
6337
6338 self.pixmap
6339 .fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
6340 }
6341
6342 fn stroke_path(&mut self, path: &PsPath, params: &StrokeParams) {
6343 self.ensure_full_pixmap();
6344 let stroke = build_stroke(params, self.dpi);
6345 let adjusted;
6346 let draw_path =
6347 if params.stroke_adjust && stroke.width <= 2.0 && ctm_is_device_space(¶ms.ctm) {
6348 adjusted =
6349 stroke_adjust_path_viewport(path, stroke.width as f64, 1.0, 1.0, 0.0, 0.0);
6350 &adjusted
6351 } else {
6352 path
6353 };
6354 let Some(skia_path) = build_skia_path(draw_path) else {
6355 return;
6356 };
6357 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, self.no_aa);
6358 let transform = to_transform(¶ms.ctm);
6359
6360 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6361 let mut temp_mask = None;
6362 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6363 return; };
6365
6366 self.pixmap
6367 .stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
6368 }
6369
6370 fn clip_path(&mut self, path: &PsPath, params: &ClipParams) {
6371 self.ensure_full_pixmap();
6372 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6373
6374 if let Some(new_rect) = detect_rect(path, w, h) {
6376 match self.clip_region.take() {
6377 None => {
6378 self.clip_region = Some(ClipRegion::Rect(new_rect));
6379 }
6380 Some(ClipRegion::Rect(existing)) => {
6381 self.clip_region = Some(ClipRegion::Rect(existing.intersect(&new_rect)));
6383 }
6384 Some(ClipRegion::Mask(mut mask)) => {
6385 intersect_mask_with_rect(&mut mask, &new_rect, w, h);
6387 self.clip_region = Some(ClipRegion::Mask(mask));
6388 }
6389 }
6390 return;
6391 }
6392
6393 let fill_rule = to_fill_rule(¶ms.fill_rule);
6395 let path_hash = hash_clip_path(path, ¶ms.fill_rule);
6396 let prev_region = self.clip_region.take();
6397
6398 macro_rules! take_spare {
6400 ($self:expr, $w:expr, $h:expr) => {
6401 $self
6402 .spare_mask
6403 .take()
6404 .unwrap_or_else(|| Mask::new($w, $h).expect("Failed to create mask"))
6405 };
6406 }
6407
6408 let path_mask = if let Some(cached) = self.clip_mask_cache.get(&path_hash) {
6410 let mut mask = take_spare!(self, w, h);
6412 mask.data_mut().copy_from_slice(cached.data());
6413 mask
6414 } else {
6415 let Some(skia_path) = build_skia_path(path) else {
6416 self.clip_region = prev_region;
6417 return;
6418 };
6419 let transform = to_transform(¶ms.ctm);
6420 let mut mask = take_spare!(self, w, h);
6421 mask.data_mut().fill(0); mask.fill_path(&skia_path, fill_rule, false, transform);
6423 if !self.clip_mask_seen.insert(path_hash) {
6425 self.clip_mask_cache.insert(path_hash, mask.clone());
6427 }
6428 mask
6429 };
6430
6431 match prev_region {
6432 None => {
6433 self.clip_region = Some(ClipRegion::Mask(path_mask));
6434 }
6435 Some(ClipRegion::Rect(rect)) => {
6436 if rect.is_empty() {
6437 self.spare_mask = Some(path_mask); } else {
6439 let mut mask = path_mask;
6440 intersect_mask_with_rect(&mut mask, &rect, w, h);
6441 self.clip_region = Some(ClipRegion::Mask(mask));
6442 }
6443 }
6444 Some(ClipRegion::Mask(mut existing)) => {
6445 intersect_masks(&mut existing, &path_mask);
6446 self.spare_mask = Some(path_mask); self.clip_region = Some(ClipRegion::Mask(existing));
6448 }
6449 }
6450 }
6451
6452 fn init_clip(&mut self) {
6453 if let Some(ClipRegion::Mask(mask)) = self.clip_region.take() {
6454 self.spare_mask = Some(mask);
6455 }
6456 self.clip_region = None;
6457 }
6458
6459 fn erase_page(&mut self) {
6460 self.pixmap.fill(Color::WHITE);
6463 if let Some(ClipRegion::Mask(mask)) = self.clip_region.take() {
6464 self.spare_mask = Some(mask);
6465 }
6466 self.clip_region = None;
6467 }
6468
6469 fn show_page(&mut self, output_path: &str) -> Result<(), String> {
6470 let w = self.pixmap.width();
6471 let h = self.pixmap.height();
6472 composite_onto_white(self.pixmap.data_mut());
6474 let mut sink = self.sink_factory.create_sink(output_path)?;
6475 sink.begin_page(w, h)?;
6476 sink.write_rows(self.pixmap.data(), h)?;
6477 sink.end_page()
6478 }
6479
6480 fn draw_image(&mut self, sample_data: &[u8], params: &ImageParams) {
6481 self.ensure_full_pixmap();
6482 let w = params.width;
6483 let h = params.height;
6484 if w == 0 || h == 0 {
6485 return;
6486 }
6487 let mut rgba_data =
6488 samples_to_rgba(sample_data, params, self.render_icc_cache.as_ref(), false);
6489 if params.mask_color.is_some() {
6490 apply_mask_color_rgba(&mut rgba_data, sample_data, params);
6491 }
6492 let expected = (w * h * 4) as usize;
6493 if rgba_data.len() < expected {
6494 return;
6495 }
6496
6497 let Some(image_inv) = params.image_matrix.invert() else {
6498 return;
6499 };
6500 let combined = params.ctm.concat(&image_inv);
6501 let raw_transform = enforce_min_image_size(to_transform(&combined), w, h);
6502
6503 let prescaled = prescale_image(&rgba_data, w, h, raw_transform, params.interpolate);
6504 let (img_data, img_w, img_h, transform) = match &prescaled {
6505 Some((data, pw, ph, t)) => (data.as_slice(), *pw, *ph, *t),
6506 None => (rgba_data.as_slice(), w, h, raw_transform),
6507 };
6508
6509 let Some(img_pixmap) = stet_tiny_skia::PixmapRef::from_bytes(img_data, img_w, img_h) else {
6510 return;
6511 };
6512
6513 let (pw, ph) = (self.pixmap.width(), self.pixmap.height());
6514 let mut temp_mask = None;
6515 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, pw, ph) else {
6516 return;
6517 };
6518
6519 let paint = stet_tiny_skia::PixmapPaint {
6520 quality: image_filter_quality(transform, params.interpolate),
6521 opacity: params.alpha as f32,
6522 blend_mode: u8_to_blend_mode(params.blend_mode),
6523 };
6524 self.pixmap
6525 .draw_pixmap(0, 0, img_pixmap, &paint, transform, mask_ref);
6526 }
6527
6528 fn paint_axial_shading(&mut self, params: &AxialShadingParams) {
6529 self.ensure_full_pixmap();
6530 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6531 let mut temp_mask = None;
6532 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6533 return;
6534 };
6535 render_axial_shading(
6536 &mut self.pixmap,
6537 params,
6538 0.0,
6539 0.0,
6540 1.0,
6541 1.0,
6542 mask_ref,
6543 self.no_aa,
6544 None,
6545 None,
6546 );
6547 }
6548
6549 fn paint_radial_shading(&mut self, params: &RadialShadingParams) {
6550 self.ensure_full_pixmap();
6551 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6552 let mut temp_mask = None;
6553 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6554 return;
6555 };
6556 render_radial_shading(
6557 &mut self.pixmap,
6558 params,
6559 0.0,
6560 0.0,
6561 1.0,
6562 1.0,
6563 mask_ref,
6564 self.no_aa,
6565 None,
6566 None,
6567 );
6568 }
6569
6570 fn paint_mesh_shading(&mut self, params: &MeshShadingParams) {
6571 self.ensure_full_pixmap();
6572 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6573 let mut temp_mask = None;
6574 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6575 return;
6576 };
6577 render_mesh_shading(
6578 &mut self.pixmap,
6579 params,
6580 0.0,
6581 0.0,
6582 1.0,
6583 1.0,
6584 mask_ref,
6585 None,
6586 None,
6587 );
6588 }
6589
6590 fn paint_patch_shading(&mut self, params: &PatchShadingParams) {
6591 self.ensure_full_pixmap();
6592 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6593 let mut temp_mask = None;
6594 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6595 return;
6596 };
6597 render_patch_shading(
6598 &mut self.pixmap,
6599 params,
6600 0.0,
6601 0.0,
6602 1.0,
6603 1.0,
6604 mask_ref,
6605 None,
6606 None,
6607 );
6608 }
6609
6610 fn paint_pattern_fill(&mut self, params: &stet_graphics::device::PatternFillParams) {
6611 self.ensure_full_pixmap();
6612 let w = self.pixmap.width();
6613 let h = self.pixmap.height();
6614 let mut band_state = BandState {
6615 clip_region: self.clip_region.take(),
6616 spare_mask: self.spare_mask.take(),
6617 clip_mask_cache: HashMap::new(),
6618 clip_mask_seen: HashSet::new(),
6619 mask_pool: Vec::new(),
6620 cmyk_buffer: None,
6621 op_bg_snapshot: None,
6622 op_touched: None,
6623 spot_mask: None,
6624 };
6625 {
6626 let ctx = RenderContext {
6627 vp_x: 0.0,
6628 vp_y: 0.0,
6629 scale_x: 1.0,
6630 scale_y: 1.0,
6631 out_w: w,
6632 out_h: h,
6633 effective_dpi: self.dpi,
6634 icc: None,
6635 image_cache: None,
6636 preprocessed: None,
6637 elem_idx: 0,
6638 no_aa: self.no_aa,
6639 opm_zero_transparent: false,
6640 knockout_painter_pass: KnockoutPainterPass::None,
6641 parent_group_isolated: false,
6642 alpha_extraction_pass: false,
6643 };
6644 render_pattern_fill(&mut self.pixmap, &mut band_state, params, &ctx);
6645 }
6646 self.clip_region = band_state.clip_region.take();
6647 if let Some(mask) = band_state.spare_mask.take() {
6648 self.spare_mask = Some(mask);
6649 }
6650 }
6651
6652 fn page_size(&self) -> (u32, u32) {
6653 (self.page_w, self.page_h)
6654 }
6655
6656 fn replay_and_show(&mut self, list: DisplayList, output_path: &str) -> Result<(), String> {
6657 self.join_pending()?;
6659
6660 let (page_w, page_h) = self.page_size();
6661
6662 if self.use_viewport_path {
6667 let icc_cache = build_icc_cache_for_list(&list, self.system_cmyk_bytes.as_ref());
6668 let rgba = render_to_rgba_viewport(
6669 &list,
6670 page_w,
6671 page_h,
6672 self.dpi,
6673 Some(&icc_cache),
6674 self.no_aa,
6675 );
6676 let mut sink = self.sink_factory.create_sink(output_path)?;
6677 sink.begin_page(page_w, page_h)?;
6678 sink.write_rows(&rgba, page_h)?;
6679 sink.end_page()?;
6680 return Ok(());
6681 }
6682
6683 let band_h = select_band_height(page_w, page_h);
6684
6685 let icc_cache = build_icc_cache_for_list(&list, self.system_cmyk_bytes.as_ref());
6687
6688 if band_h >= page_h {
6692 self.ensure_full_pixmap();
6693 let ctx = RenderContext {
6694 vp_x: 0.0,
6695 vp_y: 0.0,
6696 scale_x: 1.0,
6697 scale_y: 1.0,
6698 out_w: page_w,
6699 out_h: page_h,
6700 effective_dpi: self.dpi,
6701 icc: Some(&icc_cache),
6702 image_cache: None,
6703 preprocessed: None,
6704 elem_idx: 0,
6705 no_aa: self.no_aa,
6706 opm_zero_transparent: false,
6707 knockout_painter_pass: KnockoutPainterPass::None,
6708 parent_group_isolated: false,
6709 alpha_extraction_pass: false,
6710 };
6711 let mut band_state = BandState {
6712 clip_region: None,
6713 spare_mask: None,
6714 clip_mask_cache: HashMap::new(),
6715 clip_mask_seen: HashSet::new(),
6716 mask_pool: Vec::new(),
6717 cmyk_buffer: None,
6718 op_bg_snapshot: None,
6719 op_touched: None,
6720 spot_mask: None,
6721 };
6722 for (idx, elem) in list.elements().iter().enumerate() {
6723 let elem_ctx = RenderContext {
6724 elem_idx: idx,
6725 ..ctx
6726 };
6727 render_element(&mut self.pixmap, &mut band_state, elem, &elem_ctx);
6728 }
6729 return self.show_page(output_path);
6730 }
6731
6732 if self.pixmap.width() > 1 {
6736 self.pixmap = Pixmap::new(1, 1).expect("Failed to create placeholder pixmap");
6737 }
6738
6739 let mut sink = self.sink_factory.create_sink(output_path)?;
6741 let dpi = self.dpi;
6742
6743 #[cfg(feature = "parallel")]
6744 {
6745 let no_aa = self.no_aa;
6749 let (tx, rx) = std::sync::mpsc::sync_channel(1);
6750 rayon::spawn(move || {
6751 let result = render_banded_to_sink(
6752 page_w, page_h, band_h, dpi, &list, &mut *sink, &icc_cache, no_aa,
6753 );
6754 let _ = tx.send(result);
6755 });
6756 self.pending_render = Some(rx);
6757 }
6758 #[cfg(not(feature = "parallel"))]
6759 {
6760 render_banded_to_sink(
6761 page_w, page_h, band_h, dpi, &list, &mut *sink, &icc_cache, self.no_aa,
6762 )?;
6763 }
6764
6765 Ok(())
6766 }
6767
6768 fn finish(&mut self) -> Result<(), String> {
6769 self.join_pending()
6770 }
6771}
6772
6773impl Drop for SkiaDevice {
6774 fn drop(&mut self) {
6775 if let Some(rx) = self.pending_render.take() {
6777 let _ = rx.recv();
6778 }
6779 }
6780}
6781
6782impl SkiaDevice {
6783 fn join_pending(&mut self) -> Result<(), String> {
6785 if let Some(rx) = self.pending_render.take() {
6786 match rx.recv() {
6787 Ok(result) => result?,
6788 Err(_) => return Err("Background render task failed".to_string()),
6789 }
6790 }
6791 Ok(())
6792 }
6793}
6794
6795fn has_cmyk_group(list: &DisplayList) -> bool {
6800 use stet_graphics::display_list::GroupColorSpace;
6801 for elem in list.elements() {
6802 match elem {
6803 DisplayElement::Group { elements, params } => {
6804 if params.color_space == GroupColorSpace::DeviceCMYK {
6805 return true;
6806 }
6807 if has_cmyk_group(elements) {
6808 return true;
6809 }
6810 }
6811 DisplayElement::SoftMasked { content, mask, .. } => {
6812 if has_cmyk_group(content) || has_cmyk_group(mask) {
6813 return true;
6814 }
6815 }
6816 DisplayElement::OcgGroup { elements, .. } => {
6817 if has_cmyk_group(elements) {
6818 return true;
6819 }
6820 }
6821 _ => {}
6822 }
6823 }
6824 false
6825}
6826
6827fn group_only_native_cmyk_fills(elements: &DisplayList) -> bool {
6834 let mut found_paint = false;
6835 for elem in elements.elements() {
6836 match elem {
6837 DisplayElement::InitClip => continue,
6838 DisplayElement::Clip { .. } => continue,
6839 DisplayElement::Fill { params, .. } => {
6840 if params.color.native_cmyk.is_none() {
6841 return false;
6842 }
6843 found_paint = true;
6844 }
6845 DisplayElement::Stroke { params, .. } => {
6846 if params.color.native_cmyk.is_none() {
6852 return false;
6853 }
6854 found_paint = true;
6855 }
6856 _ => return false,
6857 }
6858 }
6859 found_paint
6860}
6861
6862fn group_content_is_native_cmyk(elements: &DisplayList) -> bool {
6883 let mut found_paint = false;
6884 for elem in elements.elements() {
6885 match elem {
6886 DisplayElement::InitClip => continue,
6887 DisplayElement::Clip { .. } => continue,
6888 DisplayElement::Text { .. } => continue,
6889 DisplayElement::ErasePage => continue,
6890 DisplayElement::Fill { params, .. } => {
6891 if params.color.native_cmyk.is_none() {
6892 return false;
6893 }
6894 found_paint = true;
6895 }
6896 DisplayElement::Stroke { params, .. } => {
6897 if params.color.native_cmyk.is_none() {
6898 return false;
6899 }
6900 found_paint = true;
6901 }
6902 DisplayElement::Image { params, .. } => {
6903 if !is_cmyk_color_space(¶ms.color_space) {
6904 return false;
6905 }
6906 found_paint = true;
6907 }
6908 DisplayElement::AxialShading { .. }
6909 | DisplayElement::RadialShading { .. }
6910 | DisplayElement::MeshShading { .. }
6911 | DisplayElement::PatchShading { .. } => {
6912 return false;
6915 }
6916 DisplayElement::PatternFill { .. } => {
6917 return false;
6921 }
6922 DisplayElement::Group { elements: sub, .. } => {
6923 if !group_content_is_native_cmyk(sub) {
6924 return false;
6925 }
6926 found_paint = true;
6927 }
6928 DisplayElement::SoftMasked { .. } => {
6929 return false;
6940 }
6941 DisplayElement::OcgGroup { elements: sub, .. } => {
6942 if !group_content_is_native_cmyk(sub) {
6943 return false;
6944 }
6945 found_paint = true;
6946 }
6947 }
6948 }
6949 found_paint
6950}
6951
6952fn content_list_is_simple_native_cmyk(list: &DisplayList) -> bool {
6959 let mut found_paint = false;
6960 for elem in list.elements() {
6961 match elem {
6962 DisplayElement::InitClip
6963 | DisplayElement::Clip { .. }
6964 | DisplayElement::Text { .. }
6965 | DisplayElement::ErasePage => continue,
6966 DisplayElement::Fill { params, .. } => {
6967 if params.color.native_cmyk.is_none() {
6968 return false;
6969 }
6970 if params.blend_mode != 0 || params.alpha != 1.0 {
6971 return false;
6972 }
6973 found_paint = true;
6974 }
6975 DisplayElement::Stroke { params, .. } => {
6976 if params.color.native_cmyk.is_none() {
6977 return false;
6978 }
6979 if params.blend_mode != 0 || params.alpha != 1.0 {
6980 return false;
6981 }
6982 found_paint = true;
6983 }
6984 _ => return false,
6985 }
6986 }
6987 found_paint
6988}
6989
6990fn has_overprint_elements(list: &DisplayList) -> bool {
6992 for elem in list.elements() {
6993 match elem {
6994 DisplayElement::Fill { params, .. } => {
6995 if params.overprint {
6996 return true;
6997 }
6998 }
6999 DisplayElement::Stroke { params, .. } => {
7000 if params.overprint {
7001 return true;
7002 }
7003 }
7004 DisplayElement::Image { params, .. } => {
7005 if params.overprint {
7006 return true;
7007 }
7008 }
7009 DisplayElement::AxialShading { params } => {
7010 if params.overprint {
7011 return true;
7012 }
7013 }
7014 DisplayElement::RadialShading { params } => {
7015 if params.overprint {
7016 return true;
7017 }
7018 }
7019 DisplayElement::MeshShading { params } => {
7020 if params.overprint {
7021 return true;
7022 }
7023 }
7024 DisplayElement::PatchShading { params } => {
7025 if params.overprint {
7026 return true;
7027 }
7028 }
7029 DisplayElement::Group { elements, .. } => {
7030 if has_overprint_elements(elements) {
7031 return true;
7032 }
7033 }
7034 DisplayElement::SoftMasked { content, mask, .. } => {
7035 if has_overprint_elements(content) || has_overprint_elements(mask) {
7036 return true;
7037 }
7038 }
7039 DisplayElement::OcgGroup { elements, .. } => {
7040 if has_overprint_elements(elements) {
7041 return true;
7042 }
7043 }
7044 _ => {}
7045 }
7046 }
7047 false
7048}
7049
7050#[allow(clippy::too_many_arguments)]
7053fn render_overprint_fill(
7054 pixmap: &mut Pixmap,
7055 cmyk_buf: &mut [f32],
7056 op_bg: &mut [u8],
7057 op_touched: &mut [u8],
7058 spot_mask: &[u8],
7059 band_state: &mut BandState,
7060 path: &PsPath,
7061 params: &FillParams,
7062 vp_x: f32,
7063 vp_y: f32,
7064 scale_x: f32,
7065 scale_y: f32,
7066 out_w: u32,
7067 out_h: u32,
7068 icc: Option<&IccCache>,
7069 no_aa: bool,
7070) {
7071 let Some(skia_path) = build_skia_path(path) else {
7072 return;
7073 };
7074 let fill_rule = to_fill_rule(¶ms.fill_rule);
7075
7076 let mut coverage_mask = match Mask::new(out_w, out_h) {
7077 Some(m) => m,
7078 None => return,
7079 };
7080 let transform = viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
7081 coverage_mask.fill_path(&skia_path, fill_rule, !no_aa, transform);
7082
7083 let (bbox_x0, bbox_y0, bbox_x1, bbox_y1) =
7085 path_device_bbox(&skia_path, transform, out_w, out_h);
7086
7087 let clip_coverage: Option<&[u8]> = match &band_state.clip_region {
7089 None => None,
7090 Some(ClipRegion::Rect(r)) => {
7091 let data = coverage_mask.data_mut();
7093 let stride = out_w as usize;
7094 for y in bbox_y0..bbox_y1 {
7095 let row_start = y * stride;
7096 for x in bbox_x0..bbox_x1 {
7097 let yu = y as u32;
7098 let xu = x as u32;
7099 if yu < r.y0 || yu >= r.y1 || xu < r.x0 || xu >= r.x1 {
7100 data[row_start + x] = 0;
7101 }
7102 }
7103 }
7104 None
7105 }
7106 Some(ClipRegion::Mask(clip_mask)) => Some(clip_mask.data()),
7107 };
7108
7109 let (src_c, src_m, src_y, src_k) = params.color.native_cmyk.unwrap_or_else(|| {
7110 let r = params.color.r;
7111 let g = params.color.g;
7112 let b = params.color.b;
7113 (1.0 - r, 1.0 - g, 1.0 - b, 0.0)
7114 });
7115
7116 let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
7122
7123 let mut channels = params.painted_channels;
7124 if channels == 0 {
7127 channels = stet_graphics::device::CMYK_ALL;
7128 }
7129 if params.overprint_mode == 1
7131 && channels == stet_graphics::device::CMYK_ALL
7132 && params.is_device_cmyk
7133 {
7134 channels = 0;
7135 if src_c != 0.0 {
7136 channels |= stet_graphics::device::CMYK_C;
7137 }
7138 if src_m != 0.0 {
7139 channels |= stet_graphics::device::CMYK_M;
7140 }
7141 if src_y != 0.0 {
7142 channels |= stet_graphics::device::CMYK_Y;
7143 }
7144 if src_k != 0.0 {
7145 channels |= stet_graphics::device::CMYK_K;
7146 }
7147 if channels == 0 && !params.opm_paired {
7158 channels = stet_graphics::device::CMYK_ALL;
7159 }
7160 }
7161
7162 let is_k_only_cmyk = params.is_device_cmyk
7170 && params.overprint_mode == 0
7171 && src_c == 0.0
7172 && src_m == 0.0
7173 && src_y == 0.0;
7174 if channels == stet_graphics::device::CMYK_ALL && !is_custom_spot && !is_k_only_cmyk {
7175 let cov_data = coverage_mask.data();
7176 let stride = out_w as usize;
7177 for y in bbox_y0..bbox_y1 {
7178 for x in bbox_x0..bbox_x1 {
7179 let mi = y * stride + x;
7180 let mut cov = cov_data[mi] as f32 / 255.0;
7181 if let Some(clip) = clip_coverage {
7182 cov *= clip[mi] as f32 / 255.0;
7183 }
7184 if cov > 0.0 {
7185 let ci = mi * 4;
7186 cmyk_buf[ci] = src_c as f32;
7187 cmyk_buf[ci + 1] = src_m as f32;
7188 cmyk_buf[ci + 2] = src_y as f32;
7189 cmyk_buf[ci + 3] = src_k as f32;
7190 }
7191 }
7192 }
7193 let mut temp_mask = None;
7194 let Some(mask_ref) =
7195 resolve_clip_mask(&band_state.clip_region, &mut temp_mask, out_w, out_h)
7196 else {
7197 return;
7198 };
7199 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, no_aa);
7200 pixmap.fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
7201 return;
7202 }
7203
7204 let cov_data = coverage_mask.data();
7205 let stride = out_w as usize;
7206 let px_data = pixmap.data_mut();
7207 let px_stride = out_w as usize * 4;
7208
7209 for y in bbox_y0..bbox_y1 {
7210 for x in bbox_x0..bbox_x1 {
7211 let mi = y * stride + x;
7212 let mut cov = cov_data[mi] as f32 / 255.0;
7213 if let Some(clip) = clip_coverage {
7214 cov *= clip[mi] as f32 / 255.0;
7215 }
7216 if cov <= 0.0 {
7217 continue;
7218 }
7219
7220 let ci = mi * 4;
7221 let pi = y * px_stride + x * 4;
7222 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
7229 op_bg[pi] = px_data[pi];
7230 op_bg[pi + 1] = px_data[pi + 1];
7231 op_bg[pi + 2] = px_data[pi + 2];
7232 op_bg[pi + 3] = px_data[pi + 3];
7233 op_touched[mi] = 1;
7234 }
7235 let cur_c = cmyk_buf[ci] as f64;
7236 let cur_m = cmyk_buf[ci + 1] as f64;
7237 let cur_y = cmyk_buf[ci + 2] as f64;
7238 let cur_k = cmyk_buf[ci + 3] as f64;
7239 let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
7251 let pixmap_has_colour = px_data[pi + 3] > 0
7252 && (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
7253 let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
7261
7262 let is_promoted_gray = params.painted_channels == stet_graphics::device::CMYK_K
7278 && channels == stet_graphics::device::CMYK_K
7279 && params.is_device_cmyk
7280 && src_c == 0.0
7281 && src_m == 0.0
7282 && src_y == 0.0;
7283 let effective_channels = if is_promoted_gray && spot_mask[mi] == 0 {
7284 stet_graphics::device::CMYK_ALL
7285 } else {
7286 channels
7287 };
7288
7289 let new_c = if effective_channels & stet_graphics::device::CMYK_C != 0 {
7290 src_c
7291 } else {
7292 cur_c
7293 };
7294 let new_m = if effective_channels & stet_graphics::device::CMYK_M != 0 {
7295 src_m
7296 } else {
7297 cur_m
7298 };
7299 let new_y = if effective_channels & stet_graphics::device::CMYK_Y != 0 {
7300 src_y
7301 } else {
7302 cur_y
7303 };
7304 let new_k = if effective_channels & stet_graphics::device::CMYK_K != 0 {
7305 src_k
7306 } else {
7307 cur_k
7308 };
7309
7310 if !is_custom_spot {
7314 cmyk_buf[ci] = new_c as f32;
7315 cmyk_buf[ci + 1] = new_m as f32;
7316 cmyk_buf[ci + 2] = new_y as f32;
7317 cmyk_buf[ci + 3] = new_k as f32;
7318 }
7319
7320 let delta = (new_c - cur_c)
7334 .abs()
7335 .max((new_m - cur_m).abs())
7336 .max((new_y - cur_y).abs())
7337 .max((new_k - cur_k).abs());
7338 if delta < 1e-4 && spot_mask[mi] != 0 && pixmap_has_colour && !is_custom_spot {
7339 continue;
7340 }
7341
7342 let (r, g, b) =
7343 if is_promoted_gray && effective_channels == stet_graphics::device::CMYK_ALL {
7344 (params.color.r, params.color.g, params.color.b)
7359 } else if use_multiplicative {
7360 let bg_r = px_data[pi] as f64 / 255.0;
7366 let bg_g = px_data[pi + 1] as f64 / 255.0;
7367 let bg_b = px_data[pi + 2] as f64 / 255.0;
7368 let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
7369 1.0 - src_c
7370 } else {
7371 1.0
7372 };
7373 let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
7374 1.0 - src_m
7375 } else {
7376 1.0
7377 };
7378 let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
7379 1.0 - src_y
7380 } else {
7381 1.0
7382 };
7383 let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
7384 1.0 - src_k
7385 } else {
7386 1.0
7387 };
7388 (
7389 (bg_r * over_r * k_fac).clamp(0.0, 1.0),
7390 (bg_g * over_g * k_fac).clamp(0.0, 1.0),
7391 (bg_b * over_b * k_fac).clamp(0.0, 1.0),
7392 )
7393 } else if let Some(icc_cache) = icc {
7394 icc_cache
7395 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
7396 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
7397 } else {
7398 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
7399 };
7400
7401 let a = (cov * params.alpha as f32).min(1.0);
7402 let (bk_r, bk_g, bk_b, bk_a) = if op_touched[mi] != 0 {
7415 let new_r = (r as f32 * 255.0).clamp(0.0, 255.0);
7416 let new_g = (g as f32 * 255.0).clamp(0.0, 255.0);
7417 let new_b = (b as f32 * 255.0).clamp(0.0, 255.0);
7418 let dr = (op_bg[pi] as f32 - new_r).abs();
7419 let dg = (op_bg[pi + 1] as f32 - new_g).abs();
7420 let db = (op_bg[pi + 2] as f32 - new_b).abs();
7421 if dr.max(dg).max(db) <= 4.0 {
7422 (op_bg[pi], op_bg[pi + 1], op_bg[pi + 2], op_bg[pi + 3])
7423 } else {
7424 (
7425 px_data[pi],
7426 px_data[pi + 1],
7427 px_data[pi + 2],
7428 px_data[pi + 3],
7429 )
7430 }
7431 } else {
7432 (
7433 px_data[pi],
7434 px_data[pi + 1],
7435 px_data[pi + 2],
7436 px_data[pi + 3],
7437 )
7438 };
7439 let dst_a = bk_a as f32 / 255.0;
7440 let one_minus_a = 1.0 - a;
7441 let out_a = a + dst_a * one_minus_a;
7442 if out_a > 0.0 {
7443 px_data[pi] = ((r as f32 * a + (bk_r as f32 / 255.0) * one_minus_a) * 255.0)
7448 .clamp(0.0, 255.0)
7449 .round() as u8;
7450 px_data[pi + 1] = ((g as f32 * a + (bk_g as f32 / 255.0) * one_minus_a) * 255.0)
7451 .clamp(0.0, 255.0)
7452 .round() as u8;
7453 px_data[pi + 2] = ((b as f32 * a + (bk_b as f32 / 255.0) * one_minus_a) * 255.0)
7454 .clamp(0.0, 255.0)
7455 .round() as u8;
7456 px_data[pi + 3] = (out_a * 255.0).round() as u8;
7457 }
7458 }
7459 }
7460}
7461fn cmyk_to_rgb_plrm(c: f64, m: f64, y: f64, k: f64) -> (f64, f64, f64) {
7463 (
7464 1.0 - (c + k).min(1.0),
7465 1.0 - (m + k).min(1.0),
7466 1.0 - (y + k).min(1.0),
7467 )
7468}
7469
7470#[allow(clippy::too_many_arguments)]
7472fn path_device_bbox(
7475 skia_path: &stet_tiny_skia::Path,
7476 transform: Transform,
7477 w: u32,
7478 h: u32,
7479) -> (usize, usize, usize, usize) {
7480 let b = skia_path.bounds();
7481 let mut corners = [
7482 stet_tiny_skia::Point {
7483 x: b.left(),
7484 y: b.top(),
7485 },
7486 stet_tiny_skia::Point {
7487 x: b.right(),
7488 y: b.top(),
7489 },
7490 stet_tiny_skia::Point {
7491 x: b.right(),
7492 y: b.bottom(),
7493 },
7494 stet_tiny_skia::Point {
7495 x: b.left(),
7496 y: b.bottom(),
7497 },
7498 ];
7499 transform.map_points(&mut corners);
7500 let min_x = corners.iter().map(|p| p.x).fold(f32::INFINITY, f32::min);
7501 let min_y = corners.iter().map(|p| p.y).fold(f32::INFINITY, f32::min);
7502 let max_x = corners
7503 .iter()
7504 .map(|p| p.x)
7505 .fold(f32::NEG_INFINITY, f32::max);
7506 let max_y = corners
7507 .iter()
7508 .map(|p| p.y)
7509 .fold(f32::NEG_INFINITY, f32::max);
7510 let x0 = (min_x.floor() as i32 - 1).max(0) as usize;
7512 let y0 = (min_y.floor() as i32 - 1).max(0) as usize;
7513 let x1 = (max_x.ceil() as i32 + 1).clamp(0, w as i32) as usize;
7514 let y1 = (max_y.ceil() as i32 + 1).clamp(0, h as i32) as usize;
7515 (x0, y0, x1, y1)
7516}
7517
7518fn update_cmyk_buffer_for_fill(
7519 cmyk_buf: &mut [f32],
7520 spot_mask: &mut [u8],
7521 path: &PsPath,
7522 params: &FillParams,
7523 vp_x: f32,
7524 vp_y: f32,
7525 scale_x: f32,
7526 scale_y: f32,
7527 out_w: u32,
7528 out_h: u32,
7529 clip_region: &Option<ClipRegion>,
7530 no_aa: bool,
7531 icc: Option<&IccCache>,
7532) {
7533 let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
7539
7540 let has_spot_contrib = (is_custom_spot && params.color.native_cmyk.is_some())
7554 || matches!(
7555 (params.color.native_cmyk, params.color.process_cmyk),
7556 (Some(nat), Some(proc_))
7557 if (nat.0 - proc_.0).abs() > 1e-6
7558 || (nat.1 - proc_.1).abs() > 1e-6
7559 || (nat.2 - proc_.2).abs() > 1e-6
7560 || (nat.3 - proc_.3).abs() > 1e-6
7561 );
7562
7563 let (src_c, src_m, src_y, src_k) = if is_custom_spot {
7571 (0.0, 0.0, 0.0, 0.0)
7572 } else if let Some(c) = params.color.process_cmyk {
7573 c
7574 } else if let Some(c) = params.color.native_cmyk {
7575 c
7576 } else if let Some(cmyk) = icc.and_then(|i| {
7577 i.convert_rgb_to_cmyk_readonly(params.color.r, params.color.g, params.color.b)
7578 }) {
7579 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
7580 } else {
7581 (
7582 (1.0 - params.color.r).clamp(0.0, 1.0),
7583 (1.0 - params.color.g).clamp(0.0, 1.0),
7584 (1.0 - params.color.b).clamp(0.0, 1.0),
7585 0.0,
7586 )
7587 };
7588 let Some(skia_path) = build_skia_path(path) else {
7589 return;
7590 };
7591
7592 let mut coverage_mask = match Mask::new(out_w, out_h) {
7593 Some(m) => m,
7594 None => return,
7595 };
7596 let transform = viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
7597 let fill_rule = to_fill_rule(¶ms.fill_rule);
7598 coverage_mask.fill_path(&skia_path, fill_rule, !no_aa, transform);
7599
7600 let cov_data = coverage_mask.data();
7601 let clip_data: Option<&[u8]> = match clip_region {
7602 Some(ClipRegion::Mask(m)) => Some(m.data()),
7603 _ => None,
7604 };
7605
7606 let (mut bx0, mut by0, mut bx1, mut by1) =
7608 path_device_bbox(&skia_path, transform, out_w, out_h);
7609 if let Some(ClipRegion::Rect(r)) = clip_region {
7610 bx0 = bx0.max(r.x0 as usize);
7611 by0 = by0.max(r.y0 as usize);
7612 bx1 = bx1.min(r.x1 as usize);
7613 by1 = by1.min(r.y1 as usize);
7614 }
7615
7616 let stride = out_w as usize;
7617 for y in by0..by1 {
7618 for x in bx0..bx1 {
7619 let mi = y * stride + x;
7620 let mut cov = cov_data[mi] as f32 / 255.0;
7621 if let Some(clip) = clip_data {
7622 cov *= clip[mi] as f32 / 255.0;
7623 }
7624 if cov > 0.0 {
7625 let ci = mi * 4;
7626 cmyk_buf[ci] = src_c as f32;
7627 cmyk_buf[ci + 1] = src_m as f32;
7628 cmyk_buf[ci + 2] = src_y as f32;
7629 cmyk_buf[ci + 3] = src_k as f32;
7630 if has_spot_contrib {
7631 spot_mask[mi] = 1;
7632 }
7633 }
7634 }
7635 }
7636}
7637
7638#[allow(clippy::too_many_arguments)]
7643fn render_overprint_stroke(
7644 pixmap: &mut Pixmap,
7645 cmyk_buf: &mut [f32],
7646 op_bg: &mut [u8],
7647 op_touched: &mut [u8],
7648 spot_mask: &[u8],
7649 band_state: &mut BandState,
7650 skia_path: &stet_tiny_skia::Path,
7651 stroke: &Stroke,
7652 transform: Transform,
7653 params: &StrokeParams,
7654 out_w: u32,
7655 out_h: u32,
7656 icc: Option<&IccCache>,
7657 no_aa: bool,
7658) {
7659 let resolution_scale = (transform.sx * transform.sx + transform.sy * transform.sy)
7661 .sqrt()
7662 .max(1.0);
7663 let dashed_op;
7664 let stroke_src = if let Some(ref dash) = stroke.dash {
7665 dashed_op = skia_path.dash(dash, resolution_scale);
7666 match dashed_op.as_ref() {
7667 Some(p) => p,
7668 None => skia_path,
7669 }
7670 } else {
7671 skia_path
7672 };
7673 let Some(stroked_user) = stroke_src.stroke(stroke, resolution_scale) else {
7674 return;
7675 };
7676 let Some(stroked) = stroked_user.transform(transform) else {
7677 return;
7678 };
7679
7680 let mut coverage_mask = match Mask::new(out_w, out_h) {
7681 Some(m) => m,
7682 None => return,
7683 };
7684 coverage_mask.fill_path(
7685 &stroked,
7686 SkiaFillRule::Winding,
7687 !no_aa,
7688 Transform::identity(),
7689 );
7690
7691 let (bbox_x0, bbox_y0, bbox_x1, bbox_y1) =
7692 path_device_bbox(&stroked, Transform::identity(), out_w, out_h);
7693
7694 let clip_coverage: Option<&[u8]> = match &band_state.clip_region {
7696 None => None,
7697 Some(ClipRegion::Rect(r)) => {
7698 let data = coverage_mask.data_mut();
7699 let stride = out_w as usize;
7700 for y in bbox_y0..bbox_y1 {
7701 let row_start = y * stride;
7702 for x in bbox_x0..bbox_x1 {
7703 let yu = y as u32;
7704 let xu = x as u32;
7705 if yu < r.y0 || yu >= r.y1 || xu < r.x0 || xu >= r.x1 {
7706 data[row_start + x] = 0;
7707 }
7708 }
7709 }
7710 None
7711 }
7712 Some(ClipRegion::Mask(clip_mask)) => Some(clip_mask.data()),
7713 };
7714
7715 let (src_c, src_m, src_y, src_k) = params.color.native_cmyk.unwrap_or_else(|| {
7716 let r = params.color.r;
7717 let g = params.color.g;
7718 let b = params.color.b;
7719 (1.0 - r, 1.0 - g, 1.0 - b, 0.0)
7720 });
7721
7722 let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
7726
7727 let mut channels = params.painted_channels;
7728 if channels == 0 {
7729 channels = stet_graphics::device::CMYK_ALL;
7730 }
7731 if params.overprint_mode == 1
7732 && channels == stet_graphics::device::CMYK_ALL
7733 && params.is_device_cmyk
7734 {
7735 channels = 0;
7736 if src_c != 0.0 {
7737 channels |= stet_graphics::device::CMYK_C;
7738 }
7739 if src_m != 0.0 {
7740 channels |= stet_graphics::device::CMYK_M;
7741 }
7742 if src_y != 0.0 {
7743 channels |= stet_graphics::device::CMYK_Y;
7744 }
7745 if src_k != 0.0 {
7746 channels |= stet_graphics::device::CMYK_K;
7747 }
7748 if channels == 0 && !params.opm_paired {
7752 channels = stet_graphics::device::CMYK_ALL;
7753 }
7754 }
7755
7756 let is_k_only_cmyk = params.is_device_cmyk
7757 && params.overprint_mode == 0
7758 && src_c == 0.0
7759 && src_m == 0.0
7760 && src_y == 0.0;
7761 if channels == stet_graphics::device::CMYK_ALL && !is_custom_spot && !is_k_only_cmyk {
7762 let cov_data = coverage_mask.data();
7767 let stride = out_w as usize;
7768 for y in bbox_y0..bbox_y1 {
7769 for x in bbox_x0..bbox_x1 {
7770 let mi = y * stride + x;
7771 let mut cov = cov_data[mi] as f32 / 255.0;
7772 if let Some(clip) = clip_coverage {
7773 cov *= clip[mi] as f32 / 255.0;
7774 }
7775 if cov > 0.0 {
7776 let ci = mi * 4;
7777 cmyk_buf[ci] = src_c as f32;
7778 cmyk_buf[ci + 1] = src_m as f32;
7779 cmyk_buf[ci + 2] = src_y as f32;
7780 cmyk_buf[ci + 3] = src_k as f32;
7781 }
7782 }
7783 }
7784 let mut temp_mask = None;
7785 let Some(mask_ref) =
7786 resolve_clip_mask(&band_state.clip_region, &mut temp_mask, out_w, out_h)
7787 else {
7788 return;
7789 };
7790 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, no_aa);
7791 pixmap.stroke_path(skia_path, &paint, stroke, transform, mask_ref);
7792 return;
7793 }
7794
7795 let cov_data = coverage_mask.data();
7796 let stride = out_w as usize;
7797 let px_data = pixmap.data_mut();
7798 let px_stride = out_w as usize * 4;
7799
7800 for y in bbox_y0..bbox_y1 {
7801 for x in bbox_x0..bbox_x1 {
7802 let mi = y * stride + x;
7803 let mut cov = cov_data[mi] as f32 / 255.0;
7804 if let Some(clip) = clip_coverage {
7805 cov *= clip[mi] as f32 / 255.0;
7806 }
7807 if cov <= 0.0 {
7808 continue;
7809 }
7810
7811 let ci = mi * 4;
7812 let pi = y * px_stride + x * 4;
7813 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
7818 op_bg[pi] = px_data[pi];
7819 op_bg[pi + 1] = px_data[pi + 1];
7820 op_bg[pi + 2] = px_data[pi + 2];
7821 op_bg[pi + 3] = px_data[pi + 3];
7822 op_touched[mi] = 1;
7823 }
7824 let cur_c = cmyk_buf[ci] as f64;
7825 let cur_m = cmyk_buf[ci + 1] as f64;
7826 let cur_y = cmyk_buf[ci + 2] as f64;
7827 let cur_k = cmyk_buf[ci + 3] as f64;
7828 let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
7829 let pixmap_has_colour = px_data[pi + 3] > 0
7830 && (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
7831 let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
7839
7840 let is_promoted_gray = params.painted_channels == stet_graphics::device::CMYK_K
7843 && channels == stet_graphics::device::CMYK_K
7844 && params.is_device_cmyk
7845 && src_c == 0.0
7846 && src_m == 0.0
7847 && src_y == 0.0;
7848 let effective_channels = if is_promoted_gray && spot_mask[mi] == 0 {
7849 stet_graphics::device::CMYK_ALL
7850 } else {
7851 channels
7852 };
7853
7854 let new_c = if effective_channels & stet_graphics::device::CMYK_C != 0 {
7855 src_c
7856 } else {
7857 cur_c
7858 };
7859 let new_m = if effective_channels & stet_graphics::device::CMYK_M != 0 {
7860 src_m
7861 } else {
7862 cur_m
7863 };
7864 let new_y = if effective_channels & stet_graphics::device::CMYK_Y != 0 {
7865 src_y
7866 } else {
7867 cur_y
7868 };
7869 let new_k = if effective_channels & stet_graphics::device::CMYK_K != 0 {
7870 src_k
7871 } else {
7872 cur_k
7873 };
7874
7875 if !is_custom_spot {
7876 cmyk_buf[ci] = new_c as f32;
7877 cmyk_buf[ci + 1] = new_m as f32;
7878 cmyk_buf[ci + 2] = new_y as f32;
7879 cmyk_buf[ci + 3] = new_k as f32;
7880 }
7881
7882 let delta = (new_c - cur_c)
7884 .abs()
7885 .max((new_m - cur_m).abs())
7886 .max((new_y - cur_y).abs())
7887 .max((new_k - cur_k).abs());
7888 if delta < 1e-4 && spot_mask[mi] != 0 && pixmap_has_colour && !is_custom_spot {
7889 continue;
7890 }
7891
7892 let (r, g, b) =
7893 if is_promoted_gray && effective_channels == stet_graphics::device::CMYK_ALL {
7894 (params.color.r, params.color.g, params.color.b)
7900 } else if use_multiplicative {
7901 let bg_r = px_data[pi] as f64 / 255.0;
7902 let bg_g = px_data[pi + 1] as f64 / 255.0;
7903 let bg_b = px_data[pi + 2] as f64 / 255.0;
7904 let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
7905 1.0 - src_c
7906 } else {
7907 1.0
7908 };
7909 let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
7910 1.0 - src_m
7911 } else {
7912 1.0
7913 };
7914 let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
7915 1.0 - src_y
7916 } else {
7917 1.0
7918 };
7919 let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
7920 1.0 - src_k
7921 } else {
7922 1.0
7923 };
7924 (
7925 (bg_r * over_r * k_fac).clamp(0.0, 1.0),
7926 (bg_g * over_g * k_fac).clamp(0.0, 1.0),
7927 (bg_b * over_b * k_fac).clamp(0.0, 1.0),
7928 )
7929 } else if let Some(icc_cache) = icc {
7930 icc_cache
7931 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
7932 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
7933 } else {
7934 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
7935 };
7936
7937 let a = (cov * params.alpha as f32).min(1.0);
7938 let (bk_r, bk_g, bk_b, bk_a) = if op_touched[mi] != 0 {
7943 let new_r = (r as f32 * 255.0).clamp(0.0, 255.0);
7944 let new_g = (g as f32 * 255.0).clamp(0.0, 255.0);
7945 let new_b = (b as f32 * 255.0).clamp(0.0, 255.0);
7946 let dr = (op_bg[pi] as f32 - new_r).abs();
7947 let dg = (op_bg[pi + 1] as f32 - new_g).abs();
7948 let db = (op_bg[pi + 2] as f32 - new_b).abs();
7949 if dr.max(dg).max(db) <= 4.0 {
7950 (op_bg[pi], op_bg[pi + 1], op_bg[pi + 2], op_bg[pi + 3])
7951 } else {
7952 (
7953 px_data[pi],
7954 px_data[pi + 1],
7955 px_data[pi + 2],
7956 px_data[pi + 3],
7957 )
7958 }
7959 } else {
7960 (
7961 px_data[pi],
7962 px_data[pi + 1],
7963 px_data[pi + 2],
7964 px_data[pi + 3],
7965 )
7966 };
7967 let dst_a = bk_a as f32 / 255.0;
7968 let one_minus_a = 1.0 - a;
7969 let out_a = a + dst_a * one_minus_a;
7970 if out_a > 0.0 {
7971 px_data[pi] = ((r as f32 * a + (bk_r as f32 / 255.0) * one_minus_a) * 255.0)
7973 .clamp(0.0, 255.0)
7974 .round() as u8;
7975 px_data[pi + 1] = ((g as f32 * a + (bk_g as f32 / 255.0) * one_minus_a) * 255.0)
7976 .clamp(0.0, 255.0)
7977 .round() as u8;
7978 px_data[pi + 2] = ((b as f32 * a + (bk_b as f32 / 255.0) * one_minus_a) * 255.0)
7979 .clamp(0.0, 255.0)
7980 .round() as u8;
7981 px_data[pi + 3] = (out_a * 255.0).round() as u8;
7982 }
7983 }
7984 }
7985}
7986
7987#[allow(clippy::too_many_arguments)]
7992fn update_cmyk_buffer_for_stroke(
7993 cmyk_buf: &mut [f32],
7994 spot_mask: &mut [u8],
7995 path: &PsPath,
7996 params: &StrokeParams,
7997 stroke: &Stroke,
7998 transform: Transform,
7999 out_w: u32,
8000 out_h: u32,
8001 clip_region: &Option<ClipRegion>,
8002 no_aa: bool,
8003 icc: Option<&IccCache>,
8004) {
8005 let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
8009 let has_spot_contrib = (is_custom_spot && params.color.native_cmyk.is_some())
8011 || matches!(
8012 (params.color.native_cmyk, params.color.process_cmyk),
8013 (Some(nat), Some(proc_))
8014 if (nat.0 - proc_.0).abs() > 1e-6
8015 || (nat.1 - proc_.1).abs() > 1e-6
8016 || (nat.2 - proc_.2).abs() > 1e-6
8017 || (nat.3 - proc_.3).abs() > 1e-6
8018 );
8019
8020 let (src_c, src_m, src_y, src_k) = if is_custom_spot {
8021 (0.0, 0.0, 0.0, 0.0)
8022 } else if let Some(c) = params.color.process_cmyk {
8023 c
8024 } else if let Some(c) = params.color.native_cmyk {
8025 c
8026 } else if let Some(cmyk) = icc.and_then(|i| {
8027 i.convert_rgb_to_cmyk_readonly(params.color.r, params.color.g, params.color.b)
8028 }) {
8029 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
8030 } else {
8031 (
8032 (1.0 - params.color.r).clamp(0.0, 1.0),
8033 (1.0 - params.color.g).clamp(0.0, 1.0),
8034 (1.0 - params.color.b).clamp(0.0, 1.0),
8035 0.0,
8036 )
8037 };
8038
8039 let Some(skia_path) = build_skia_path(path) else {
8040 return;
8041 };
8042
8043 let resolution_scale = (transform.sx * transform.sx + transform.sy * transform.sy)
8047 .sqrt()
8048 .max(1.0);
8049 let dashed_op;
8050 let stroke_src = if let Some(ref dash) = stroke.dash {
8051 dashed_op = skia_path.dash(dash, resolution_scale);
8052 match dashed_op.as_ref() {
8053 Some(p) => p,
8054 None => &skia_path,
8055 }
8056 } else {
8057 &skia_path
8058 };
8059 let Some(stroked_user) = stroke_src.stroke(stroke, resolution_scale) else {
8060 return;
8061 };
8062 let Some(stroked) = stroked_user.transform(transform) else {
8063 return;
8064 };
8065
8066 let mut coverage_mask = match Mask::new(out_w, out_h) {
8067 Some(m) => m,
8068 None => return,
8069 };
8070 coverage_mask.fill_path(
8071 &stroked,
8072 SkiaFillRule::Winding,
8073 !no_aa,
8074 Transform::identity(),
8075 );
8076
8077 let cov_data = coverage_mask.data();
8078 let clip_data: Option<&[u8]> = match clip_region {
8079 Some(ClipRegion::Mask(m)) => Some(m.data()),
8080 _ => None,
8081 };
8082
8083 let (mut bx0, mut by0, mut bx1, mut by1) =
8084 path_device_bbox(&stroked, Transform::identity(), out_w, out_h);
8085 if let Some(ClipRegion::Rect(r)) = clip_region {
8086 bx0 = bx0.max(r.x0 as usize);
8087 by0 = by0.max(r.y0 as usize);
8088 bx1 = bx1.min(r.x1 as usize);
8089 by1 = by1.min(r.y1 as usize);
8090 }
8091
8092 let stride = out_w as usize;
8093 for y in by0..by1 {
8094 for x in bx0..bx1 {
8095 let mi = y * stride + x;
8096 let mut cov = cov_data[mi] as f32 / 255.0;
8097 if let Some(clip) = clip_data {
8098 cov *= clip[mi] as f32 / 255.0;
8099 }
8100 if cov > 0.0 {
8101 let ci = mi * 4;
8102 cmyk_buf[ci] = src_c as f32;
8103 cmyk_buf[ci + 1] = src_m as f32;
8104 cmyk_buf[ci + 2] = src_y as f32;
8105 cmyk_buf[ci + 3] = src_k as f32;
8106 if has_spot_contrib {
8107 spot_mask[mi] = 1;
8108 }
8109 }
8110 }
8111 }
8112}
8113
8114#[allow(clippy::too_many_arguments)]
8116fn render_overprint_image(
8117 pixmap: &mut Pixmap,
8118 cmyk_buf: &mut [f32],
8119 op_bg: &mut [u8],
8120 op_touched: &mut [u8],
8121 band_state: &mut BandState,
8122 sample_data: &[u8],
8123 params: &ImageParams,
8124 vp_x: f32,
8125 vp_y: f32,
8126 scale_x: f32,
8127 scale_y: f32,
8128 out_w: u32,
8129 out_h: u32,
8130 icc: Option<&IccCache>,
8131) {
8132 let iw = params.width as usize;
8133 let ih = params.height as usize;
8134 let Some(image_inv) = params.image_matrix.invert() else {
8135 return;
8136 };
8137 let combined = params.ctm.concat(&image_inv);
8138 let Some(inv_combined) = combined.invert() else {
8139 return;
8140 };
8141
8142 let px_data = pixmap.data_mut();
8143 let stride = out_w as usize;
8144 let inv_sx = 1.0 / scale_x as f64;
8145 let inv_sy = 1.0 / scale_y as f64;
8146
8147 let clip_data: Option<&[u8]> = match &band_state.clip_region {
8148 Some(ClipRegion::Mask(m)) => Some(m.data()),
8149 _ => None,
8150 };
8151 let clip_rect = match &band_state.clip_region {
8152 Some(ClipRegion::Rect(r)) => Some(*r),
8153 _ => None,
8154 };
8155
8156 let mask_info = if let ImageColorSpace::Mask { color, polarity } = ¶ms.color_space {
8157 let (src_c, src_m, src_y, src_k) = color.native_cmyk.unwrap_or_else(|| {
8158 let r = color.r;
8159 let g = color.g;
8160 let b = color.b;
8161 (1.0 - r, 1.0 - g, 1.0 - b, 0.0)
8162 });
8163 Some((src_c, src_m, src_y, src_k, *polarity, iw.div_ceil(8)))
8164 } else {
8165 None
8166 };
8167
8168 for by in 0..out_h as usize {
8169 for bx in 0..out_w as usize {
8170 if let Some(ref r) = clip_rect
8171 && ((by as u32) < r.y0
8172 || (by as u32) >= r.y1
8173 || (bx as u32) < r.x0
8174 || (bx as u32) >= r.x1)
8175 {
8176 continue;
8177 }
8178 if let Some(clip) = clip_data {
8179 let ci_clip = by * stride + bx;
8180 if clip[ci_clip] == 0 {
8181 let bh = out_h as usize;
8182 let has_neighbor = (bx > 0 && clip[ci_clip - 1] != 0)
8183 || (bx + 1 < stride && clip[ci_clip + 1] != 0)
8184 || (by > 0 && clip[ci_clip - stride] != 0)
8185 || (by + 1 < bh && clip[ci_clip + stride] != 0)
8186 || (bx > 0 && by > 0 && clip[ci_clip - stride - 1] != 0)
8187 || (bx + 1 < stride && by > 0 && clip[ci_clip - stride + 1] != 0)
8188 || (bx > 0 && by + 1 < bh && clip[ci_clip + stride - 1] != 0)
8189 || (bx + 1 < stride && by + 1 < bh && clip[ci_clip + stride + 1] != 0);
8190 if !has_neighbor {
8191 continue;
8192 }
8193 }
8194 }
8195
8196 let dx = (bx as f64 + 0.5) * inv_sx + vp_x as f64;
8198 let dy = (by as f64 + 0.5) * inv_sy + vp_y as f64;
8199 let ix = inv_combined.a * dx + inv_combined.c * dy + inv_combined.tx;
8200 let iy = inv_combined.b * dx + inv_combined.d * dy + inv_combined.ty;
8201
8202 let col = ix.floor() as i64;
8203 let row = iy.floor() as i64;
8204 if col < 0 || col >= iw as i64 || row < 0 || row >= ih as i64 {
8205 continue;
8206 }
8207 let col = col as usize;
8208 let row = row as usize;
8209
8210 let (src_c, src_m, src_y, src_k) =
8211 if let Some((mc, mm, my, mk, polarity, bytes_per_row)) = mask_info {
8212 let byte_idx = row * bytes_per_row + col / 8;
8213 let bit_offset = 7 - (col % 8);
8214 let bit = if byte_idx < sample_data.len() {
8215 (sample_data[byte_idx] >> bit_offset) & 1
8216 } else {
8217 0
8218 };
8219 let paint = if polarity { bit == 1 } else { bit == 0 };
8220 if !paint {
8221 continue;
8222 }
8223 (mc, mm, my, mk)
8224 } else if let Some(cmyk) =
8225 sample_pixel_cmyk(sample_data, ¶ms.color_space, iw, row, col)
8226 {
8227 cmyk
8228 } else {
8229 continue;
8230 };
8231
8232 let mi = by * stride + bx;
8233 let ci = mi * 4;
8234 let pi = mi * 4;
8235
8236 if image_cs_has_spot_tint_transform(¶ms.color_space) {
8255 let cur_c = cmyk_buf[ci] as f64;
8256 let cur_m = cmyk_buf[ci + 1] as f64;
8257 let cur_y = cmyk_buf[ci + 2] as f64;
8258 let cur_k = cmyk_buf[ci + 3] as f64;
8259 let cur_is_zero = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
8260 let named = params.painted_channels;
8261 let opm1 = params.overprint_mode == 1;
8268 let (new_c, new_m, new_y, new_k) = if cur_is_zero {
8269 (src_c, src_m, src_y, src_k)
8270 } else {
8271 let nc =
8272 if named & stet_graphics::device::CMYK_C != 0 && !(opm1 && src_c == 0.0) {
8273 src_c
8274 } else {
8275 cur_c
8276 };
8277 let nm =
8278 if named & stet_graphics::device::CMYK_M != 0 && !(opm1 && src_m == 0.0) {
8279 src_m
8280 } else {
8281 cur_m
8282 };
8283 let ny =
8284 if named & stet_graphics::device::CMYK_Y != 0 && !(opm1 && src_y == 0.0) {
8285 src_y
8286 } else {
8287 cur_y
8288 };
8289 let nk =
8290 if named & stet_graphics::device::CMYK_K != 0 && !(opm1 && src_k == 0.0) {
8291 src_k
8292 } else {
8293 cur_k
8294 };
8295 (nc, nm, ny, nk)
8296 };
8297 cmyk_buf[ci] = new_c as f32;
8298 cmyk_buf[ci + 1] = new_m as f32;
8299 cmyk_buf[ci + 2] = new_y as f32;
8300 cmyk_buf[ci + 3] = new_k as f32;
8301 let (r, g, b) = if let Some(icc_cache) = icc {
8302 icc_cache
8303 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
8304 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
8305 } else {
8306 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
8307 };
8308 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
8309 op_bg[pi] = px_data[pi];
8310 op_bg[pi + 1] = px_data[pi + 1];
8311 op_bg[pi + 2] = px_data[pi + 2];
8312 op_bg[pi + 3] = px_data[pi + 3];
8313 op_touched[mi] = 1;
8314 }
8315 px_data[pi] = (r * 255.0).round() as u8;
8316 px_data[pi + 1] = (g * 255.0).round() as u8;
8317 px_data[pi + 2] = (b * 255.0).round() as u8;
8318 px_data[pi + 3] = 255;
8319 continue;
8320 }
8321
8322 let mut channels = params.painted_channels;
8323 if channels == 0 {
8326 channels = stet_graphics::device::CMYK_ALL;
8327 }
8328 let is_direct_cmyk = matches!(
8329 ¶ms.color_space,
8330 ImageColorSpace::DeviceCMYK
8331 | ImageColorSpace::ICCBased { n: 4, .. }
8332 | ImageColorSpace::Mask { .. }
8333 );
8334 let is_custom_spot = params.painted_channels == 0
8344 && !is_cmyk_color_space(¶ms.color_space)
8345 && match ¶ms.color_space {
8346 ImageColorSpace::Mask { color, .. } => color.native_cmyk.is_some(),
8347 _ => true,
8348 };
8349 if params.overprint_mode == 1
8350 && channels == stet_graphics::device::CMYK_ALL
8351 && is_direct_cmyk
8352 {
8353 channels = 0;
8354 if src_c != 0.0 {
8355 channels |= stet_graphics::device::CMYK_C;
8356 }
8357 if src_m != 0.0 {
8358 channels |= stet_graphics::device::CMYK_M;
8359 }
8360 if src_y != 0.0 {
8361 channels |= stet_graphics::device::CMYK_Y;
8362 }
8363 if src_k != 0.0 {
8364 channels |= stet_graphics::device::CMYK_K;
8365 }
8366 }
8367
8368 let cur_c = cmyk_buf[ci] as f64;
8369 let cur_m = cmyk_buf[ci + 1] as f64;
8370 let cur_y = cmyk_buf[ci + 2] as f64;
8371 let cur_k = cmyk_buf[ci + 3] as f64;
8372 let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
8373 let pixmap_has_colour = px_data[pi + 3] > 0
8374 && (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
8375 let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
8383
8384 let new_c = if channels & stet_graphics::device::CMYK_C != 0 {
8385 src_c
8386 } else {
8387 cur_c
8388 };
8389 let new_m = if channels & stet_graphics::device::CMYK_M != 0 {
8390 src_m
8391 } else {
8392 cur_m
8393 };
8394 let new_y = if channels & stet_graphics::device::CMYK_Y != 0 {
8395 src_y
8396 } else {
8397 cur_y
8398 };
8399 let new_k = if channels & stet_graphics::device::CMYK_K != 0 {
8400 src_k
8401 } else {
8402 cur_k
8403 };
8404
8405 if !is_custom_spot {
8406 cmyk_buf[ci] = new_c as f32;
8407 cmyk_buf[ci + 1] = new_m as f32;
8408 cmyk_buf[ci + 2] = new_y as f32;
8409 cmyk_buf[ci + 3] = new_k as f32;
8410 }
8411
8412 let (r, g, b) = if use_multiplicative {
8413 let bg_r = px_data[pi] as f64 / 255.0;
8414 let bg_g = px_data[pi + 1] as f64 / 255.0;
8415 let bg_b = px_data[pi + 2] as f64 / 255.0;
8416 let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
8417 1.0 - src_c
8418 } else {
8419 1.0
8420 };
8421 let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
8422 1.0 - src_m
8423 } else {
8424 1.0
8425 };
8426 let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
8427 1.0 - src_y
8428 } else {
8429 1.0
8430 };
8431 let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
8432 1.0 - src_k
8433 } else {
8434 1.0
8435 };
8436 (
8437 (bg_r * over_r * k_fac).clamp(0.0, 1.0),
8438 (bg_g * over_g * k_fac).clamp(0.0, 1.0),
8439 (bg_b * over_b * k_fac).clamp(0.0, 1.0),
8440 )
8441 } else if let Some(icc_cache) = icc {
8442 icc_cache
8443 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
8444 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
8445 } else {
8446 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
8447 };
8448
8449 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
8452 op_bg[pi] = px_data[pi];
8453 op_bg[pi + 1] = px_data[pi + 1];
8454 op_bg[pi + 2] = px_data[pi + 2];
8455 op_bg[pi + 3] = px_data[pi + 3];
8456 op_touched[mi] = 1;
8457 }
8458
8459 px_data[pi] = (r * 255.0).round() as u8;
8460 px_data[pi + 1] = (g * 255.0).round() as u8;
8461 px_data[pi + 2] = (b * 255.0).round() as u8;
8462 px_data[pi + 3] = 255;
8463 }
8464 }
8465}
8466
8467#[allow(clippy::too_many_arguments)]
8477fn update_cmyk_buffer_for_image(
8478 cmyk_buf: &mut [f32],
8479 sample_data: &[u8],
8480 pixmap_rgba: &[u8],
8481 params: &ImageParams,
8482 vp_x: f32,
8483 vp_y: f32,
8484 scale_x: f32,
8485 scale_y: f32,
8486 out_w: u32,
8487 out_h: u32,
8488 clip_region: &Option<ClipRegion>,
8489 icc: Option<&IccCache>,
8490) {
8491 let iw = params.width as usize;
8492 let ih = params.height as usize;
8493 let Some(image_inv) = params.image_matrix.invert() else {
8494 return;
8495 };
8496 let combined = params.ctm.concat(&image_inv);
8497 let Some(inv_combined) = combined.invert() else {
8498 return;
8499 };
8500 let stride = out_w as usize;
8501 let inv_sx = 1.0 / scale_x as f64;
8502 let inv_sy = 1.0 / scale_y as f64;
8503
8504 let mask_info = if let ImageColorSpace::Mask { color, polarity } = ¶ms.color_space {
8505 let Some((c, m, y, k)) = color.native_cmyk else {
8506 return;
8507 };
8508 Some((
8509 c as f32,
8510 m as f32,
8511 y as f32,
8512 k as f32,
8513 *polarity,
8514 iw.div_ceil(8),
8515 ))
8516 } else {
8517 None
8518 };
8519
8520 let clip_data: Option<&[u8]> = match clip_region {
8521 Some(ClipRegion::Mask(m)) => Some(m.data()),
8522 _ => None,
8523 };
8524 let clip_rect = match clip_region {
8525 Some(ClipRegion::Rect(r)) => Some(*r),
8526 _ => None,
8527 };
8528
8529 for by in 0..out_h as usize {
8530 for bx in 0..out_w as usize {
8531 if let Some(ref r) = clip_rect
8532 && ((by as u32) < r.y0
8533 || (by as u32) >= r.y1
8534 || (bx as u32) < r.x0
8535 || (bx as u32) >= r.x1)
8536 {
8537 continue;
8538 }
8539 if let Some(clip) = clip_data
8540 && clip[by * stride + bx] == 0
8541 {
8542 continue;
8543 }
8544
8545 let dx = (bx as f64 + 0.5) * inv_sx + vp_x as f64;
8546 let dy = (by as f64 + 0.5) * inv_sy + vp_y as f64;
8547 let ix = inv_combined.a * dx + inv_combined.c * dy + inv_combined.tx;
8548 let iy = inv_combined.b * dx + inv_combined.d * dy + inv_combined.ty;
8549
8550 let col = ix.floor() as i64;
8551 let row = iy.floor() as i64;
8552 if col < 0 || col >= iw as i64 || row < 0 || row >= ih as i64 {
8553 continue;
8554 }
8555 let col = col as usize;
8556 let row = row as usize;
8557
8558 let ci = (by * stride + bx) * 4;
8559 if let Some((sc, sm, sy, sk, polarity, bytes_per_row)) = mask_info {
8560 let byte_idx = row * bytes_per_row + col / 8;
8561 let bit_offset = 7 - (col % 8);
8562 let bit = if byte_idx < sample_data.len() {
8563 (sample_data[byte_idx] >> bit_offset) & 1
8564 } else {
8565 0
8566 };
8567 let paint = if polarity { bit == 1 } else { bit == 0 };
8568 if paint {
8569 cmyk_buf[ci] = sc;
8570 cmyk_buf[ci + 1] = sm;
8571 cmyk_buf[ci + 2] = sy;
8572 cmyk_buf[ci + 3] = sk;
8573 }
8574 } else if let Some((sc, sm, sy, sk)) =
8575 sample_pixel_cmyk(sample_data, ¶ms.color_space, iw, row, col)
8576 {
8577 cmyk_buf[ci] = sc as f32;
8578 cmyk_buf[ci + 1] = sm as f32;
8579 cmyk_buf[ci + 2] = sy as f32;
8580 cmyk_buf[ci + 3] = sk as f32;
8581 } else if ci + 3 < pixmap_rgba.len() && pixmap_rgba[ci + 3] > 0 {
8582 let r = pixmap_rgba[ci] as f64 / 255.0;
8586 let g = pixmap_rgba[ci + 1] as f64 / 255.0;
8587 let b = pixmap_rgba[ci + 2] as f64 / 255.0;
8588 let cmyk =
8589 if let Some(c) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(r, g, b)) {
8590 c
8591 } else {
8592 [
8593 (1.0 - r).clamp(0.0, 1.0),
8594 (1.0 - g).clamp(0.0, 1.0),
8595 (1.0 - b).clamp(0.0, 1.0),
8596 0.0,
8597 ]
8598 };
8599 cmyk_buf[ci] = cmyk[0] as f32;
8600 cmyk_buf[ci + 1] = cmyk[1] as f32;
8601 cmyk_buf[ci + 2] = cmyk[2] as f32;
8602 cmyk_buf[ci + 3] = cmyk[3] as f32;
8603 }
8604 }
8605 }
8606}
8607fn image_supports_overprint(cs: &ImageColorSpace) -> bool {
8611 match cs {
8612 ImageColorSpace::Mask { .. } => true,
8613 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. } => true,
8614 ImageColorSpace::Separation { alt_space, .. }
8615 | ImageColorSpace::DeviceN { alt_space, .. } => {
8616 matches!(
8617 alt_space.as_ref(),
8618 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8619 )
8620 }
8621 ImageColorSpace::Indexed { base, .. } => image_supports_overprint(base),
8622 _ => false,
8623 }
8624}
8625
8626fn is_cmyk_color_space(cs: &ImageColorSpace) -> bool {
8628 match cs {
8629 ImageColorSpace::DeviceCMYK => true,
8630 ImageColorSpace::ICCBased { n: 4, .. } => true,
8631 ImageColorSpace::Indexed { base, .. } => is_cmyk_color_space(base),
8632 _ => false,
8633 }
8634}
8635
8636fn image_cs_has_spot_tint_transform(cs: &ImageColorSpace) -> bool {
8644 use stet_graphics::device::cmyk_channel_for_name;
8645 match cs {
8646 ImageColorSpace::Separation {
8647 name, alt_space, ..
8648 } => {
8649 cmyk_channel_for_name(name) == 0
8650 && matches!(
8651 alt_space.as_ref(),
8652 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8653 )
8654 }
8655 ImageColorSpace::DeviceN {
8656 names, alt_space, ..
8657 } => {
8658 matches!(
8659 alt_space.as_ref(),
8660 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8661 ) && names.iter().any(|n| cmyk_channel_for_name(n) == 0)
8662 }
8663 ImageColorSpace::Indexed { base, .. } => image_cs_has_spot_tint_transform(base),
8664 _ => false,
8665 }
8666}
8667
8668fn sample_pixel_cmyk(
8672 sample_data: &[u8],
8673 cs: &ImageColorSpace,
8674 iw: usize,
8675 row: usize,
8676 col: usize,
8677) -> Option<(f64, f64, f64, f64)> {
8678 match cs {
8679 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. } => {
8680 let si = (row * iw + col) * 4;
8681 if si + 3 < sample_data.len() {
8682 Some((
8683 sample_data[si] as f64 / 255.0,
8684 sample_data[si + 1] as f64 / 255.0,
8685 sample_data[si + 2] as f64 / 255.0,
8686 sample_data[si + 3] as f64 / 255.0,
8687 ))
8688 } else {
8689 None
8690 }
8691 }
8692 ImageColorSpace::Separation {
8693 alt_space,
8694 tint_table,
8695 ..
8696 } => {
8697 if !matches!(
8698 alt_space.as_ref(),
8699 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8700 ) {
8701 return None;
8702 }
8703 let si = row * iw + col;
8704 if si >= sample_data.len() {
8705 return None;
8706 }
8707 let tint = sample_data[si] as f32 / 255.0;
8708 let mut alt = [0.0f32; 4];
8709 tint_table.lookup_1d(tint, &mut alt);
8710 Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64))
8711 }
8712 ImageColorSpace::DeviceN {
8713 alt_space,
8714 tint_table,
8715 ..
8716 } => {
8717 if !matches!(
8718 alt_space.as_ref(),
8719 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8720 ) {
8721 return None;
8722 }
8723 let ni = tint_table.num_inputs as usize;
8724 let si = (row * iw + col) * ni;
8725 if si + ni > sample_data.len() {
8726 return None;
8727 }
8728 let mut inputs = vec![0.0f32; ni];
8729 for (c, inp) in inputs.iter_mut().enumerate() {
8730 *inp = sample_data[si + c] as f32 / 255.0;
8731 }
8732 let mut alt = [0.0f32; 4];
8733 tint_table.lookup_nd(&inputs, &mut alt);
8734 Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64))
8735 }
8736 ImageColorSpace::Indexed {
8737 base,
8738 hival,
8739 lookup,
8740 } => {
8741 let pi = row * iw + col;
8742 if pi >= sample_data.len() {
8743 return None;
8744 }
8745 let idx = sample_data[pi] as usize;
8746 let idx = idx.min(*hival as usize);
8747 let base_ncomp = base.num_components() as usize;
8748 let li = idx * base_ncomp;
8749 if is_cmyk_color_space(base) && base_ncomp == 4 && li + 3 < lookup.len() {
8751 return Some((
8752 lookup[li] as f64 / 255.0,
8753 lookup[li + 1] as f64 / 255.0,
8754 lookup[li + 2] as f64 / 255.0,
8755 lookup[li + 3] as f64 / 255.0,
8756 ));
8757 }
8758 if li + base_ncomp <= lookup.len() {
8760 match base.as_ref() {
8761 ImageColorSpace::Separation {
8762 alt_space,
8763 tint_table,
8764 ..
8765 } if matches!(
8766 alt_space.as_ref(),
8767 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8768 ) =>
8769 {
8770 let tint = lookup[li] as f32 / 255.0;
8771 let mut alt = [0.0f32; 4];
8772 tint_table.lookup_1d(tint, &mut alt);
8773 return Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64));
8774 }
8775 ImageColorSpace::DeviceN {
8776 alt_space,
8777 tint_table,
8778 ..
8779 } if matches!(
8780 alt_space.as_ref(),
8781 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8782 ) =>
8783 {
8784 let ni = tint_table.num_inputs as usize;
8785 let mut inputs = vec![0.0f32; ni];
8786 for (c, inp) in inputs.iter_mut().enumerate() {
8787 if c < base_ncomp {
8788 *inp = lookup[li + c] as f32 / 255.0;
8789 }
8790 }
8791 let mut alt = [0.0f32; 4];
8792 tint_table.lookup_nd(&inputs, &mut alt);
8793 return Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64));
8794 }
8795 _ => {}
8796 }
8797 }
8798 None
8799 }
8800 _ => None,
8801 }
8802}
8803#[allow(clippy::too_many_arguments)]
8809fn render_banded_to_sink(
8810 page_w: u32,
8811 page_h: u32,
8812 band_h: u32,
8813 dpi: f64,
8814 list: &DisplayList,
8815 sink: &mut dyn stet_graphics::device::PageSink,
8816 icc_cache: &IccCache,
8817 no_aa: bool,
8818) -> Result<(), String> {
8819 let bboxes = precompute_bboxes(list, dpi);
8821
8822 let epochs = build_clip_epochs(list, &bboxes);
8826
8827 let clip_seen = precompute_clip_seen(list);
8829
8830 use stet_graphics::display_list::GroupColorSpace;
8835 let needs_cmyk_buffer = has_overprint_elements(list)
8836 || list.page_group_color_space() == GroupColorSpace::DeviceCMYK
8837 || has_cmyk_group(list);
8838
8839 let preprocessed_images = preprocess_images_for_bands(list, Some(icc_cache));
8841
8842 const BAND_OVERLAP: u32 = 6;
8846
8847 let render_h = band_h + 2 * BAND_OVERLAP;
8848
8849 sink.begin_page(page_w, page_h)?;
8851
8852 let num_bands = page_h.div_ceil(band_h);
8853 let elements = list.elements();
8854 let row_bytes = page_w as usize * 4;
8855 let icc_ref = Some(icc_cache);
8856
8857 let render_band = |band_idx: u32| -> Vec<u8> {
8859 let y_start = band_idx * band_h;
8860 let actual_h = (page_h - y_start).min(band_h);
8861
8862 let render_y_start = y_start.saturating_sub(BAND_OVERLAP);
8863 let render_y_end_f = ((y_start + actual_h + BAND_OVERLAP).min(page_h)) as f64;
8864 let band_offset = y_start - render_y_start;
8865
8866 let mut band_pixmap = Pixmap::new(page_w, render_h).expect("Failed to create band pixmap");
8867 band_pixmap.as_mut().data_mut().fill(0x00);
8869
8870 let cmyk_buf = if needs_cmyk_buffer {
8871 Some(vec![0.0f32; page_w as usize * render_h as usize * 4])
8873 } else {
8874 None
8875 };
8876
8877 let mut band_state = BandState {
8878 clip_region: None,
8879 spare_mask: None,
8880 clip_mask_cache: HashMap::new(),
8881 clip_mask_seen: clip_seen.clone(),
8882 mask_pool: Vec::new(),
8883 cmyk_buffer: cmyk_buf,
8884 op_bg_snapshot: None,
8885 op_touched: None,
8886 spot_mask: None,
8887 };
8888
8889 for epoch in &epochs {
8891 if !epoch.has_erase_page {
8892 match epoch.paint_bbox {
8893 Some(ref pb)
8894 if pb.y_max <= render_y_start as f64 || pb.y_min >= render_y_end_f =>
8895 {
8896 continue;
8897 }
8898 None => continue,
8899 _ => {}
8900 }
8901 }
8902
8903 for i in epoch.start_idx..epoch.end_idx {
8904 let force_process = matches!(
8910 &elements[i],
8911 DisplayElement::OcgGroup { elements: inner, .. }
8912 if contains_clip_op(inner)
8913 );
8914 if !force_process
8915 && let Some(ref bbox) = bboxes[i]
8916 && (bbox.y_max <= render_y_start as f64 || bbox.y_min >= render_y_end_f)
8917 {
8918 continue;
8919 }
8920 let ctx = RenderContext {
8921 vp_x: 0.0,
8922 vp_y: render_y_start as f32,
8923 scale_x: 1.0,
8924 scale_y: 1.0,
8925 out_w: page_w,
8926 out_h: render_h,
8927 effective_dpi: dpi,
8928 icc: icc_ref,
8929 image_cache: None,
8930 preprocessed: Some(&preprocessed_images),
8931 elem_idx: i,
8932 no_aa,
8933 opm_zero_transparent: false,
8934 knockout_painter_pass: KnockoutPainterPass::None,
8935 parent_group_isolated: false,
8936 alpha_extraction_pass: false,
8937 };
8938 render_element(&mut band_pixmap, &mut band_state, &elements[i], &ctx);
8939 }
8940 }
8941
8942 composite_onto_white(band_pixmap.data_mut());
8944
8945 let start_byte = band_offset as usize * row_bytes;
8947 let total_bytes = actual_h as usize * row_bytes;
8948 band_pixmap.data()[start_byte..start_byte + total_bytes].to_vec()
8949 };
8950
8951 #[cfg(feature = "parallel")]
8953 {
8954 let chunk_size = rayon::current_num_threads().max(1);
8959
8960 for chunk_start in (0..num_bands).step_by(chunk_size) {
8961 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
8962
8963 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
8964 .into_par_iter()
8965 .map(&render_band)
8966 .collect();
8967
8968 for (i, band_data) in rendered.iter().enumerate() {
8969 let band_idx = chunk_start + i as u32;
8970 let y_start = band_idx * band_h;
8971 let actual_h = (page_h - y_start).min(band_h);
8972 sink.write_rows(band_data, actual_h)?;
8973 }
8974 }
8975 }
8976 #[cfg(not(feature = "parallel"))]
8977 {
8978 for band_idx in 0..num_bands {
8980 let band_data = render_band(band_idx);
8981 let y_start = band_idx * band_h;
8982 let actual_h = (page_h - y_start).min(band_h);
8983 sink.write_rows(&band_data, actual_h)?;
8984 }
8985 }
8986
8987 sink.end_page()
8988}
8989
8990#[derive(Clone, Copy)]
8992struct BBox2D {
8993 x_min: f64,
8994 y_min: f64,
8995 x_max: f64,
8996 y_max: f64,
8997}
8998
8999fn precompute_full_bboxes(list: &DisplayList, dpi: f64) -> Vec<Option<BBox2D>> {
9001 list.elements()
9002 .iter()
9003 .map(|elem| match elem {
9004 DisplayElement::Fill { path, params } => fill_device_full_bbox(path, ¶ms.ctm),
9005 DisplayElement::Stroke { path, params } => {
9006 path_full_bbox(path).map(|mut bbox| {
9007 let effective_lw = params.line_width.max(hairline_min_width(¶ms.ctm, dpi));
9009 let expand = effective_lw * params.miter_limit * 0.5;
9010 let m = ¶ms.ctm;
9011 let is_identity = m.a == 1.0
9012 && m.b == 0.0
9013 && m.c == 0.0
9014 && m.d == 1.0
9015 && m.tx == 0.0
9016 && m.ty == 0.0;
9017 if is_identity {
9018 bbox.x_min -= expand;
9019 bbox.x_max += expand;
9020 bbox.y_min -= expand;
9021 bbox.y_max += expand;
9022 } else {
9023 let col_x_len = (m.a * m.a + m.b * m.b).sqrt().max(1.0);
9026 let col_y_len = (m.c * m.c + m.d * m.d).sqrt().max(1.0);
9027 let expand_x = effective_lw * col_x_len * params.miter_limit * 0.5;
9028 let expand_y = effective_lw * col_y_len * params.miter_limit * 0.5;
9029 bbox.x_min -= expand_x;
9030 bbox.x_max += expand_x;
9031 bbox.y_min -= expand_y;
9032 bbox.y_max += expand_y;
9033 let corners = [
9035 (
9036 m.a * bbox.x_min + m.c * bbox.y_min + m.tx,
9037 m.b * bbox.x_min + m.d * bbox.y_min + m.ty,
9038 ),
9039 (
9040 m.a * bbox.x_max + m.c * bbox.y_min + m.tx,
9041 m.b * bbox.x_max + m.d * bbox.y_min + m.ty,
9042 ),
9043 (
9044 m.a * bbox.x_min + m.c * bbox.y_max + m.tx,
9045 m.b * bbox.x_min + m.d * bbox.y_max + m.ty,
9046 ),
9047 (
9048 m.a * bbox.x_max + m.c * bbox.y_max + m.tx,
9049 m.b * bbox.x_max + m.d * bbox.y_max + m.ty,
9050 ),
9051 ];
9052 bbox.x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
9053 bbox.x_max = corners
9054 .iter()
9055 .map(|c| c.0)
9056 .fold(f64::NEG_INFINITY, f64::max);
9057 bbox.y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
9058 bbox.y_max = corners
9059 .iter()
9060 .map(|c| c.1)
9061 .fold(f64::NEG_INFINITY, f64::max);
9062 }
9063 bbox
9064 })
9065 }
9066 DisplayElement::Image { params, .. } => image_full_bbox(params),
9067 DisplayElement::AxialShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9068 DisplayElement::RadialShading { params } => {
9069 shading_full_bbox(¶ms.bbox, ¶ms.ctm)
9070 }
9071 DisplayElement::MeshShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9072 DisplayElement::PatchShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9073 DisplayElement::PatternFill { params } => pattern_fill_full_bbox(params),
9074 DisplayElement::Group { params, .. } => Some(BBox2D {
9075 x_min: params.bbox[0],
9076 y_min: params.bbox[1],
9077 x_max: params.bbox[2],
9078 y_max: params.bbox[3],
9079 }),
9080 DisplayElement::SoftMasked { params, .. } => Some(BBox2D {
9081 x_min: params.bbox[0],
9082 y_min: params.bbox[1],
9083 x_max: params.bbox[2],
9084 y_max: params.bbox[3],
9085 }),
9086 DisplayElement::OcgGroup {
9087 elements,
9088 default_visible,
9089 ..
9090 } => {
9091 if !*default_visible && !contains_clip_op(elements) {
9096 return None;
9097 }
9098 let child_bboxes = precompute_full_bboxes(elements, dpi);
9099 let mut x_min = f64::INFINITY;
9100 let mut y_min = f64::INFINITY;
9101 let mut x_max = f64::NEG_INFINITY;
9102 let mut y_max = f64::NEG_INFINITY;
9103 for cb in child_bboxes.into_iter().flatten() {
9104 x_min = x_min.min(cb.x_min);
9105 y_min = y_min.min(cb.y_min);
9106 x_max = x_max.max(cb.x_max);
9107 y_max = y_max.max(cb.y_max);
9108 }
9109 if x_min <= x_max && y_min <= y_max {
9110 Some(BBox2D {
9111 x_min,
9112 y_min,
9113 x_max,
9114 y_max,
9115 })
9116 } else {
9117 None
9118 }
9119 }
9120 _ => None, })
9122 .collect()
9123}
9124
9125fn clip_path_bbox(path: &PsPath, params: &ClipParams) -> Option<BBox2D> {
9134 let mut bbox = path_full_bbox(path)?;
9135 let ctm = ¶ms.ctm;
9136 let is_identity = ctm.a == 1.0
9137 && ctm.b == 0.0
9138 && ctm.c == 0.0
9139 && ctm.d == 1.0
9140 && ctm.tx == 0.0
9141 && ctm.ty == 0.0;
9142 if !is_identity {
9143 let corners = [
9144 ctm.transform_point(bbox.x_min, bbox.y_min),
9145 ctm.transform_point(bbox.x_max, bbox.y_min),
9146 ctm.transform_point(bbox.x_min, bbox.y_max),
9147 ctm.transform_point(bbox.x_max, bbox.y_max),
9148 ];
9149 bbox.x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
9150 bbox.x_max = corners
9151 .iter()
9152 .map(|c| c.0)
9153 .fold(f64::NEG_INFINITY, f64::max);
9154 bbox.y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
9155 bbox.y_max = corners
9156 .iter()
9157 .map(|c| c.1)
9158 .fold(f64::NEG_INFINITY, f64::max);
9159 }
9160 if let Some(sp) = ¶ms.stroke_params {
9161 let scale = (ctm.a * ctm.a + ctm.b * ctm.b)
9162 .sqrt()
9163 .max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt())
9164 .max(1.0);
9165 let expand = sp.line_width * 0.5 * scale;
9166 bbox.x_min -= expand;
9167 bbox.x_max += expand;
9168 bbox.y_min -= expand;
9169 bbox.y_max += expand;
9170 }
9171 Some(bbox)
9172}
9173
9174fn intersect_bbox(a: &BBox2D, b: &BBox2D) -> Option<BBox2D> {
9176 let x_min = a.x_min.max(b.x_min);
9177 let y_min = a.y_min.max(b.y_min);
9178 let x_max = a.x_max.min(b.x_max);
9179 let y_max = a.y_max.min(b.y_max);
9180 if x_min < x_max && y_min < y_max {
9181 Some(BBox2D {
9182 x_min,
9183 y_min,
9184 x_max,
9185 y_max,
9186 })
9187 } else {
9188 None
9189 }
9190}
9191
9192fn compute_paint_bounds(list: &DisplayList, _dpi: f64) -> Option<BBox2D> {
9213 let mut clip_stack: Vec<BBox2D> = Vec::new();
9217 let mut union: Option<BBox2D> = None;
9218
9219 let push_paint = |union: &mut Option<BBox2D>, clip_stack: &[BBox2D], bbox: BBox2D| {
9220 let visible = match clip_stack.last() {
9224 Some(clip) => match intersect_bbox(clip, &bbox) {
9225 Some(b) => b,
9226 None => return,
9227 },
9228 None => bbox,
9229 };
9230 *union = Some(match union.take() {
9231 None => visible,
9232 Some(u) => BBox2D {
9233 x_min: u.x_min.min(visible.x_min),
9234 y_min: u.y_min.min(visible.y_min),
9235 x_max: u.x_max.max(visible.x_max),
9236 y_max: u.y_max.max(visible.y_max),
9237 },
9238 });
9239 };
9240
9241 for elem in list.elements() {
9242 match elem {
9243 DisplayElement::Clip { path, params } => {
9244 if let Some(cb) = clip_path_bbox(path, params) {
9245 let new_top = match clip_stack.last() {
9246 Some(prev) => match intersect_bbox(prev, &cb) {
9247 Some(b) => b,
9248 None => BBox2D {
9252 x_min: 0.0,
9253 y_min: 0.0,
9254 x_max: 0.0,
9255 y_max: 0.0,
9256 },
9257 },
9258 None => cb,
9259 };
9260 clip_stack.push(new_top);
9261 }
9262 }
9263 DisplayElement::InitClip | DisplayElement::ErasePage => {
9264 clip_stack.clear();
9265 }
9266 DisplayElement::Fill { path, .. } => {
9267 if let Some(b) = path_full_bbox(path) {
9268 push_paint(&mut union, &clip_stack, b);
9269 }
9270 }
9271 DisplayElement::Stroke { path, params } => {
9272 if let Some(mut b) = path_full_bbox(path) {
9273 let expand = params.line_width * params.miter_limit * 0.5;
9274 b.x_min -= expand;
9275 b.x_max += expand;
9276 b.y_min -= expand;
9277 b.y_max += expand;
9278 push_paint(&mut union, &clip_stack, b);
9279 }
9280 }
9281 DisplayElement::Image { params, .. } => {
9282 if let Some(b) = image_full_bbox(params) {
9283 push_paint(&mut union, &clip_stack, b);
9284 }
9285 }
9286 DisplayElement::AxialShading { params } => {
9287 let b = match ¶ms.bbox {
9288 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9289 None => clip_stack.last().copied(),
9290 };
9291 if let Some(b) = b {
9292 push_paint(&mut union, &clip_stack, b);
9293 }
9294 }
9295 DisplayElement::RadialShading { params } => {
9296 let b = match ¶ms.bbox {
9297 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9298 None => clip_stack.last().copied(),
9299 };
9300 if let Some(b) = b {
9301 push_paint(&mut union, &clip_stack, b);
9302 }
9303 }
9304 DisplayElement::MeshShading { params } => {
9305 let b = match ¶ms.bbox {
9306 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9307 None => clip_stack.last().copied(),
9308 };
9309 if let Some(b) = b {
9310 push_paint(&mut union, &clip_stack, b);
9311 }
9312 }
9313 DisplayElement::PatchShading { params } => {
9314 let b = match ¶ms.bbox {
9315 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9316 None => clip_stack.last().copied(),
9317 };
9318 if let Some(b) = b {
9319 push_paint(&mut union, &clip_stack, b);
9320 }
9321 }
9322 DisplayElement::PatternFill { params } => {
9323 if let Some(b) = pattern_fill_full_bbox(params) {
9324 push_paint(&mut union, &clip_stack, b);
9325 }
9326 }
9327 DisplayElement::Group { params, .. } => {
9328 push_paint(
9329 &mut union,
9330 &clip_stack,
9331 BBox2D {
9332 x_min: params.bbox[0],
9333 y_min: params.bbox[1],
9334 x_max: params.bbox[2],
9335 y_max: params.bbox[3],
9336 },
9337 );
9338 }
9339 DisplayElement::SoftMasked { params, .. } => {
9340 push_paint(
9341 &mut union,
9342 &clip_stack,
9343 BBox2D {
9344 x_min: params.bbox[0],
9345 y_min: params.bbox[1],
9346 x_max: params.bbox[2],
9347 y_max: params.bbox[3],
9348 },
9349 );
9350 }
9351 DisplayElement::Text { .. } => {} DisplayElement::OcgGroup { .. } => {
9353 }
9358 }
9359 }
9360 union
9361}
9362
9363fn fill_device_full_bbox(path: &PsPath, ctm: &Matrix) -> Option<BBox2D> {
9368 let bbox = path_full_bbox(path)?;
9369 let is_identity = ctm.a == 1.0
9370 && ctm.b == 0.0
9371 && ctm.c == 0.0
9372 && ctm.d == 1.0
9373 && ctm.tx == 0.0
9374 && ctm.ty == 0.0;
9375 if is_identity {
9376 return Some(bbox);
9377 }
9378 let corners = [
9379 (bbox.x_min, bbox.y_min),
9380 (bbox.x_max, bbox.y_min),
9381 (bbox.x_min, bbox.y_max),
9382 (bbox.x_max, bbox.y_max),
9383 ];
9384 let mut x_min = f64::INFINITY;
9385 let mut x_max = f64::NEG_INFINITY;
9386 let mut y_min = f64::INFINITY;
9387 let mut y_max = f64::NEG_INFINITY;
9388 for (x, y) in &corners {
9389 let dx = ctm.a * x + ctm.c * y + ctm.tx;
9390 let dy = ctm.b * x + ctm.d * y + ctm.ty;
9391 x_min = x_min.min(dx);
9392 x_max = x_max.max(dx);
9393 y_min = y_min.min(dy);
9394 y_max = y_max.max(dy);
9395 }
9396 Some(BBox2D {
9397 x_min,
9398 y_min,
9399 x_max,
9400 y_max,
9401 })
9402}
9403
9404fn path_full_bbox(path: &PsPath) -> Option<BBox2D> {
9405 let mut x_min = f64::INFINITY;
9406 let mut x_max = f64::NEG_INFINITY;
9407 let mut y_min = f64::INFINITY;
9408 let mut y_max = f64::NEG_INFINITY;
9409 for seg in &path.segments {
9410 match seg {
9411 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => {
9412 x_min = x_min.min(*x);
9413 x_max = x_max.max(*x);
9414 y_min = y_min.min(*y);
9415 y_max = y_max.max(*y);
9416 }
9417 PathSegment::CurveTo {
9418 x1,
9419 y1,
9420 x2,
9421 y2,
9422 x3,
9423 y3,
9424 } => {
9425 x_min = x_min.min(*x1).min(*x2).min(*x3);
9426 x_max = x_max.max(*x1).max(*x2).max(*x3);
9427 y_min = y_min.min(*y1).min(*y2).min(*y3);
9428 y_max = y_max.max(*y1).max(*y2).max(*y3);
9429 }
9430 PathSegment::ClosePath => {}
9431 }
9432 }
9433 if x_min <= x_max {
9434 Some(BBox2D {
9435 x_min,
9436 y_min,
9437 x_max,
9438 y_max,
9439 })
9440 } else {
9441 None
9442 }
9443}
9444
9445fn pattern_fill_full_bbox(params: &stet_graphics::device::PatternFillParams) -> Option<BBox2D> {
9450 if let Some(ref sp) = params.stroke_params {
9451 let bbox = path_full_bbox(¶ms.path)?;
9452 let ctm = &sp.ctm;
9453 let corners = [
9454 ctm.transform_point(bbox.x_min, bbox.y_min),
9455 ctm.transform_point(bbox.x_max, bbox.y_min),
9456 ctm.transform_point(bbox.x_min, bbox.y_max),
9457 ctm.transform_point(bbox.x_max, bbox.y_max),
9458 ];
9459 let mut dev_bbox = BBox2D {
9460 x_min: f64::INFINITY,
9461 y_min: f64::INFINITY,
9462 x_max: f64::NEG_INFINITY,
9463 y_max: f64::NEG_INFINITY,
9464 };
9465 for (x, y) in &corners {
9466 dev_bbox.x_min = dev_bbox.x_min.min(*x);
9467 dev_bbox.y_min = dev_bbox.y_min.min(*y);
9468 dev_bbox.x_max = dev_bbox.x_max.max(*x);
9469 dev_bbox.y_max = dev_bbox.y_max.max(*y);
9470 }
9471 let half_w = sp.line_width
9472 * 0.5
9473 * (ctm.a * ctm.a + ctm.b * ctm.b)
9474 .sqrt()
9475 .max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt());
9476 dev_bbox.x_min -= half_w;
9477 dev_bbox.y_min -= half_w;
9478 dev_bbox.x_max += half_w;
9479 dev_bbox.y_max += half_w;
9480 Some(dev_bbox)
9481 } else {
9482 path_full_bbox(¶ms.path)
9483 }
9484}
9485
9486fn pattern_fill_y_bbox(params: &stet_graphics::device::PatternFillParams) -> Option<YBBox> {
9488 let bbox = pattern_fill_full_bbox(params)?;
9489 Some(YBBox {
9490 y_min: bbox.y_min,
9491 y_max: bbox.y_max,
9492 })
9493}
9494
9495fn image_full_bbox(params: &ImageParams) -> Option<BBox2D> {
9497 let m = ¶ms.ctm;
9498 let im = ¶ms.image_matrix;
9499 let im_inv = im.invert()?;
9500 let combined = m.concat(&im_inv);
9501 let w = params.width as f64;
9503 let h = params.height as f64;
9504 let corners = [
9505 combined.transform_point(0.0, 0.0),
9506 combined.transform_point(w, 0.0),
9507 combined.transform_point(0.0, h),
9508 combined.transform_point(w, h),
9509 ];
9510 let mut x_min = f64::INFINITY;
9511 let mut x_max = f64::NEG_INFINITY;
9512 let mut y_min = f64::INFINITY;
9513 let mut y_max = f64::NEG_INFINITY;
9514 for (x, y) in &corners {
9515 x_min = x_min.min(*x);
9516 x_max = x_max.max(*x);
9517 y_min = y_min.min(*y);
9518 y_max = y_max.max(*y);
9519 }
9520 Some(BBox2D {
9521 x_min,
9522 y_min,
9523 x_max,
9524 y_max,
9525 })
9526}
9527
9528fn shading_full_bbox(bbox: &Option<[f64; 4]>, ctm: &Matrix) -> Option<BBox2D> {
9530 if let Some(bbox) = bbox {
9531 let corners = [
9532 ctm.transform_point(bbox[0], bbox[1]),
9533 ctm.transform_point(bbox[2], bbox[1]),
9534 ctm.transform_point(bbox[0], bbox[3]),
9535 ctm.transform_point(bbox[2], bbox[3]),
9536 ];
9537 let mut x_min = f64::INFINITY;
9538 let mut x_max = f64::NEG_INFINITY;
9539 let mut y_min = f64::INFINITY;
9540 let mut y_max = f64::NEG_INFINITY;
9541 for (x, y) in &corners {
9542 x_min = x_min.min(*x);
9543 x_max = x_max.max(*x);
9544 y_min = y_min.min(*y);
9545 y_max = y_max.max(*y);
9546 }
9547 Some(BBox2D {
9548 x_min,
9549 y_min,
9550 x_max,
9551 y_max,
9552 })
9553 } else {
9554 Some(BBox2D {
9555 x_min: 0.0,
9556 y_min: 0.0,
9557 x_max: 1e9,
9558 y_max: 1e9,
9559 })
9560 }
9561}
9562
9563fn build_viewport_epochs(list: &DisplayList, bboxes: &[Option<BBox2D>]) -> Vec<ViewportEpoch> {
9565 let elements = list.elements();
9566 let mut epochs = Vec::new();
9567 let mut epoch_start = 0;
9568 let mut x_min = f64::INFINITY;
9569 let mut x_max = f64::NEG_INFINITY;
9570 let mut y_min = f64::INFINITY;
9571 let mut y_max = f64::NEG_INFINITY;
9572 let mut has_erase = false;
9573
9574 for (i, element) in elements.iter().enumerate() {
9575 if matches!(element, DisplayElement::InitClip) && i > epoch_start {
9576 epochs.push(ViewportEpoch {
9577 start_idx: epoch_start,
9578 end_idx: i,
9579 paint_bbox: if x_min <= x_max {
9580 Some(BBox2D {
9581 x_min,
9582 y_min,
9583 x_max,
9584 y_max,
9585 })
9586 } else {
9587 None
9588 },
9589 has_erase_page: has_erase,
9590 });
9591 epoch_start = i;
9592 x_min = f64::INFINITY;
9593 x_max = f64::NEG_INFINITY;
9594 y_min = f64::INFINITY;
9595 y_max = f64::NEG_INFINITY;
9596 has_erase = false;
9597 }
9598 if matches!(element, DisplayElement::ErasePage) {
9599 has_erase = true;
9600 }
9601 if let Some(ref bbox) = bboxes[i] {
9602 x_min = x_min.min(bbox.x_min);
9603 x_max = x_max.max(bbox.x_max);
9604 y_min = y_min.min(bbox.y_min);
9605 y_max = y_max.max(bbox.y_max);
9606 }
9607 }
9608 if epoch_start < elements.len() {
9609 epochs.push(ViewportEpoch {
9610 start_idx: epoch_start,
9611 end_idx: elements.len(),
9612 paint_bbox: if x_min <= x_max {
9613 Some(BBox2D {
9614 x_min,
9615 y_min,
9616 x_max,
9617 y_max,
9618 })
9619 } else {
9620 None
9621 },
9622 has_erase_page: has_erase,
9623 });
9624 }
9625 epochs
9626}
9627
9628struct ViewportEpoch {
9630 start_idx: usize,
9631 end_idx: usize,
9632 paint_bbox: Option<BBox2D>,
9633 has_erase_page: bool,
9634}
9635
9636pub struct PreparedDisplayList {
9642 bboxes: Vec<Option<BBox2D>>,
9643 epochs: Vec<ViewportEpoch>,
9644 clip_seen: HashSet<u64>,
9645}
9646
9647pub fn prepare_display_list(list: &DisplayList) -> PreparedDisplayList {
9652 let bboxes = precompute_full_bboxes(list, 72.0);
9653 let epochs = build_viewport_epochs(list, &bboxes);
9654 let clip_seen = precompute_clip_seen(list);
9655 PreparedDisplayList {
9656 bboxes,
9657 epochs,
9658 clip_seen,
9659 }
9660}
9661
9662struct PreprocessedImage {
9667 data: Vec<u8>,
9669 width: u32,
9671 height: u32,
9672 adj_sx: f32,
9676 adj_ky: f32,
9677 adj_kx: f32,
9678 adj_sy: f32,
9679 quality: stet_tiny_skia::FilterQuality,
9681}
9682
9683pub struct ImageCache {
9689 entries: Vec<Option<Vec<u8>>>,
9691}
9692
9693impl ImageCache {
9694 pub fn build(list: &DisplayList, icc: Option<&IccCache>) -> Self {
9696 let entries = list
9697 .elements()
9698 .iter()
9699 .map(|elem| {
9700 if let DisplayElement::Image {
9701 sample_data,
9702 params,
9703 } = elem
9704 {
9705 if params.width == 0 || params.height == 0 {
9706 return None;
9707 }
9708 let mut rgba = samples_to_rgba(sample_data, params, icc, false);
9709 if params.mask_color.is_some() {
9710 apply_mask_color_rgba(&mut rgba, sample_data, params);
9711 }
9712 Some(rgba)
9713 } else {
9714 None
9715 }
9716 })
9717 .collect();
9718 Self { entries }
9719 }
9720
9721 pub fn get(&self, index: usize) -> Option<&[u8]> {
9723 self.entries.get(index).and_then(|e| e.as_deref())
9724 }
9725}
9726
9727fn preprocess_images_for_bands(
9732 list: &DisplayList,
9733 icc: Option<&IccCache>,
9734) -> Vec<Option<PreprocessedImage>> {
9735 list.elements()
9736 .iter()
9737 .map(|elem| {
9738 let DisplayElement::Image {
9739 sample_data,
9740 params,
9741 } = elem
9742 else {
9743 return None;
9744 };
9745 let iw = params.width;
9746 let ih = params.height;
9747 if iw == 0 || ih == 0 {
9748 return None;
9749 }
9750 if params.overprint {
9752 return None;
9753 }
9754
9755 let mut rgba = samples_to_rgba(sample_data, params, icc, false);
9757 if params.mask_color.is_some() {
9758 apply_mask_color_rgba(&mut rgba, sample_data, params);
9759 }
9760
9761 let image_inv = params.image_matrix.invert()?;
9763 let combined = params.ctm.concat(&image_inv);
9764 let base_transform = enforce_min_image_size(to_transform(&combined), iw, ih);
9765
9766 let (data, width, height, adj_t) =
9768 match prescale_image(&rgba, iw, ih, base_transform, params.interpolate) {
9769 Some((d, w, h, t)) => {
9770 drop(rgba); (d, w, h, t)
9772 }
9773 None => (rgba, iw, ih, base_transform),
9774 };
9775
9776 let quality = image_filter_quality(adj_t, params.interpolate);
9777
9778 Some(PreprocessedImage {
9779 data,
9780 width,
9781 height,
9782 adj_sx: adj_t.sx,
9783 adj_ky: adj_t.ky,
9784 adj_kx: adj_t.kx,
9785 adj_sy: adj_t.sy,
9786 quality,
9787 })
9788 })
9789 .collect()
9790}
9791
9792#[allow(clippy::too_many_arguments)]
9798pub fn render_region_prepared(
9799 list: &DisplayList,
9800 prepared: &PreparedDisplayList,
9801 vp_x: f64,
9802 vp_y: f64,
9803 vp_w: f64,
9804 vp_h: f64,
9805 pixel_w: u32,
9806 pixel_h: u32,
9807 dpi: f64,
9808 icc: Option<&IccCache>,
9809 image_cache: Option<&ImageCache>,
9810 no_aa: bool,
9811) -> Vec<u8> {
9812 if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
9813 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
9814 }
9815
9816 let scale_x = pixel_w as f64 / vp_w;
9817 let scale_y = pixel_h as f64 / vp_h;
9818 let effective_dpi = dpi * scale_x;
9819
9820 const OVERLAP: u32 = 6;
9831 let render_h = pixel_h + 2 * OVERLAP;
9832 let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create viewport pixmap");
9833 pixmap.fill(Color::TRANSPARENT);
9835
9836 let cmyk_buf = if has_overprint_elements(list)
9837 || list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
9838 || has_cmyk_group(list)
9839 {
9840 Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
9841 } else {
9842 None
9843 };
9844
9845 let mut state = BandState {
9846 clip_region: None,
9847 spare_mask: None,
9848 clip_mask_cache: HashMap::new(),
9849 clip_mask_seen: prepared.clip_seen.clone(),
9850 mask_pool: Vec::new(),
9851 cmyk_buffer: cmyk_buf,
9852 op_bg_snapshot: None,
9853 op_touched: None,
9854 spot_mask: None,
9855 };
9856
9857 let elements = list.elements();
9858 let vp_x_f = vp_x as f32;
9859 let vp_y_f = vp_y as f32;
9860 let sx = scale_x as f32;
9861 let sy = scale_y as f32;
9862 let vp_x_max = vp_x + vp_w;
9863 let vp_y_max = vp_y + vp_h;
9864
9865 for epoch in &prepared.epochs {
9866 if !epoch.has_erase_page {
9867 match epoch.paint_bbox {
9868 Some(ref pb)
9869 if pb.x_max <= vp_x
9870 || pb.x_min >= vp_x_max
9871 || pb.y_max <= vp_y
9872 || pb.y_min >= vp_y_max =>
9873 {
9874 continue;
9875 }
9876 None => continue,
9877 _ => {}
9878 }
9879 }
9880
9881 #[allow(clippy::needless_range_loop)]
9882 for i in epoch.start_idx..epoch.end_idx {
9883 let force_process = matches!(
9886 &elements[i],
9887 DisplayElement::OcgGroup { elements: inner, .. }
9888 if contains_clip_op(inner)
9889 );
9890 if !force_process
9891 && let Some(ref bbox) = prepared.bboxes[i]
9892 && (bbox.x_max <= vp_x
9893 || bbox.x_min >= vp_x_max
9894 || bbox.y_max <= vp_y
9895 || bbox.y_min >= vp_y_max)
9896 {
9897 continue;
9898 }
9899 let ctx = RenderContext {
9900 vp_x: vp_x_f,
9901 vp_y: vp_y_f,
9902 scale_x: sx,
9903 scale_y: sy,
9904 out_w: pixel_w,
9905 out_h: render_h,
9906 effective_dpi,
9907 icc,
9908 image_cache,
9909 preprocessed: None,
9910 elem_idx: i,
9911 no_aa,
9912 opm_zero_transparent: false,
9913 knockout_painter_pass: KnockoutPainterPass::None,
9914 parent_group_isolated: false,
9915 alpha_extraction_pass: false,
9916 };
9917 render_element(&mut pixmap, &mut state, &elements[i], &ctx);
9918 }
9919 }
9920
9921 composite_onto_white(pixmap.data_mut());
9923 let row_bytes = pixel_w as usize * 4;
9925 let end = pixel_h as usize * row_bytes;
9926 pixmap.data()[..end].to_vec()
9927}
9928
9929pub fn viewport_band_count(pixel_w: u32, pixel_h: u32) -> (u32, u32) {
9934 let band_h = select_band_height(pixel_w, pixel_h);
9935 let num_bands = if band_h >= pixel_h {
9936 1
9937 } else {
9938 pixel_h.div_ceil(band_h)
9939 };
9940 (num_bands, band_h)
9941}
9942
9943#[allow(clippy::too_many_arguments)]
9952pub fn render_region_single_band(
9953 list: &DisplayList,
9954 prepared: &PreparedDisplayList,
9955 vp_x: f64,
9956 vp_y: f64,
9957 vp_w: f64,
9958 vp_h: f64,
9959 pixel_w: u32,
9960 pixel_h: u32,
9961 band_idx: u32,
9962 band_h: u32,
9963 num_bands: u32,
9964 dpi: f64,
9965 icc: Option<&IccCache>,
9966 image_cache: Option<&ImageCache>,
9967 no_aa: bool,
9968) -> Vec<u8> {
9969 if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
9970 let actual_h = if band_idx < num_bands - 1 {
9971 band_h
9972 } else {
9973 pixel_h - band_idx * band_h
9974 };
9975 return vec![0xFF; pixel_w as usize * actual_h as usize * 4];
9976 }
9977
9978 let scale_x = pixel_w as f64 / vp_w;
9979 let scale_y = pixel_h as f64 / vp_h;
9980 let effective_dpi = dpi * scale_x;
9981
9982 let out_y_start = band_idx * band_h;
9984 let actual_h = if band_idx < num_bands - 1 {
9985 band_h
9986 } else {
9987 pixel_h - out_y_start
9988 };
9989
9990 const OVERLAP: u32 = 6;
10001 let render_y_start = out_y_start.saturating_sub(OVERLAP);
10002 let render_y_end = (out_y_start + actual_h + OVERLAP).min(pixel_h);
10003 let render_h = band_h + 2 * OVERLAP;
10004 let overlap_top = out_y_start - render_y_start;
10005
10006 let src_y_min = vp_y + render_y_start as f64 / scale_y;
10008 let src_y_max = vp_y + render_y_end as f64 / scale_y;
10009
10010 let band_vp_y = vp_y + render_y_start as f64 / scale_y;
10012
10013 let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create band pixmap");
10014 pixmap.fill(Color::TRANSPARENT);
10015
10016 let cmyk_buf = if has_overprint_elements(list)
10017 || list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
10018 || has_cmyk_group(list)
10019 {
10020 Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
10021 } else {
10022 None
10023 };
10024
10025 let mut state = BandState {
10026 clip_region: None,
10027 spare_mask: None,
10028 clip_mask_cache: HashMap::new(),
10029 clip_mask_seen: prepared.clip_seen.clone(),
10030 mask_pool: Vec::new(),
10031 cmyk_buffer: cmyk_buf,
10032 op_bg_snapshot: None,
10033 op_touched: None,
10034 spot_mask: None,
10035 };
10036
10037 let elements = list.elements();
10038 let vp_x_f = vp_x as f32;
10039 let band_vp_y_f = band_vp_y as f32;
10040 let sx = scale_x as f32;
10041 let sy = scale_y as f32;
10042 let vp_x_max = vp_x + vp_w;
10043
10044 for epoch in &prepared.epochs {
10045 if !epoch.has_erase_page {
10046 match epoch.paint_bbox {
10047 Some(ref pb)
10048 if pb.x_max <= vp_x
10049 || pb.x_min >= vp_x_max
10050 || pb.y_max <= src_y_min
10051 || pb.y_min >= src_y_max =>
10052 {
10053 continue;
10054 }
10055 None => continue,
10056 _ => {}
10057 }
10058 }
10059
10060 #[allow(clippy::needless_range_loop)]
10061 for i in epoch.start_idx..epoch.end_idx {
10062 let force_process = matches!(
10066 &elements[i],
10067 DisplayElement::OcgGroup { elements: inner, .. }
10068 if contains_clip_op(inner)
10069 );
10070 if !force_process
10071 && let Some(ref bbox) = prepared.bboxes[i]
10072 && (bbox.x_max <= vp_x
10073 || bbox.x_min >= vp_x_max
10074 || bbox.y_max <= src_y_min
10075 || bbox.y_min >= src_y_max)
10076 {
10077 continue;
10078 }
10079 let ctx = RenderContext {
10080 vp_x: vp_x_f,
10081 vp_y: band_vp_y_f,
10082 scale_x: sx,
10083 scale_y: sy,
10084 out_w: pixel_w,
10085 out_h: render_h,
10086 effective_dpi,
10087 icc,
10088 image_cache,
10089 preprocessed: None,
10090 elem_idx: i,
10091 no_aa,
10092 opm_zero_transparent: false,
10093 knockout_painter_pass: KnockoutPainterPass::None,
10094 parent_group_isolated: false,
10095 alpha_extraction_pass: false,
10096 };
10097 render_element(&mut pixmap, &mut state, &elements[i], &ctx);
10098 }
10099 }
10100
10101 composite_onto_white(pixmap.data_mut());
10103
10104 let row_bytes = pixel_w as usize * 4;
10106 let start = overlap_top as usize * row_bytes;
10107 let end = start + actual_h as usize * row_bytes;
10108 pixmap.data()[start..end].to_vec()
10109}
10110
10111#[allow(clippy::too_many_arguments)]
10120pub fn render_region_prepared_parallel(
10121 list: &DisplayList,
10122 prepared: &PreparedDisplayList,
10123 vp_x: f64,
10124 vp_y: f64,
10125 vp_w: f64,
10126 vp_h: f64,
10127 pixel_w: u32,
10128 pixel_h: u32,
10129 dpi: f64,
10130 icc: Option<&IccCache>,
10131 image_cache: Option<&ImageCache>,
10132 no_aa: bool,
10133) -> Vec<u8> {
10134 let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
10135
10136 if num_bands <= 1 {
10137 return render_region_prepared(
10139 list,
10140 prepared,
10141 vp_x,
10142 vp_y,
10143 vp_w,
10144 vp_h,
10145 pixel_w,
10146 pixel_h,
10147 dpi,
10148 icc,
10149 image_cache,
10150 no_aa,
10151 );
10152 }
10153
10154 let render_band = |band_idx: u32| -> Vec<u8> {
10155 render_region_single_band(
10156 list,
10157 prepared,
10158 vp_x,
10159 vp_y,
10160 vp_w,
10161 vp_h,
10162 pixel_w,
10163 pixel_h,
10164 band_idx,
10165 band_h,
10166 num_bands,
10167 dpi,
10168 icc,
10169 image_cache,
10170 no_aa,
10171 )
10172 };
10173
10174 let row_bytes = pixel_w as usize * 4;
10175 let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
10176
10177 #[cfg(feature = "parallel")]
10178 {
10179 let chunk_size = rayon::current_num_threads().max(1);
10180
10181 for chunk_start in (0..num_bands).step_by(chunk_size) {
10182 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
10183
10184 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
10185 .into_par_iter()
10186 .map(&render_band)
10187 .collect();
10188
10189 for (i, band_data) in rendered.iter().enumerate() {
10190 let band_idx = chunk_start + i as u32;
10191 let y_start = (band_idx * band_h) as usize;
10192 let dest_start = y_start * row_bytes;
10193 let len = band_data.len();
10194 result[dest_start..dest_start + len].copy_from_slice(band_data);
10195 }
10196 }
10197 }
10198 #[cfg(not(feature = "parallel"))]
10199 {
10200 for band_idx in 0..num_bands {
10201 let band_data = render_band(band_idx);
10202 let y_start = (band_idx * band_h) as usize;
10203 let dest_start = y_start * row_bytes;
10204 let len = band_data.len();
10205 result[dest_start..dest_start + len].copy_from_slice(&band_data);
10206 }
10207 }
10208
10209 result
10210}
10211
10212#[allow(clippy::too_many_arguments)]
10217pub fn render_region_prepared_parallel_with_progress(
10218 list: &DisplayList,
10219 prepared: &PreparedDisplayList,
10220 vp_x: f64,
10221 vp_y: f64,
10222 vp_w: f64,
10223 vp_h: f64,
10224 pixel_w: u32,
10225 pixel_h: u32,
10226 dpi: f64,
10227 icc: Option<&IccCache>,
10228 image_cache: Option<&ImageCache>,
10229 no_aa: bool,
10230 progress: &std::sync::atomic::AtomicU32,
10231) -> Vec<u8> {
10232 let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
10233
10234 if num_bands <= 1 {
10235 let result = render_region_prepared(
10236 list,
10237 prepared,
10238 vp_x,
10239 vp_y,
10240 vp_w,
10241 vp_h,
10242 pixel_w,
10243 pixel_h,
10244 dpi,
10245 icc,
10246 image_cache,
10247 no_aa,
10248 );
10249 progress.store(1, std::sync::atomic::Ordering::Relaxed);
10250 return result;
10251 }
10252
10253 let render_band = |band_idx: u32| -> Vec<u8> {
10254 render_region_single_band(
10255 list,
10256 prepared,
10257 vp_x,
10258 vp_y,
10259 vp_w,
10260 vp_h,
10261 pixel_w,
10262 pixel_h,
10263 band_idx,
10264 band_h,
10265 num_bands,
10266 dpi,
10267 icc,
10268 image_cache,
10269 no_aa,
10270 )
10271 };
10272
10273 let row_bytes = pixel_w as usize * 4;
10274 let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
10275
10276 #[cfg(feature = "parallel")]
10277 {
10278 let chunk_size = rayon::current_num_threads().max(1);
10279
10280 for chunk_start in (0..num_bands).step_by(chunk_size) {
10281 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
10282
10283 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
10284 .into_par_iter()
10285 .map(&render_band)
10286 .collect();
10287
10288 for (i, band_data) in rendered.iter().enumerate() {
10289 let band_idx = chunk_start + i as u32;
10290 let y_start = (band_idx * band_h) as usize;
10291 let dest_start = y_start * row_bytes;
10292 let len = band_data.len();
10293 result[dest_start..dest_start + len].copy_from_slice(band_data);
10294 }
10295 progress.store(chunk_end, std::sync::atomic::Ordering::Relaxed);
10296 }
10297 }
10298 #[cfg(not(feature = "parallel"))]
10299 {
10300 for band_idx in 0..num_bands {
10301 let band_data = render_band(band_idx);
10302 let y_start = (band_idx * band_h) as usize;
10303 let dest_start = y_start * row_bytes;
10304 let len = band_data.len();
10305 result[dest_start..dest_start + len].copy_from_slice(&band_data);
10306 progress.store(band_idx + 1, std::sync::atomic::Ordering::Relaxed);
10307 }
10308 }
10309
10310 result
10311}
10312
10313#[allow(clippy::too_many_arguments)]
10316pub fn render_region_prepared_parallel_cancellable(
10317 list: &DisplayList,
10318 prepared: &PreparedDisplayList,
10319 vp_x: f64,
10320 vp_y: f64,
10321 vp_w: f64,
10322 vp_h: f64,
10323 pixel_w: u32,
10324 pixel_h: u32,
10325 dpi: f64,
10326 icc: Option<&IccCache>,
10327 image_cache: Option<&ImageCache>,
10328 no_aa: bool,
10329 cancelled: &std::sync::atomic::AtomicBool,
10330) -> Option<Vec<u8>> {
10331 if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
10332 return None;
10333 }
10334
10335 let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
10336
10337 if num_bands <= 1 {
10338 return Some(render_region_prepared(
10339 list,
10340 prepared,
10341 vp_x,
10342 vp_y,
10343 vp_w,
10344 vp_h,
10345 pixel_w,
10346 pixel_h,
10347 dpi,
10348 icc,
10349 image_cache,
10350 no_aa,
10351 ));
10352 }
10353
10354 let render_band = |band_idx: u32| -> Vec<u8> {
10355 render_region_single_band(
10356 list,
10357 prepared,
10358 vp_x,
10359 vp_y,
10360 vp_w,
10361 vp_h,
10362 pixel_w,
10363 pixel_h,
10364 band_idx,
10365 band_h,
10366 num_bands,
10367 dpi,
10368 icc,
10369 image_cache,
10370 no_aa,
10371 )
10372 };
10373
10374 let row_bytes = pixel_w as usize * 4;
10375 let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
10376
10377 #[cfg(feature = "parallel")]
10378 {
10379 let chunk_size = rayon::current_num_threads().max(1);
10380
10381 for chunk_start in (0..num_bands).step_by(chunk_size) {
10382 if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
10383 return None;
10384 }
10385 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
10386
10387 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
10388 .into_par_iter()
10389 .map(&render_band)
10390 .collect();
10391
10392 for (i, band_data) in rendered.iter().enumerate() {
10393 let band_idx = chunk_start + i as u32;
10394 let y_start = (band_idx * band_h) as usize;
10395 let dest_start = y_start * row_bytes;
10396 let len = band_data.len();
10397 result[dest_start..dest_start + len].copy_from_slice(band_data);
10398 }
10399 }
10400 }
10401 #[cfg(not(feature = "parallel"))]
10402 {
10403 for band_idx in 0..num_bands {
10404 if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
10405 return None;
10406 }
10407 let band_data = render_band(band_idx);
10408 let y_start = (band_idx * band_h) as usize;
10409 let dest_start = y_start * row_bytes;
10410 let len = band_data.len();
10411 result[dest_start..dest_start + len].copy_from_slice(&band_data);
10412 }
10413 }
10414
10415 Some(result)
10416}
10417
10418pub fn render_to_rgba(
10426 list: &DisplayList,
10427 pixel_w: u32,
10428 pixel_h: u32,
10429 dpi: f64,
10430 icc: Option<&IccCache>,
10431 no_aa: bool,
10432) -> Vec<u8> {
10433 if pixel_w == 0 || pixel_h == 0 {
10434 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10435 }
10436
10437 let mut icc_cache = match icc {
10438 Some(c) => c.clone(),
10439 None => IccCache::new(),
10440 };
10441 register_shading_icc_profiles(list, &mut icc_cache);
10444
10445 let mut sink = MemorySink {
10446 data: Vec::new(),
10447 width: 0,
10448 };
10449
10450 let band_h = select_band_height(pixel_w, pixel_h);
10451 if let Err(e) = render_banded_to_sink(
10452 pixel_w, pixel_h, band_h, dpi, list, &mut sink, &icc_cache, no_aa,
10453 ) {
10454 eprintln!("render_to_rgba: banded render failed: {e}");
10455 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10456 }
10457
10458 sink.data
10459}
10460
10461pub fn render_to_rgba_viewport(
10474 list: &DisplayList,
10475 pixel_w: u32,
10476 pixel_h: u32,
10477 dpi: f64,
10478 icc: Option<&IccCache>,
10479 no_aa: bool,
10480) -> Vec<u8> {
10481 if pixel_w == 0 || pixel_h == 0 {
10482 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10483 }
10484
10485 let mut icc_cache = match icc {
10486 Some(c) => c.clone(),
10487 None => IccCache::new(),
10488 };
10489 register_shading_icc_profiles(list, &mut icc_cache);
10490
10491 let prepared = prepare_display_list(list);
10492 render_region_prepared_parallel(
10493 list,
10494 &prepared,
10495 0.0,
10496 0.0,
10497 pixel_w as f64,
10498 pixel_h as f64,
10499 pixel_w,
10500 pixel_h,
10501 dpi,
10502 Some(&icc_cache),
10503 None,
10504 no_aa,
10505 )
10506}
10507
10508fn debug_bbox_lines(list: &DisplayList, dpi: f64, depth: usize, out: &mut Vec<String>) {
10515 let y_bboxes = precompute_bboxes(list, dpi);
10516 let full_bboxes = precompute_full_bboxes(list, dpi);
10517 let elements = list.elements();
10518 let indent = " ".repeat(depth);
10519 for (i, elem) in elements.iter().enumerate() {
10520 let kind = match elem {
10521 DisplayElement::Fill { .. } => "Fill",
10522 DisplayElement::Stroke { .. } => "Stroke",
10523 DisplayElement::Image { .. } => "Image",
10524 DisplayElement::AxialShading { .. } => "AxialShading",
10525 DisplayElement::RadialShading { .. } => "RadialShading",
10526 DisplayElement::MeshShading { .. } => "MeshShading",
10527 DisplayElement::PatchShading { .. } => "PatchShading",
10528 DisplayElement::PatternFill { .. } => "PatternFill",
10529 DisplayElement::Group { .. } => "Group",
10530 DisplayElement::SoftMasked { .. } => "SoftMasked",
10531 DisplayElement::OcgGroup { .. } => "OcgGroup",
10532 DisplayElement::Clip { .. } => "Clip",
10533 DisplayElement::InitClip => "InitClip",
10534 DisplayElement::ErasePage => "ErasePage",
10535 DisplayElement::Text { .. } => "Text",
10536 };
10537 let yb = &y_bboxes[i];
10538 let fb = &full_bboxes[i];
10539 let mut diff = false;
10540 if yb.is_some() != fb.is_some() {
10541 diff = true;
10542 }
10543 if let (Some(yb), Some(fb)) = (yb, fb)
10544 && ((yb.y_min - fb.y_min).abs() > 1e-9 || (yb.y_max - fb.y_max).abs() > 1e-9)
10545 {
10546 diff = true;
10547 }
10548 let yb_s = match yb {
10549 Some(b) => format!("Y[{:8.3}..{:8.3}]", b.y_min, b.y_max),
10550 None => "Y[None]".to_string(),
10551 };
10552 let fb_s = match fb {
10553 Some(b) => format!(
10554 "2D[x {:8.3}..{:8.3} y {:8.3}..{:8.3}]",
10555 b.x_min, b.x_max, b.y_min, b.y_max
10556 ),
10557 None => "2D[None]".to_string(),
10558 };
10559 out.push(format!(
10560 "{}{:4} {:15} {:30} {:55} {}",
10561 indent,
10562 i,
10563 kind,
10564 yb_s,
10565 fb_s,
10566 if diff { "DIFF" } else { "" }
10567 ));
10568 if let DisplayElement::Stroke { path, params } = elem {
10569 let rp = path_full_bbox(path);
10570 let m = ¶ms.ctm;
10571 out.push(format!(
10572 "{} ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] lw={:.4} miter={:.4} raw={}",
10573 indent,
10574 m.a,
10575 m.b,
10576 m.c,
10577 m.d,
10578 m.tx,
10579 m.ty,
10580 params.line_width,
10581 params.miter_limit,
10582 match rp {
10583 Some(b) => format!(
10584 "x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
10585 b.x_min, b.x_max, b.y_min, b.y_max
10586 ),
10587 None => "None".to_string(),
10588 }
10589 ));
10590 }
10591 if let DisplayElement::Clip { path, params } = elem {
10592 let rp = path_full_bbox(path);
10593 let m = ¶ms.ctm;
10594 out.push(format!(
10595 "{} clip ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] rule={:?} raw={}",
10596 indent,
10597 m.a,
10598 m.b,
10599 m.c,
10600 m.d,
10601 m.tx,
10602 m.ty,
10603 params.fill_rule,
10604 match rp {
10605 Some(b) => format!(
10606 "x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
10607 b.x_min, b.x_max, b.y_min, b.y_max
10608 ),
10609 None => "None".to_string(),
10610 }
10611 ));
10612 }
10613 if let DisplayElement::PatchShading { params } = elem {
10614 out.push(format!(
10615 "{} patch ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] bbox={:?} patches={}",
10616 indent,
10617 params.ctm.a,
10618 params.ctm.b,
10619 params.ctm.c,
10620 params.ctm.d,
10621 params.ctm.tx,
10622 params.ctm.ty,
10623 params.bbox,
10624 params.patches.len()
10625 ));
10626 if !params.patches.is_empty() {
10627 let patch = ¶ms.patches[0];
10628 let mut x_min = f64::INFINITY;
10630 let mut y_min = f64::INFINITY;
10631 let mut x_max = f64::NEG_INFINITY;
10632 let mut y_max = f64::NEG_INFINITY;
10633 for &(px, py) in &patch.points {
10634 let (dx, dy) = params.ctm.transform_point(px, py);
10635 x_min = x_min.min(dx);
10636 y_min = y_min.min(dy);
10637 x_max = x_max.max(dx);
10638 y_max = y_max.max(dy);
10639 }
10640 out.push(format!(
10641 "{} patch[0] pts={} dev x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
10642 indent,
10643 patch.points.len(),
10644 x_min,
10645 x_max,
10646 y_min,
10647 y_max
10648 ));
10649 }
10650 }
10651 if let DisplayElement::Group {
10652 elements: inner,
10653 params,
10654 } = elem
10655 {
10656 out.push(format!(
10657 "{} group bbox={:?} iso={} ko={} alpha={} bm={} cs={:?}",
10658 indent,
10659 params.bbox,
10660 params.isolated,
10661 params.knockout,
10662 params.alpha,
10663 params.blend_mode,
10664 params.color_space
10665 ));
10666 debug_bbox_lines(inner, dpi, depth + 1, out);
10667 }
10668 if let DisplayElement::SoftMasked {
10669 content, params, ..
10670 } = elem
10671 {
10672 out.push(format!(
10673 "{} softmasked bbox={:?}",
10674 indent, params.bbox
10675 ));
10676 debug_bbox_lines(content, dpi, depth + 1, out);
10677 }
10678 if let DisplayElement::OcgGroup {
10679 elements: inner,
10680 default_visible,
10681 ..
10682 } = elem
10683 {
10684 out.push(format!(
10685 "{} ocg default_visible={}",
10686 indent, default_visible
10687 ));
10688 debug_bbox_lines(inner, dpi, depth + 1, out);
10689 }
10690 }
10691}
10692
10693pub fn debug_bbox_comparison(list: &DisplayList, dpi: f64) -> Vec<String> {
10694 let mut out = Vec::new();
10695 debug_bbox_lines(list, dpi, 0, &mut out);
10696 out
10697}
10698
10699struct MemorySink {
10701 data: Vec<u8>,
10702 width: u32,
10703}
10704
10705impl stet_graphics::device::PageSink for MemorySink {
10706 fn begin_page(&mut self, width: u32, height: u32) -> Result<(), String> {
10707 self.width = width;
10708 self.data.reserve(width as usize * height as usize * 4);
10709 Ok(())
10710 }
10711
10712 fn write_rows(&mut self, rgba_rows: &[u8], _num_rows: u32) -> Result<(), String> {
10713 self.data.extend_from_slice(rgba_rows);
10714 Ok(())
10715 }
10716
10717 fn end_page(&mut self) -> Result<(), String> {
10718 Ok(())
10719 }
10720}
10721
10722#[allow(clippy::too_many_arguments)]
10731pub fn render_region(
10732 list: &DisplayList,
10733 vp_x: f64,
10734 vp_y: f64,
10735 vp_w: f64,
10736 vp_h: f64,
10737 pixel_w: u32,
10738 pixel_h: u32,
10739 dpi: f64,
10740 icc: Option<&IccCache>,
10741 image_cache: Option<&ImageCache>,
10742 no_aa: bool,
10743) -> Vec<u8> {
10744 if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
10745 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10746 }
10747
10748 let scale_x = pixel_w as f64 / vp_w;
10749 let scale_y = pixel_h as f64 / vp_h;
10750 let effective_dpi = dpi * scale_x;
10752
10753 let bboxes = precompute_full_bboxes(list, effective_dpi);
10754 let epochs = build_viewport_epochs(list, &bboxes);
10755 let clip_seen = precompute_clip_seen(list);
10756
10757 const OVERLAP: u32 = 6;
10761 let render_h = pixel_h + 2 * OVERLAP;
10762
10763 let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create viewport pixmap");
10764 pixmap.fill(Color::TRANSPARENT);
10765
10766 let cmyk_buf = if has_overprint_elements(list)
10767 || list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
10768 || has_cmyk_group(list)
10769 {
10770 Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
10771 } else {
10772 None
10773 };
10774
10775 let mut state = BandState {
10776 clip_region: None,
10777 spare_mask: None,
10778 clip_mask_cache: HashMap::new(),
10779 clip_mask_seen: clip_seen,
10780 mask_pool: Vec::new(),
10781 cmyk_buffer: cmyk_buf,
10782 op_bg_snapshot: None,
10783 op_touched: None,
10784 spot_mask: None,
10785 };
10786
10787 let elements = list.elements();
10788 let vp_x_f = vp_x as f32;
10789 let vp_y_f = vp_y as f32;
10790 let sx = scale_x as f32;
10791 let sy = scale_y as f32;
10792 let vp_x_max = vp_x + vp_w;
10793 let vp_y_max = vp_y + vp_h;
10794
10795 for epoch in &epochs {
10796 if !epoch.has_erase_page {
10798 match epoch.paint_bbox {
10799 Some(ref pb)
10800 if pb.x_max <= vp_x
10801 || pb.x_min >= vp_x_max
10802 || pb.y_max <= vp_y
10803 || pb.y_min >= vp_y_max =>
10804 {
10805 continue;
10806 }
10807 None => continue,
10808 _ => {}
10809 }
10810 }
10811
10812 for i in epoch.start_idx..epoch.end_idx {
10813 let force_process = matches!(
10816 &elements[i],
10817 DisplayElement::OcgGroup { elements: inner, .. }
10818 if contains_clip_op(inner)
10819 );
10820 if !force_process
10822 && let Some(ref bbox) = bboxes[i]
10823 && (bbox.x_max <= vp_x
10824 || bbox.x_min >= vp_x_max
10825 || bbox.y_max <= vp_y
10826 || bbox.y_min >= vp_y_max)
10827 {
10828 continue;
10829 }
10830 let ctx = RenderContext {
10831 vp_x: vp_x_f,
10832 vp_y: vp_y_f,
10833 scale_x: sx,
10834 scale_y: sy,
10835 out_w: pixel_w,
10836 out_h: render_h,
10837 effective_dpi,
10838 icc,
10839 image_cache,
10840 preprocessed: None,
10841 elem_idx: i,
10842 no_aa,
10843 opm_zero_transparent: false,
10844 knockout_painter_pass: KnockoutPainterPass::None,
10845 parent_group_isolated: false,
10846 alpha_extraction_pass: false,
10847 };
10848 render_element(&mut pixmap, &mut state, &elements[i], &ctx);
10849 }
10850 }
10851
10852 composite_onto_white(pixmap.data_mut());
10853 let row_bytes = pixel_w as usize * 4;
10855 let end = pixel_h as usize * row_bytes;
10856 pixmap.data()[..end].to_vec()
10857}
10858fn copy_backdrop_crop(
10860 parent: &Pixmap,
10861 crop_x: i32,
10862 crop_y: i32,
10863 crop_w: u32,
10864 crop_h: u32,
10865) -> Vec<u8> {
10866 let pw = parent.width() as usize;
10867 let src = parent.data();
10868 let cw = crop_w as usize;
10869 let ch = crop_h as usize;
10870 let cx = crop_x as usize;
10871 let cy = crop_y as usize;
10872 let mut backdrop = vec![0u8; cw * ch * 4];
10873 for row in 0..ch {
10874 let src_off = ((cy + row) * pw + cx) * 4;
10875 let dst_off = row * cw * 4;
10876 backdrop[dst_off..dst_off + cw * 4].copy_from_slice(&src[src_off..src_off + cw * 4]);
10877 }
10878 backdrop
10879}
10880fn clip_polygon_halfplane(
10885 poly: &[(f32, f32)],
10886 nx: f32,
10887 ny: f32,
10888 px: f32,
10889 py: f32,
10890) -> Vec<(f32, f32)> {
10891 if poly.is_empty() {
10892 return vec![];
10893 }
10894 let dot = |x: f32, y: f32| nx * (x - px) + ny * (y - py);
10895 let mut out = Vec::with_capacity(poly.len() + 1);
10896 let n = poly.len();
10897 for i in 0..n {
10898 let (ax, ay) = poly[i];
10899 let (bx, by) = poly[(i + 1) % n];
10900 let da = dot(ax, ay);
10901 let db = dot(bx, by);
10902 if da >= 0.0 {
10903 out.push((ax, ay));
10904 }
10905 if (da >= 0.0) != (db >= 0.0) {
10906 let t = da / (da - db);
10908 out.push((ax + t * (bx - ax), ay + t * (by - ay)));
10909 }
10910 }
10911 out
10912}
10913
10914#[allow(clippy::too_many_arguments)]
10916fn render_axial_shading(
10917 pixmap: &mut Pixmap,
10918 params: &AxialShadingParams,
10919 vp_x: f32,
10920 vp_y: f32,
10921 scale_x: f32,
10922 scale_y: f32,
10923 clip_mask: Option<&Mask>,
10924 no_aa: bool,
10925 cmyk_buf: Option<&mut [f32]>,
10926 icc: Option<&IccCache>,
10927) {
10928 let pw = pixmap.width();
10929 let ph = pixmap.height();
10930 if params.color_stops.is_empty() || pw == 0 || ph == 0 {
10931 return;
10932 }
10933
10934 let (mut rx_min, mut ry_min, mut rx_max, mut ry_max) = if let Some(bbox) = ¶ms.bbox {
10935 let corners = [
10936 params.ctm.transform_point(bbox[0], bbox[1]),
10937 params.ctm.transform_point(bbox[2], bbox[1]),
10938 params.ctm.transform_point(bbox[0], bbox[3]),
10939 params.ctm.transform_point(bbox[2], bbox[3]),
10940 ];
10941 let x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
10942 let y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
10943 let x_max = corners
10944 .iter()
10945 .map(|c| c.0)
10946 .fold(f64::NEG_INFINITY, f64::max);
10947 let y_max = corners
10948 .iter()
10949 .map(|c| c.1)
10950 .fold(f64::NEG_INFINITY, f64::max);
10951 (
10952 ((x_min as f32 - vp_x) * scale_x).max(0.0),
10953 ((y_min as f32 - vp_y) * scale_y).max(0.0),
10954 ((x_max as f32 - vp_x) * scale_x).min(pw as f32),
10955 ((y_max as f32 - vp_y) * scale_y).min(ph as f32),
10956 )
10957 } else {
10958 (0.0, 0.0, pw as f32, ph as f32)
10959 };
10960
10961 if rx_max <= rx_min || ry_max <= ry_min {
10962 return;
10963 }
10964
10965 let (dx0, dy0) = params.ctm.transform_point(params.x0, params.y0);
10967 let (dx1, dy1) = params.ctm.transform_point(params.x1, params.y1);
10968
10969 let needs_perpendicular_clip = (!params.extend_start || !params.extend_end) && {
10973 let axis_x = dx1 - dx0;
10974 let axis_y = dy1 - dy0;
10975 axis_x.abs() > 1e-6 && axis_y.abs() > 1e-6
10976 };
10977
10978 let bbox_is_rotated =
10981 params.bbox.is_some() && (params.ctm.b.abs() > 1e-10 || params.ctm.c.abs() > 1e-10);
10982
10983 if needs_perpendicular_clip {
10984 let stops = build_gradient_stops(¶ms.color_stops);
10987 if stops.is_empty() {
10988 return;
10989 }
10990 let start = stet_tiny_skia::Point::from_xy(params.x0 as f32, params.y0 as f32);
10991 let end = stet_tiny_skia::Point::from_xy(params.x1 as f32, params.y1 as f32);
10992 let gradient_transform =
10993 viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
10994 let Some(gradient) = stet_tiny_skia::LinearGradient::new(
10995 start,
10996 end,
10997 stops,
10998 stet_tiny_skia::SpreadMode::Pad,
10999 gradient_transform,
11000 ) else {
11001 return;
11002 };
11003 let paint = Paint {
11004 shader: gradient,
11005 anti_alias: !no_aa,
11006 ..Paint::default()
11007 };
11008
11009 let mut poly: Vec<(f32, f32)> = if bbox_is_rotated {
11011 let bbox = params.bbox.as_ref().unwrap();
11012 let corners = [
11013 params.ctm.transform_point(bbox[0], bbox[1]),
11014 params.ctm.transform_point(bbox[2], bbox[1]),
11015 params.ctm.transform_point(bbox[2], bbox[3]),
11016 params.ctm.transform_point(bbox[0], bbox[3]),
11017 ];
11018 corners
11019 .iter()
11020 .map(|(x, y)| ((*x as f32 - vp_x) * scale_x, (*y as f32 - vp_y) * scale_y))
11021 .collect()
11022 } else {
11023 vec![
11024 (rx_min, ry_min),
11025 (rx_max, ry_min),
11026 (rx_max, ry_max),
11027 (rx_min, ry_max),
11028 ]
11029 };
11030 let ax = (dx1 - dx0) as f32 * scale_x;
11031 let ay = (dy1 - dy0) as f32 * scale_y;
11032 if !params.extend_start {
11033 let px = (dx0 as f32 - vp_x) * scale_x;
11034 let py = (dy0 as f32 - vp_y) * scale_y;
11035 poly = clip_polygon_halfplane(&poly, ax, ay, px, py);
11036 }
11037 if !params.extend_end {
11038 let px = (dx1 as f32 - vp_x) * scale_x;
11039 let py = (dy1 as f32 - vp_y) * scale_y;
11040 poly = clip_polygon_halfplane(&poly, -ax, -ay, px, py);
11041 }
11042 if poly.len() >= 3 {
11043 let mut pb = PathBuilder::new();
11044 pb.move_to(poly[0].0, poly[0].1);
11045 for &(x, y) in &poly[1..] {
11046 pb.line_to(x, y);
11047 }
11048 pb.close();
11049 if let Some(path) = pb.finish() {
11050 pixmap.fill_path(
11051 &path,
11052 &paint,
11053 SkiaFillRule::Winding,
11054 Transform::identity(),
11055 clip_mask,
11056 );
11057 }
11058 }
11059 } else {
11060 if !params.extend_start || !params.extend_end {
11063 let axis_x = dx1 - dx0;
11064 let axis_y = dy1 - dy0;
11065 let gx0 = (dx0 as f32 - vp_x) * scale_x;
11066 let gy0 = (dy0 as f32 - vp_y) * scale_y;
11067 let gx1 = (dx1 as f32 - vp_x) * scale_x;
11068 let gy1 = (dy1 as f32 - vp_y) * scale_y;
11069
11070 if axis_x.abs() >= axis_y.abs() {
11071 if !params.extend_start {
11072 if axis_x >= 0.0 {
11073 rx_min = rx_min.max(gx0);
11074 } else {
11075 rx_max = rx_max.min(gx0);
11076 }
11077 }
11078 if !params.extend_end {
11079 if axis_x >= 0.0 {
11080 rx_max = rx_max.min(gx1);
11081 } else {
11082 rx_min = rx_min.max(gx1);
11083 }
11084 }
11085 } else {
11086 if !params.extend_start {
11087 if axis_y >= 0.0 {
11088 ry_min = ry_min.max(gy0);
11089 } else {
11090 ry_max = ry_max.min(gy0);
11091 }
11092 }
11093 if !params.extend_end {
11094 if axis_y >= 0.0 {
11095 ry_max = ry_max.min(gy1);
11096 } else {
11097 ry_min = ry_min.max(gy1);
11098 }
11099 }
11100 }
11101 if rx_max <= rx_min || ry_max <= ry_min {
11102 return;
11103 }
11104 }
11105
11106 let ax = params.x1 - params.x0;
11108 let ay = params.y1 - params.y0;
11109 let axis_sq = ax * ax + ay * ay;
11110 if axis_sq < 1e-20 {
11111 return;
11112 }
11113
11114 let pixel_dx = (dx1 - dx0) * scale_x as f64;
11118 let pixel_dy = (dy1 - dy0) * scale_y as f64;
11119 let pixel_axis_len = (pixel_dx * pixel_dx + pixel_dy * pixel_dy).sqrt();
11120 let lut_size = (pixel_axis_len as usize)
11121 .max(params.color_stops.len())
11122 .max(256)
11123 .min(16384);
11124 let lut = build_gradient_lut(¶ms.color_stops, lut_size);
11125
11126 let Some(inv) = params.ctm.invert() else {
11127 return;
11128 };
11129 let inv_sx = 1.0 / scale_x as f64;
11130 let inv_sy = 1.0 / scale_y as f64;
11131 let dev_origin_x = vp_x as f64;
11132 let dev_origin_y = vp_y as f64;
11133
11134 let sx_base = inv.a * dev_origin_x + inv.c * dev_origin_y + inv.tx;
11138 let sy_base = inv.b * dev_origin_x + inv.d * dev_origin_y + inv.ty;
11139 let dsx_dx = inv.a * inv_sx;
11140 let dsx_dy = inv.c * inv_sy;
11141 let dsy_dx = inv.b * inv_sx;
11142 let dsy_dy = inv.d * inv_sy;
11143
11144 let inv_axis_sq = 1.0 / axis_sq;
11146 let t_origin = ((sx_base - params.x0) * ax + (sy_base - params.y0) * ay) * inv_axis_sq;
11147 let dt_dx = (dsx_dx * ax + dsy_dx * ay) * inv_axis_sq;
11148 let dt_dy = (dsx_dy * ax + dsy_dy * ay) * inv_axis_sq;
11149
11150 let bbox_pixel_clip = if bbox_is_rotated {
11153 let bbox = params.bbox.as_ref().unwrap();
11154 let (bx0, bx1) = (bbox[0].min(bbox[2]), bbox[0].max(bbox[2]));
11155 let (by0, by1) = (bbox[1].min(bbox[3]), bbox[1].max(bbox[3]));
11156 Some((
11157 dsx_dx, dsx_dy, sx_base, dsy_dx, dsy_dy, sy_base, bx0, by0, bx1, by1,
11158 ))
11159 } else {
11160 None
11161 };
11162
11163 let ix_min = rx_min.floor() as u32;
11164 let ix_max = rx_max.ceil().min(pw as f32) as u32;
11165 let iy_min = ry_min.floor() as u32;
11166 let iy_max = ry_max.ceil().min(ph as f32) as u32;
11167
11168 let stride = pw as usize * 4;
11169 let data = pixmap.data_mut();
11170 let mask_data = clip_mask.map(|m| m.data());
11171 let alpha = (params.alpha.clamp(0.0, 1.0) * 255.0 + 0.5) as u16;
11172
11173 for py in iy_min..iy_max {
11174 let t_row = t_origin + dt_dy * py as f64;
11175 let row_offset = py as usize * stride;
11176
11177 let (ux_row, uy_row) =
11179 if let Some((_, dux_dy, ux_base, _, duy_dy, uy_base, ..)) = &bbox_pixel_clip {
11180 (ux_base + dux_dy * py as f64, uy_base + duy_dy * py as f64)
11181 } else {
11182 (0.0, 0.0)
11183 };
11184
11185 for px in ix_min..ix_max {
11186 if let Some(md) = mask_data {
11188 if md[py as usize * pw as usize + px as usize] == 0 {
11189 continue;
11190 }
11191 }
11192
11193 if let Some((dux_dx, _, _, duy_dx, _, _, bx0, by0, bx1, by1)) = &bbox_pixel_clip {
11195 let ux = ux_row + dux_dx * px as f64;
11196 let uy = uy_row + duy_dx * px as f64;
11197 if ux < *bx0 || ux > *bx1 || uy < *by0 || uy > *by1 {
11198 continue;
11199 }
11200 }
11201
11202 let t = t_row + dt_dx * px as f64;
11203 let t_clamped = t.clamp(0.0, 1.0);
11204 let idx = (t_clamped * (lut_size - 1) as f64 + 0.5) as usize;
11205 let [r, g, b, _] = lut[idx.min(lut_size - 1)];
11206
11207 let offset = row_offset + px as usize * 4;
11208 if alpha >= 255 {
11209 data[offset] = r;
11210 data[offset + 1] = g;
11211 data[offset + 2] = b;
11212 data[offset + 3] = 255;
11213 } else {
11214 let a = alpha as u16;
11216 let inv_a = 255 - a;
11217 data[offset] = ((r as u16 * a + data[offset] as u16 * inv_a + 127) / 255) as u8;
11218 data[offset + 1] =
11219 ((g as u16 * a + data[offset + 1] as u16 * inv_a + 127) / 255) as u8;
11220 data[offset + 2] =
11221 ((b as u16 * a + data[offset + 2] as u16 * inv_a + 127) / 255) as u8;
11222 data[offset + 3] = ((a + data[offset + 3] as u16 * inv_a / 255).min(255)) as u8;
11223 }
11224 }
11225 }
11226 }
11227
11228 if let Some(buf) = cmyk_buf {
11230 let pw = pixmap.width();
11231 let inv_sx = 1.0 / scale_x as f64;
11232 let inv_sy = 1.0 / scale_y as f64;
11233 let axis_x = params.x1 - params.x0;
11234 let axis_y = params.y1 - params.y0;
11235 let axis_len_sq = axis_x * axis_x + axis_y * axis_y;
11236 let Some(inv_ctm) = params.ctm.invert() else {
11237 return;
11238 };
11239
11240 let iy_min = ry_min.floor() as u32;
11241 let iy_max = ry_max.ceil().min(pixmap.height() as f32) as u32;
11242 let ix_min = rx_min.floor() as u32;
11243 let ix_max = rx_max.ceil().min(pw as f32) as u32;
11244
11245 for py in iy_min..iy_max {
11246 let dev_y = py as f64 * inv_sy + vp_y as f64;
11247 for px in ix_min..ix_max {
11248 let dev_x = px as f64 * inv_sx + vp_x as f64;
11249 let (ux, uy) = inv_ctm.transform_point(dev_x, dev_y);
11250 let t = if axis_len_sq > 1e-10 {
11251 ((ux - params.x0) * axis_x + (uy - params.y0) * axis_y) / axis_len_sq
11252 } else {
11253 0.0
11254 };
11255 if t < 0.0 && !params.extend_start {
11256 continue;
11257 }
11258 if t > 1.0 && !params.extend_end {
11259 continue;
11260 }
11261 let clamped = t.clamp(0.0, 1.0);
11262
11263 if let Some(mask) = clip_mask {
11264 let mi = py as usize * pw as usize + px as usize;
11265 if mask.data()[mi] == 0 {
11266 continue;
11267 }
11268 }
11269
11270 let color = interpolate_color_stops(¶ms.color_stops, clamped);
11271 let cmyk = interpolate_cmyk_from_stops(
11272 ¶ms.color_stops,
11273 ¶ms.color_space,
11274 clamped,
11275 &color,
11276 icc,
11277 );
11278 let ci = (py as usize * pw as usize + px as usize) * 4;
11279 if ci + 3 < buf.len() {
11280 if params.spot_tint_blend && params.overprint {
11281 let cur_c = buf[ci] as f64;
11314 let cur_m = buf[ci + 1] as f64;
11315 let cur_y = buf[ci + 2] as f64;
11316 let cur_k = buf[ci + 3] as f64;
11317 let cur_is_zero =
11318 cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
11319 let named = params.painted_channels;
11320 if cur_is_zero {
11321 if named & stet_graphics::device::CMYK_C != 0 {
11322 buf[ci] = cmyk.0 as f32;
11323 }
11324 if named & stet_graphics::device::CMYK_M != 0 {
11325 buf[ci + 1] = cmyk.1 as f32;
11326 }
11327 if named & stet_graphics::device::CMYK_Y != 0 {
11328 buf[ci + 2] = cmyk.2 as f32;
11329 }
11330 if named & stet_graphics::device::CMYK_K != 0 {
11331 buf[ci + 3] = cmyk.3 as f32;
11332 }
11333 } else {
11334 let new_c = if named & stet_graphics::device::CMYK_C != 0 {
11335 cmyk.0
11336 } else {
11337 cur_c
11338 };
11339 let new_m = if named & stet_graphics::device::CMYK_M != 0 {
11340 cmyk.1
11341 } else {
11342 cur_m
11343 };
11344 let new_y = if named & stet_graphics::device::CMYK_Y != 0 {
11345 cmyk.2
11346 } else {
11347 cur_y
11348 };
11349 let new_k = if named & stet_graphics::device::CMYK_K != 0 {
11350 cmyk.3
11351 } else {
11352 cur_k
11353 };
11354 buf[ci] = new_c as f32;
11355 buf[ci + 1] = new_m as f32;
11356 buf[ci + 2] = new_y as f32;
11357 buf[ci + 3] = new_k as f32;
11358 let (rv, gv, bv) = if let Some(icc_cache) = icc {
11359 icc_cache
11360 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
11361 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
11362 } else {
11363 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
11364 };
11365 let stride = pixmap.data().len() / pixmap.height() as usize;
11366 let offset = py as usize * stride + px as usize * 4;
11367 let data = pixmap.data_mut();
11368 data[offset] = (rv * 255.0).round().clamp(0.0, 255.0) as u8;
11369 data[offset + 1] = (gv * 255.0).round().clamp(0.0, 255.0) as u8;
11370 data[offset + 2] = (bv * 255.0).round().clamp(0.0, 255.0) as u8;
11371 }
11372 } else if params.overprint
11373 && params.painted_channels != stet_graphics::device::CMYK_ALL
11374 {
11375 if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
11376 buf[ci] = cmyk.0 as f32;
11377 }
11378 if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
11379 buf[ci + 1] = cmyk.1 as f32;
11380 }
11381 if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
11382 buf[ci + 2] = cmyk.2 as f32;
11383 }
11384 if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
11385 buf[ci + 3] = cmyk.3 as f32;
11386 }
11387 let c = buf[ci] as f64;
11389 let m = buf[ci + 1] as f64;
11390 let y = buf[ci + 2] as f64;
11391 let k = buf[ci + 3] as f64;
11392 let (rv, gv, bv) = if let Some(icc_cache) = icc {
11393 icc_cache
11394 .convert_cmyk_readonly(c, m, y, k)
11395 .unwrap_or_else(|| cmyk_to_rgb_plrm(c, m, y, k))
11396 } else {
11397 cmyk_to_rgb_plrm(c, m, y, k)
11398 };
11399 let stride = pixmap.data().len() / pixmap.height() as usize;
11400 let offset = py as usize * stride + px as usize * 4;
11401 let data = pixmap.data_mut();
11402 data[offset] = (rv * 255.0).round().clamp(0.0, 255.0) as u8;
11403 data[offset + 1] = (gv * 255.0).round().clamp(0.0, 255.0) as u8;
11404 data[offset + 2] = (bv * 255.0).round().clamp(0.0, 255.0) as u8;
11405 } else {
11406 buf[ci] = cmyk.0 as f32;
11423 buf[ci + 1] = cmyk.1 as f32;
11424 buf[ci + 2] = cmyk.2 as f32;
11425 buf[ci + 3] = cmyk.3 as f32;
11426 }
11427 }
11428 }
11429 }
11430 }
11431}
11432
11433#[allow(clippy::too_many_arguments)]
11435fn render_radial_shading(
11436 pixmap: &mut Pixmap,
11437 params: &RadialShadingParams,
11438 vp_x: f32,
11439 vp_y: f32,
11440 scale_x: f32,
11441 scale_y: f32,
11442 clip_mask: Option<&Mask>,
11443 _no_aa: bool,
11444 mut cmyk_buf: Option<&mut [f32]>,
11445 icc: Option<&IccCache>,
11446) {
11447 let pw = pixmap.width();
11448 let ph = pixmap.height();
11449 if params.color_stops.is_empty() || pw == 0 || ph == 0 {
11450 return;
11451 }
11452
11453 let Some(inv_ctm) = params.ctm.invert() else {
11454 return;
11455 };
11456
11457 let (px_min, py_min, px_max, py_max) = if let Some(bbox) = ¶ms.bbox {
11458 let corners = [
11459 params.ctm.transform_point(bbox[0], bbox[1]),
11460 params.ctm.transform_point(bbox[2], bbox[1]),
11461 params.ctm.transform_point(bbox[0], bbox[3]),
11462 params.ctm.transform_point(bbox[2], bbox[3]),
11463 ];
11464 let x_min = corners
11465 .iter()
11466 .map(|c| c.0 as f32)
11467 .fold(f32::INFINITY, f32::min);
11468 let y_min = corners
11469 .iter()
11470 .map(|c| c.1 as f32)
11471 .fold(f32::INFINITY, f32::min);
11472 let x_max = corners
11473 .iter()
11474 .map(|c| c.0 as f32)
11475 .fold(f32::NEG_INFINITY, f32::max);
11476 let y_max = corners
11477 .iter()
11478 .map(|c| c.1 as f32)
11479 .fold(f32::NEG_INFINITY, f32::max);
11480 (
11481 ((x_min - vp_x) * scale_x).max(0.0) as u32,
11482 ((y_min - vp_y) * scale_y).max(0.0) as u32,
11483 (((x_max - vp_x) * scale_x).ceil() as u32).min(pw),
11484 (((y_max - vp_y) * scale_y).ceil() as u32).min(ph),
11485 )
11486 } else {
11487 (0, 0, pw, ph)
11488 };
11489
11490 let inv_sx = 1.0 / scale_x as f64;
11491 let inv_sy = 1.0 / scale_y as f64;
11492
11493 let rotated_bbox = if let Some(bbox) = ¶ms.bbox {
11495 if params.ctm.b.abs() > 1e-10 || params.ctm.c.abs() > 1e-10 {
11496 let (bx0, bx1) = (bbox[0].min(bbox[2]), bbox[0].max(bbox[2]));
11497 let (by0, by1) = (bbox[1].min(bbox[3]), bbox[1].max(bbox[3]));
11498 Some((bx0, by0, bx1, by1))
11499 } else {
11500 None
11501 }
11502 } else {
11503 None
11504 };
11505
11506 let data = pixmap.data_mut();
11507 let stride = pw as usize * 4;
11508
11509 for py in py_min..py_max {
11510 let dev_y = py as f64 * inv_sy + vp_y as f64;
11511 for px in px_min..px_max {
11512 let dev_x = px as f64 * inv_sx + vp_x as f64;
11513 let (ux, uy) = inv_ctm.transform_point(dev_x, dev_y);
11514
11515 if let Some((bx0, by0, bx1, by1)) = rotated_bbox {
11517 if ux < bx0 || ux > bx1 || uy < by0 || uy > by1 {
11518 continue;
11519 }
11520 }
11521
11522 let t = solve_radial_t(
11523 ux,
11524 uy,
11525 params.x0,
11526 params.y0,
11527 params.r0,
11528 params.x1,
11529 params.y1,
11530 params.r1,
11531 params.extend_start,
11532 params.extend_end,
11533 );
11534 if let Some(t) = t {
11535 let clamped = t.clamp(0.0, 1.0);
11536 let color = interpolate_color_stops(¶ms.color_stops, clamped);
11537
11538 let clipped = clip_mask
11539 .is_some_and(|mask| mask.data()[py as usize * pw as usize + px as usize] == 0);
11540
11541 if clipped {
11542 continue;
11543 }
11544
11545 let cmyk = interpolate_cmyk_from_stops(
11554 ¶ms.color_stops,
11555 ¶ms.color_space,
11556 clamped,
11557 &color,
11558 icc,
11559 );
11560 let ci = (py as usize * pw as usize + px as usize) * 4;
11561 let buffer_clean = if let Some(ref buf) = cmyk_buf {
11562 if ci + 3 < buf.len() {
11563 buf[ci] == 0.0
11564 && buf[ci + 1] == 0.0
11565 && buf[ci + 2] == 0.0
11566 && buf[ci + 3] == 0.0
11567 } else {
11568 false
11569 }
11570 } else {
11571 false
11572 };
11573 let offset_for_check = py as usize * stride + px as usize * 4;
11574 let pixmap_has_colour = data[offset_for_check + 3] > 0
11575 && (data[offset_for_check] < 250
11576 || data[offset_for_check + 1] < 250
11577 || data[offset_for_check + 2] < 250);
11578 let use_multiplicative = params.overprint
11579 && params.painted_channels != stet_graphics::device::CMYK_ALL
11580 && buffer_clean
11581 && pixmap_has_colour;
11582
11583 if let Some(ref mut buf) = cmyk_buf
11585 && ci + 3 < buf.len()
11586 {
11587 if params.overprint
11588 && params.painted_channels != stet_graphics::device::CMYK_ALL
11589 {
11590 if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
11591 buf[ci] = cmyk.0 as f32;
11592 }
11593 if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
11594 buf[ci + 1] = cmyk.1 as f32;
11595 }
11596 if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
11597 buf[ci + 2] = cmyk.2 as f32;
11598 }
11599 if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
11600 buf[ci + 3] = cmyk.3 as f32;
11601 }
11602 } else {
11603 buf[ci] = cmyk.0 as f32;
11604 buf[ci + 1] = cmyk.1 as f32;
11605 buf[ci + 2] = cmyk.2 as f32;
11606 buf[ci + 3] = cmyk.3 as f32;
11607 }
11608 }
11609
11610 let offset = py as usize * stride + px as usize * 4;
11611 if use_multiplicative {
11612 let bg_r = data[offset] as f64 / 255.0;
11617 let bg_g = data[offset + 1] as f64 / 255.0;
11618 let bg_b = data[offset + 2] as f64 / 255.0;
11619 let over_r = if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
11620 1.0 - cmyk.0
11621 } else {
11622 1.0
11623 };
11624 let over_g = if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
11625 1.0 - cmyk.1
11626 } else {
11627 1.0
11628 };
11629 let over_b = if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
11630 1.0 - cmyk.2
11631 } else {
11632 1.0
11633 };
11634 let k_fac = if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
11635 1.0 - cmyk.3
11636 } else {
11637 1.0
11638 };
11639 data[offset] = ((bg_r * over_r * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
11640 data[offset + 1] =
11641 ((bg_g * over_g * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
11642 data[offset + 2] =
11643 ((bg_b * over_b * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
11644 data[offset + 3] = 255;
11645 } else {
11646 data[offset] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
11647 data[offset + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
11648 data[offset + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
11649 data[offset + 3] = 255;
11650
11651 if params.overprint
11666 && params.painted_channels != stet_graphics::device::CMYK_ALL
11667 && matches!(params.color_space, ShadingColorSpace::DeviceCMYK)
11668 && let Some(ref mut buf) = cmyk_buf
11669 && ci + 3 < buf.len()
11670 && let Some(icc_cache) = icc
11671 {
11672 let c = buf[ci] as f64;
11673 let m = buf[ci + 1] as f64;
11674 let y = buf[ci + 2] as f64;
11675 let k = buf[ci + 3] as f64;
11676 if let Some((r, g, b)) = icc_cache.convert_cmyk_readonly(c, m, y, k) {
11677 data[offset] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
11678 data[offset + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
11679 data[offset + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
11680 }
11681 }
11682 }
11683 }
11684 }
11685 }
11686}
11687#[allow(clippy::too_many_arguments)]
11692fn solve_radial_t(
11693 px: f64,
11694 py: f64,
11695 x0: f64,
11696 y0: f64,
11697 r0: f64,
11698 x1: f64,
11699 y1: f64,
11700 r1: f64,
11701 extend_start: bool,
11702 extend_end: bool,
11703) -> Option<f64> {
11704 let cdx = x1 - x0;
11707 let cdy = y1 - y0;
11708 let dr = r1 - r0;
11709
11710 let a = cdx * cdx + cdy * cdy - dr * dr;
11711 let dpx = px - x0;
11712 let dpy = py - y0;
11713 let b = -2.0 * (dpx * cdx + dpy * cdy + r0 * dr);
11714 let c = dpx * dpx + dpy * dpy - r0 * r0;
11715
11716 let in_domain = |t: f64| -> bool {
11718 (0.0..=1.0).contains(&t) || (t < 0.0 && extend_start) || (t > 1.0 && extend_end)
11719 };
11720
11721 if a.abs() < 1e-10 {
11722 if b.abs() < 1e-10 {
11724 return None;
11725 }
11726 let t = -c / b;
11727 let radius = r0 + t * dr;
11728 if radius >= 0.0 && in_domain(t) {
11729 return Some(t);
11730 }
11731 return None;
11732 }
11733
11734 let discriminant = b * b - 4.0 * a * c;
11735 if discriminant < 0.0 {
11736 return None;
11737 }
11738 let sqrt_d = discriminant.sqrt();
11739 let t1 = (-b + sqrt_d) / (2.0 * a);
11740 let t2 = (-b - sqrt_d) / (2.0 * a);
11741
11742 let mut best: Option<f64> = None;
11744 for t in [t1, t2] {
11745 let radius = r0 + t * dr;
11746 if radius >= 0.0 && in_domain(t) {
11747 best = Some(match best {
11748 Some(prev) => prev.max(t),
11749 None => t,
11750 });
11751 }
11752 }
11753 best
11754}
11755
11756#[allow(clippy::too_many_arguments)]
11758fn render_mesh_shading(
11759 pixmap: &mut Pixmap,
11760 params: &MeshShadingParams,
11761 vp_x: f32,
11762 vp_y: f32,
11763 scale_x: f32,
11764 scale_y: f32,
11765 clip_mask: Option<&Mask>,
11766 mut cmyk_buf: Option<&mut [f32]>,
11767 icc: Option<&IccCache>,
11768) {
11769 let pw = pixmap.width() as usize;
11770 let ph = pixmap.height() as usize;
11771 if pw == 0 || ph == 0 {
11772 return;
11773 }
11774 let data = pixmap.data_mut();
11775 let stride = pw * 4;
11776
11777 let lut = params.color_lut.as_deref();
11778
11779 for tri in ¶ms.triangles {
11780 let (dx0, dy0) = params.ctm.transform_point(tri.v0.x, tri.v0.y);
11781 let (dx1, dy1) = params.ctm.transform_point(tri.v1.x, tri.v1.y);
11782 let (dx2, dy2) = params.ctm.transform_point(tri.v2.x, tri.v2.y);
11783
11784 let x0 = (dx0 as f32 - vp_x) * scale_x;
11785 let y0 = (dy0 as f32 - vp_y) * scale_y;
11786 let x1 = (dx1 as f32 - vp_x) * scale_x;
11787 let y1 = (dy1 as f32 - vp_y) * scale_y;
11788 let x2 = (dx2 as f32 - vp_x) * scale_x;
11789 let y2 = (dy2 as f32 - vp_y) * scale_y;
11790
11791 let min_x = (x0.min(x1).min(x2).floor().max(0.0)) as usize;
11792 let max_x = (x0.max(x1).max(x2).ceil() as usize).min(pw);
11793 let min_y = (y0.min(y1).min(y2).floor().max(0.0)) as usize;
11794 let max_y = (y0.max(y1).max(y2).ceil() as usize).min(ph);
11795
11796 if min_x >= max_x || min_y >= max_y {
11797 continue;
11798 }
11799
11800 let x0 = x0 as f64;
11801 let y0 = y0 as f64;
11802 let x1 = x1 as f64;
11803 let y1 = y1 as f64;
11804 let x2 = x2 as f64;
11805 let y2 = y2 as f64;
11806 let denom = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2);
11811 if denom.abs() < 1e-10 {
11812 continue;
11813 }
11814 let (x1, y1, x2, y2) = if denom < 0.0 {
11815 (x2, y2, x1, y1)
11816 } else {
11817 (x1, y1, x2, y2)
11818 };
11819 let (v1_ref, v2_ref) = if denom < 0.0 {
11820 (&tri.v2, &tri.v1)
11821 } else {
11822 (&tri.v1, &tri.v2)
11823 };
11824 let denom = denom.abs();
11825 let inv_denom = 1.0 / denom;
11826
11827 for py in min_y..max_y {
11828 for px in min_x..max_x {
11829 let pxf = px as f64 + 0.5;
11830 let pyf = py as f64 + 0.5;
11831
11832 let w0 = ((y1 - y2) * (pxf - x2) + (x2 - x1) * (pyf - y2)) * inv_denom;
11833 let w1 = ((y2 - y0) * (pxf - x2) + (x0 - x2) * (pyf - y2)) * inv_denom;
11834 let w2 = 1.0 - w0 - w1;
11835
11836 if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 {
11837 continue;
11838 }
11839
11840 let clipped = clip_mask.is_some_and(|mask| mask.data()[py * pw + px] == 0);
11841
11842 let w0c = w0.max(0.0);
11843 let w1c = w1.max(0.0);
11844 let w2c = w2.max(0.0);
11845 let wsum = w0c + w1c + w2c;
11846 let w0n = w0c / wsum;
11847 let w1n = w1c / wsum;
11848 let w2n = w2c / wsum;
11849
11850 let (r, g, b) = if let Some(lut) = lut {
11853 let raw = w0n * tri.v0.raw_components[0]
11855 + w1n * v1_ref.raw_components[0]
11856 + w2n * v2_ref.raw_components[0];
11857 let raw = raw.clamp(0.0, 1.0);
11858 let fi = raw * (lut.len() - 1) as f64;
11860 let i0 = (fi as usize).min(lut.len().saturating_sub(2));
11861 let frac = fi - i0 as f64;
11862 let c0 = &lut[i0];
11863 let c1 = &lut[i0 + 1];
11864 (
11865 c0.r + frac * (c1.r - c0.r),
11866 c0.g + frac * (c1.g - c0.g),
11867 c0.b + frac * (c1.b - c0.b),
11868 )
11869 } else {
11870 (
11871 w0n * tri.v0.color.r + w1n * v1_ref.color.r + w2n * v2_ref.color.r,
11872 w0n * tri.v0.color.g + w1n * v1_ref.color.g + w2n * v2_ref.color.g,
11873 w0n * tri.v0.color.b + w1n * v1_ref.color.b + w2n * v2_ref.color.b,
11874 )
11875 };
11876
11877 if let Some(ref mut buf) = cmyk_buf {
11879 let ci = (py * pw + px) * 4;
11880 if ci + 3 < buf.len() {
11881 let cmyk = interpolate_cmyk_from_vertices(
11882 &tri.v0,
11883 v1_ref,
11884 v2_ref,
11885 w0n,
11886 w1n,
11887 w2n,
11888 ¶ms.color_space,
11889 r,
11890 g,
11891 b,
11892 icc,
11893 );
11894 if params.overprint
11895 && params.painted_channels != stet_graphics::device::CMYK_ALL
11896 {
11897 if !clipped {
11898 if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
11899 buf[ci] = cmyk.0 as f32;
11900 }
11901 if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
11902 buf[ci + 1] = cmyk.1 as f32;
11903 }
11904 if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
11905 buf[ci + 2] = cmyk.2 as f32;
11906 }
11907 if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
11908 buf[ci + 3] = cmyk.3 as f32;
11909 }
11910 }
11911 } else {
11912 buf[ci] = cmyk.0 as f32;
11913 buf[ci + 1] = cmyk.1 as f32;
11914 buf[ci + 2] = cmyk.2 as f32;
11915 buf[ci + 3] = cmyk.3 as f32;
11916 }
11917 }
11918 }
11919
11920 if clipped {
11921 continue;
11922 }
11923
11924 let offset = py * stride + px * 4;
11925 data[offset] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
11926 data[offset + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
11927 data[offset + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
11928 data[offset + 3] = 255;
11929 }
11930 }
11931 }
11932}
11933
11934#[allow(clippy::too_many_arguments)]
11936fn render_patch_shading(
11937 pixmap: &mut Pixmap,
11938 params: &PatchShadingParams,
11939 vp_x: f32,
11940 vp_y: f32,
11941 scale_x: f32,
11942 scale_y: f32,
11943 clip_mask: Option<&Mask>,
11944 cmyk_buf: Option<&mut [f32]>,
11945 icc: Option<&IccCache>,
11946) {
11947 let mut triangles = Vec::new();
11948 let scale = scale_x.max(scale_y) as f64;
11949 for patch in ¶ms.patches {
11950 if patch.points.len() >= 12 {
11951 let mut x_min = f64::INFINITY;
11953 let mut y_min = f64::INFINITY;
11954 let mut x_max = f64::NEG_INFINITY;
11955 let mut y_max = f64::NEG_INFINITY;
11956 for &(px, py) in &patch.points {
11957 let (dx, dy) = params.ctm.transform_point(px, py);
11958 x_min = x_min.min(dx);
11959 y_min = y_min.min(dy);
11960 x_max = x_max.max(dx);
11961 y_max = y_max.max(dy);
11962 }
11963 let extent = (x_max - x_min).max(y_max - y_min).abs() * scale;
11964 let n = (extent / 2.0).ceil().clamp(8.0, 64.0) as usize;
11966 let icc_profile_hash = match ¶ms.color_space {
11968 stet_graphics::device::ShadingColorSpace::ICCBased { profile_hash, .. } => {
11969 Some(profile_hash)
11970 }
11971 _ => None,
11972 };
11973 subdivide_patch_to_triangles(patch, &mut triangles, n, icc_profile_hash, icc);
11974 }
11975 }
11976 if !triangles.is_empty() {
11977 let mesh_params = MeshShadingParams {
11978 triangles,
11979 ctm: params.ctm,
11980 bbox: params.bbox,
11981 color_space: params.color_space.clone(),
11982 overprint: params.overprint,
11983 painted_channels: params.painted_channels,
11984 color_lut: params.color_lut.clone(),
11985 };
11986 render_mesh_shading(
11987 pixmap,
11988 &mesh_params,
11989 vp_x,
11990 vp_y,
11991 scale_x,
11992 scale_y,
11993 clip_mask,
11994 cmyk_buf,
11995 icc,
11996 );
11997 }
11998}
11999fn subdivide_patch_to_triangles(
12004 patch: &stet_graphics::device::ShadingPatch,
12005 triangles: &mut Vec<stet_graphics::device::ShadingTriangle>,
12006 n: usize,
12007 icc_profile_hash: Option<&stet_graphics::icc::ProfileHash>,
12008 icc_cache: Option<&IccCache>,
12009) {
12010 let mut grid: Vec<(f64, f64, DeviceColor, Vec<f64>)> = Vec::with_capacity((n + 1) * (n + 1));
12014 let use_tensor = patch.points.len() >= 16;
12015 let has_raw = !patch.raw_colors[0].is_empty();
12016 let use_icc_interp = has_raw && icc_profile_hash.is_some() && icc_cache.is_some();
12018
12019 for row in 0..=n {
12020 let v = row as f64 / n as f64;
12021 for col in 0..=n {
12022 let u = col as f64 / n as f64;
12023 let (x, y) = if use_tensor {
12024 eval_tensor_patch(patch, u, v)
12025 } else {
12026 eval_coons_patch(patch, u, v)
12027 };
12028 let raw = if has_raw {
12029 bilinear_raw(&patch.raw_colors, u, v)
12030 } else {
12031 vec![]
12032 };
12033 let color = if use_icc_interp {
12039 if let Some((r, g, b)) = icc_cache
12040 .unwrap()
12041 .convert_color_readonly(icc_profile_hash.unwrap(), &raw)
12042 {
12043 DeviceColor::from_rgb(r, g, b)
12044 } else {
12045 bilinear_color(&patch.colors, u, v)
12046 }
12047 } else {
12048 bilinear_color(&patch.colors, u, v)
12049 };
12050 grid.push((x, y, color, raw));
12051 }
12052 }
12053
12054 let cols = n + 1;
12056 for row in 0..n {
12057 for col in 0..n {
12058 let i00 = row * cols + col;
12059 let i10 = i00 + 1;
12060 let i01 = i00 + cols;
12061 let i11 = i01 + 1;
12062
12063 let (x00, y00, c00, r00) = &grid[i00];
12064 let (x10, y10, c10, r10) = &grid[i10];
12065 let (x01, y01, c01, r01) = &grid[i01];
12066 let (x11, y11, c11, r11) = &grid[i11];
12067
12068 use stet_graphics::device::ShadingVertex;
12069 triangles.push(stet_graphics::device::ShadingTriangle {
12070 v0: ShadingVertex {
12071 x: *x00,
12072 y: *y00,
12073 color: c00.clone(),
12074 raw_components: r00.clone(),
12075 },
12076 v1: ShadingVertex {
12077 x: *x10,
12078 y: *y10,
12079 color: c10.clone(),
12080 raw_components: r10.clone(),
12081 },
12082 v2: ShadingVertex {
12083 x: *x01,
12084 y: *y01,
12085 color: c01.clone(),
12086 raw_components: r01.clone(),
12087 },
12088 });
12089 triangles.push(stet_graphics::device::ShadingTriangle {
12090 v0: ShadingVertex {
12091 x: *x10,
12092 y: *y10,
12093 color: c10.clone(),
12094 raw_components: r10.clone(),
12095 },
12096 v1: ShadingVertex {
12097 x: *x11,
12098 y: *y11,
12099 color: c11.clone(),
12100 raw_components: r11.clone(),
12101 },
12102 v2: ShadingVertex {
12103 x: *x01,
12104 y: *y01,
12105 color: c01.clone(),
12106 raw_components: r01.clone(),
12107 },
12108 });
12109 }
12110 }
12111}
12112
12113fn eval_coons_patch(patch: &stet_graphics::device::ShadingPatch, u: f64, v: f64) -> (f64, f64) {
12116 let pts = &patch.points;
12117 if pts.len() < 12 {
12118 return (0.0, 0.0);
12119 }
12120
12121 let c0 = eval_cubic_bezier(pts[0], pts[1], pts[2], pts[3], u);
12126 let c2 = eval_cubic_bezier(pts[6], pts[7], pts[8], pts[9], 1.0 - u);
12127 let d0 = eval_cubic_bezier(pts[0], pts[11], pts[10], pts[9], v);
12128 let d1 = eval_cubic_bezier(pts[3], pts[4], pts[5], pts[6], v);
12129
12130 let p00 = pts[0];
12132 let p10 = pts[3];
12133 let p01 = pts[9];
12134 let p11 = pts[6];
12135 let bx = (1.0 - u) * (1.0 - v) * p00.0
12136 + u * (1.0 - v) * p10.0
12137 + (1.0 - u) * v * p01.0
12138 + u * v * p11.0;
12139 let by = (1.0 - u) * (1.0 - v) * p00.1
12140 + u * (1.0 - v) * p10.1
12141 + (1.0 - u) * v * p01.1
12142 + u * v * p11.1;
12143
12144 let x = (1.0 - v) * c0.0 + v * c2.0 + (1.0 - u) * d0.0 + u * d1.0 - bx;
12146 let y = (1.0 - v) * c0.1 + v * c2.1 + (1.0 - u) * d0.1 + u * d1.1 - by;
12147
12148 (x, y)
12149}
12150
12151fn eval_tensor_patch(patch: &stet_graphics::device::ShadingPatch, u: f64, v: f64) -> (f64, f64) {
12165 let pts = &patch.points;
12166
12167 let grid: [[usize; 4]; 4] = [[0, 1, 2, 3], [11, 12, 13, 4], [10, 15, 14, 5], [9, 8, 7, 6]];
12171
12172 let su = 1.0 - u;
12174 let bu = [su * su * su, 3.0 * su * su * u, 3.0 * su * u * u, u * u * u];
12175 let sv = 1.0 - v;
12176 let bv = [sv * sv * sv, 3.0 * sv * sv * v, 3.0 * sv * v * v, v * v * v];
12177
12178 let mut x = 0.0;
12179 let mut y = 0.0;
12180 for j in 0..4 {
12181 for i in 0..4 {
12182 let w = bu[i] * bv[j];
12183 let p = pts[grid[j][i]];
12184 x += w * p.0;
12185 y += w * p.1;
12186 }
12187 }
12188 (x, y)
12189}
12190
12191fn eval_cubic_bezier(
12193 p0: (f64, f64),
12194 p1: (f64, f64),
12195 p2: (f64, f64),
12196 p3: (f64, f64),
12197 t: f64,
12198) -> (f64, f64) {
12199 let s = 1.0 - t;
12200 let s2 = s * s;
12201 let t2 = t * t;
12202 let b0 = s2 * s;
12203 let b1 = 3.0 * s2 * t;
12204 let b2 = 3.0 * s * t2;
12205 let b3 = t2 * t;
12206 (
12207 b0 * p0.0 + b1 * p1.0 + b2 * p2.0 + b3 * p3.0,
12208 b0 * p0.1 + b1 * p1.1 + b2 * p2.1 + b3 * p3.1,
12209 )
12210}
12211
12212fn bilinear_color(colors: &[DeviceColor; 4], u: f64, v: f64) -> DeviceColor {
12214 let r = (1.0 - u) * (1.0 - v) * colors[0].r
12215 + u * (1.0 - v) * colors[1].r
12216 + (1.0 - u) * v * colors[3].r
12217 + u * v * colors[2].r;
12218 let g = (1.0 - u) * (1.0 - v) * colors[0].g
12219 + u * (1.0 - v) * colors[1].g
12220 + (1.0 - u) * v * colors[3].g
12221 + u * v * colors[2].g;
12222 let b = (1.0 - u) * (1.0 - v) * colors[0].b
12223 + u * (1.0 - v) * colors[1].b
12224 + (1.0 - u) * v * colors[3].b
12225 + u * v * colors[2].b;
12226 DeviceColor::from_rgb(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0))
12227}
12228
12229fn bilinear_raw(raw_colors: &[Vec<f64>; 4], u: f64, v: f64) -> Vec<f64> {
12231 let n = raw_colors[0].len();
12232 let mut result = vec![0.0; n];
12233 for i in 0..n {
12234 result[i] = (1.0 - u) * (1.0 - v) * raw_colors[0][i]
12235 + u * (1.0 - v) * raw_colors[1][i]
12236 + (1.0 - u) * v * raw_colors[3][i]
12237 + u * v * raw_colors[2][i];
12238 }
12239 result
12240}
12241
12242fn build_gradient_lut(stops: &[stet_graphics::device::ColorStop], size: usize) -> Vec<[u8; 4]> {
12248 let size = size.max(2);
12249 let mut lut = vec![[0u8; 4]; size];
12250 if stops.is_empty() {
12251 return lut;
12252 }
12253 let mut si = 0usize; let last = (size - 1) as f64;
12255 for i in 0..size {
12256 let t = i as f64 / last;
12257 while si + 1 < stops.len() && stops[si + 1].position < t {
12259 si += 1;
12260 }
12261 let (r, g, b) = if si + 1 >= stops.len() {
12262 let c = &stops[stops.len() - 1].color;
12263 (c.r, c.g, c.b)
12264 } else if t <= stops[si].position {
12265 let c = &stops[si].color;
12266 (c.r, c.g, c.b)
12267 } else {
12268 let t0 = stops[si].position;
12269 let t1 = stops[si + 1].position;
12270 let frac = if (t1 - t0).abs() < 1e-10 {
12271 0.0
12272 } else {
12273 (t - t0) / (t1 - t0)
12274 };
12275 let c0 = &stops[si].color;
12276 let c1 = &stops[si + 1].color;
12277 (
12278 c0.r + frac * (c1.r - c0.r),
12279 c0.g + frac * (c1.g - c0.g),
12280 c0.b + frac * (c1.b - c0.b),
12281 )
12282 };
12283 lut[i] = [
12284 (r * 255.0).round().clamp(0.0, 255.0) as u8,
12285 (g * 255.0).round().clamp(0.0, 255.0) as u8,
12286 (b * 255.0).round().clamp(0.0, 255.0) as u8,
12287 255,
12288 ];
12289 }
12290 lut
12291}
12292
12293fn build_gradient_stops(
12295 stops: &[stet_graphics::device::ColorStop],
12296) -> Vec<stet_tiny_skia::GradientStop> {
12297 let mut result = Vec::with_capacity(stops.len());
12298 for stop in stops {
12299 let r = (stop.color.r * 255.0).round().clamp(0.0, 255.0) as u8;
12300 let g = (stop.color.g * 255.0).round().clamp(0.0, 255.0) as u8;
12301 let b = (stop.color.b * 255.0).round().clamp(0.0, 255.0) as u8;
12302 result.push(stet_tiny_skia::GradientStop::new(
12303 stop.position as f32,
12304 Color::from_rgba8(r, g, b, 255),
12305 ));
12306 }
12307 result
12308}
12309
12310fn interpolate_color_stops(
12312 stops: &[stet_graphics::device::ColorStop],
12313 position: f64,
12314) -> DeviceColor {
12315 if stops.is_empty() {
12316 return DeviceColor::from_gray(0.0);
12317 }
12318 if stops.len() == 1 || position <= stops[0].position {
12319 return stops[0].color.clone();
12320 }
12321 if position >= stops.last().unwrap().position {
12322 return stops.last().unwrap().color.clone();
12323 }
12324
12325 for i in 1..stops.len() {
12327 if position <= stops[i].position {
12328 let t0 = stops[i - 1].position;
12329 let t1 = stops[i].position;
12330 let frac = if (t1 - t0).abs() < 1e-10 {
12331 0.0
12332 } else {
12333 (position - t0) / (t1 - t0)
12334 };
12335 let c0 = &stops[i - 1].color;
12336 let c1 = &stops[i].color;
12337 return DeviceColor::from_rgb(
12338 (c0.r + frac * (c1.r - c0.r)).clamp(0.0, 1.0),
12339 (c0.g + frac * (c1.g - c0.g)).clamp(0.0, 1.0),
12340 (c0.b + frac * (c1.b - c0.b)).clamp(0.0, 1.0),
12341 );
12342 }
12343 }
12344
12345 stops.last().unwrap().color.clone()
12346}
12347
12348fn interpolate_cmyk_from_stops(
12360 stops: &[stet_graphics::device::ColorStop],
12361 cs: &ShadingColorSpace,
12362 t: f64,
12363 color: &DeviceColor,
12364 icc: Option<&IccCache>,
12365) -> (f64, f64, f64, f64) {
12366 let rgb_to_cmyk = |c: &DeviceColor| -> (f64, f64, f64, f64) {
12367 if let Some(cmyk) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(c.r, c.g, c.b)) {
12368 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
12369 } else {
12370 (
12371 (1.0 - c.r).clamp(0.0, 1.0),
12372 (1.0 - c.g).clamp(0.0, 1.0),
12373 (1.0 - c.b).clamp(0.0, 1.0),
12374 0.0,
12375 )
12376 }
12377 };
12378
12379 match cs {
12380 ShadingColorSpace::DeviceCMYK => {
12381 if stops.len() == 1 {
12383 let rc = &stops[0].raw_components;
12384 if rc.len() >= 4 {
12385 return (rc[0], rc[1], rc[2], rc[3]);
12386 }
12387 }
12388 let mut lo = &stops[0];
12390 let mut hi = stops.last().unwrap();
12391 for i in 0..stops.len() - 1 {
12392 if stops[i + 1].position >= t {
12393 lo = &stops[i];
12394 hi = &stops[i + 1];
12395 break;
12396 }
12397 }
12398 let span = hi.position - lo.position;
12399 let frac = if span > 1e-10 {
12400 (t - lo.position) / span
12401 } else {
12402 0.0
12403 };
12404 let frac = frac.clamp(0.0, 1.0);
12405 if lo.raw_components.len() >= 4 && hi.raw_components.len() >= 4 {
12406 (
12407 lo.raw_components[0] + frac * (hi.raw_components[0] - lo.raw_components[0]),
12408 lo.raw_components[1] + frac * (hi.raw_components[1] - lo.raw_components[1]),
12409 lo.raw_components[2] + frac * (hi.raw_components[2] - lo.raw_components[2]),
12410 lo.raw_components[3] + frac * (hi.raw_components[3] - lo.raw_components[3]),
12411 )
12412 } else {
12413 rgb_to_cmyk(color)
12414 }
12415 }
12416 _ => rgb_to_cmyk(color),
12417 }
12418}
12419
12420#[allow(clippy::too_many_arguments)]
12426fn interpolate_cmyk_from_vertices(
12427 v0: &ShadingVertex,
12428 v1: &ShadingVertex,
12429 v2: &ShadingVertex,
12430 w0: f64,
12431 w1: f64,
12432 w2: f64,
12433 cs: &ShadingColorSpace,
12434 r: f64,
12435 g: f64,
12436 b: f64,
12437 icc: Option<&IccCache>,
12438) -> (f64, f64, f64, f64) {
12439 let rgb_to_cmyk = |r: f64, g: f64, b: f64| -> (f64, f64, f64, f64) {
12440 if let Some(cmyk) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(r, g, b)) {
12441 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
12442 } else {
12443 (
12444 (1.0 - r).clamp(0.0, 1.0),
12445 (1.0 - g).clamp(0.0, 1.0),
12446 (1.0 - b).clamp(0.0, 1.0),
12447 0.0,
12448 )
12449 }
12450 };
12451
12452 match cs {
12453 ShadingColorSpace::DeviceCMYK => {
12454 if v0.raw_components.len() >= 4
12455 && v1.raw_components.len() >= 4
12456 && v2.raw_components.len() >= 4
12457 {
12458 (
12459 w0 * v0.raw_components[0]
12460 + w1 * v1.raw_components[0]
12461 + w2 * v2.raw_components[0],
12462 w0 * v0.raw_components[1]
12463 + w1 * v1.raw_components[1]
12464 + w2 * v2.raw_components[1],
12465 w0 * v0.raw_components[2]
12466 + w1 * v1.raw_components[2]
12467 + w2 * v2.raw_components[2],
12468 w0 * v0.raw_components[3]
12469 + w1 * v1.raw_components[3]
12470 + w2 * v2.raw_components[3],
12471 )
12472 } else {
12473 rgb_to_cmyk(r, g, b)
12474 }
12475 }
12476 _ => rgb_to_cmyk(r, g, b),
12477 }
12478}
12479
12480#[cfg(test)]
12481mod tests {
12482 use super::*;
12483 use stet_graphics::color::DashPattern;
12484 use stet_graphics::device::{BgUcrState, HalftoneState, TransferState};
12485
12486 #[test]
12487 fn test_create_device() {
12488 let dev = SkiaDevice::new(100, 100);
12489 assert_eq!(dev.page_size(), (100, 100));
12490 }
12491
12492 #[test]
12493 fn test_fill_rect() {
12494 let mut dev = SkiaDevice::new(100, 100);
12495 let mut path = PsPath::new();
12496 path.segments.push(PathSegment::MoveTo(10.0, 10.0));
12497 path.segments.push(PathSegment::LineTo(90.0, 10.0));
12498 path.segments.push(PathSegment::LineTo(90.0, 90.0));
12499 path.segments.push(PathSegment::LineTo(10.0, 90.0));
12500 path.segments.push(PathSegment::ClosePath);
12501
12502 let params = FillParams {
12503 color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
12504 fill_rule: FillRule::NonZeroWinding,
12505 ctm: Matrix::identity(),
12506 is_text_glyph: false,
12507 overprint: false,
12508 overprint_mode: 0,
12509 opm_paired: false,
12510 painted_channels: 0,
12511 is_device_cmyk: false,
12512 spot_color: None,
12513 rendering_intent: 0,
12514 transfer: TransferState::default(),
12515 halftone: HalftoneState::default(),
12516 bg_ucr: BgUcrState::default(),
12517 alpha: 1.0,
12518 blend_mode: 0,
12519 };
12520 dev.fill_path(&path, ¶ms);
12521
12522 let pixel = dev.pixmap().pixel(50, 50).unwrap();
12524 assert_eq!(pixel.red(), 255);
12525 assert_eq!(pixel.green(), 0);
12526 assert_eq!(pixel.blue(), 0);
12527 }
12528
12529 #[test]
12530 fn test_stroke_line() {
12531 let mut dev = SkiaDevice::new(100, 100);
12532 let mut path = PsPath::new();
12533 path.segments.push(PathSegment::MoveTo(10.0, 50.0));
12534 path.segments.push(PathSegment::LineTo(90.0, 50.0));
12535
12536 let params = StrokeParams {
12537 color: DeviceColor::from_rgb(0.0, 0.0, 1.0),
12538 line_width: 4.0,
12539 line_cap: LineCap::Butt,
12540 line_join: LineJoin::Miter,
12541 miter_limit: 10.0,
12542 dash_pattern: DashPattern::solid(),
12543 ctm: Matrix::identity(),
12544 stroke_adjust: false,
12545 is_text_glyph: false,
12546 overprint: false,
12547 overprint_mode: 0,
12548 opm_paired: false,
12549 painted_channels: 0,
12550 is_device_cmyk: false,
12551 spot_color: None,
12552 rendering_intent: 0,
12553 transfer: TransferState::default(),
12554 halftone: HalftoneState::default(),
12555 bg_ucr: BgUcrState::default(),
12556 alpha: 1.0,
12557 blend_mode: 0,
12558 };
12559 dev.stroke_path(&path, ¶ms);
12560
12561 let pixel = dev.pixmap().pixel(50, 50).unwrap();
12563 assert_eq!(pixel.blue(), 255);
12564 }
12565
12566 #[test]
12567 fn test_clip() {
12568 let mut dev = SkiaDevice::new(100, 100);
12569
12570 let mut clip_path = PsPath::new();
12572 clip_path.segments.push(PathSegment::MoveTo(0.0, 0.0));
12573 clip_path.segments.push(PathSegment::LineTo(50.0, 0.0));
12574 clip_path.segments.push(PathSegment::LineTo(50.0, 100.0));
12575 clip_path.segments.push(PathSegment::LineTo(0.0, 100.0));
12576 clip_path.segments.push(PathSegment::ClosePath);
12577
12578 let clip_params = ClipParams {
12579 fill_rule: FillRule::NonZeroWinding,
12580 ctm: Matrix::identity(),
12581 stroke_params: None,
12582 };
12583 dev.clip_path(&clip_path, &clip_params);
12584
12585 let mut fill_path = PsPath::new();
12587 fill_path.segments.push(PathSegment::MoveTo(0.0, 0.0));
12588 fill_path.segments.push(PathSegment::LineTo(100.0, 0.0));
12589 fill_path.segments.push(PathSegment::LineTo(100.0, 100.0));
12590 fill_path.segments.push(PathSegment::LineTo(0.0, 100.0));
12591 fill_path.segments.push(PathSegment::ClosePath);
12592
12593 let fill_params = FillParams {
12594 color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
12595 fill_rule: FillRule::NonZeroWinding,
12596 ctm: Matrix::identity(),
12597 is_text_glyph: false,
12598 overprint: false,
12599 overprint_mode: 0,
12600 opm_paired: false,
12601 painted_channels: 0,
12602 is_device_cmyk: false,
12603 spot_color: None,
12604 rendering_intent: 0,
12605 transfer: TransferState::default(),
12606 halftone: HalftoneState::default(),
12607 bg_ucr: BgUcrState::default(),
12608 alpha: 1.0,
12609 blend_mode: 0,
12610 };
12611 dev.fill_path(&fill_path, &fill_params);
12612
12613 let left_pixel = dev.pixmap().pixel(25, 50).unwrap();
12615 assert_eq!(left_pixel.red(), 255);
12616
12617 let right_pixel = dev.pixmap().pixel(75, 50).unwrap();
12619 assert_eq!(right_pixel.red(), 255);
12620 assert_eq!(right_pixel.green(), 255); }
12622
12623 #[test]
12624 fn test_erase_page() {
12625 let mut dev = SkiaDevice::new(100, 100);
12626 let mut path = PsPath::new();
12628 path.segments.push(PathSegment::MoveTo(0.0, 0.0));
12629 path.segments.push(PathSegment::LineTo(100.0, 0.0));
12630 path.segments.push(PathSegment::LineTo(100.0, 100.0));
12631 path.segments.push(PathSegment::LineTo(0.0, 100.0));
12632 path.segments.push(PathSegment::ClosePath);
12633 let params = FillParams {
12634 color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
12635 fill_rule: FillRule::NonZeroWinding,
12636 ctm: Matrix::identity(),
12637 is_text_glyph: false,
12638 overprint: false,
12639 overprint_mode: 0,
12640 opm_paired: false,
12641 painted_channels: 0,
12642 is_device_cmyk: false,
12643 spot_color: None,
12644 rendering_intent: 0,
12645 transfer: TransferState::default(),
12646 halftone: HalftoneState::default(),
12647 bg_ucr: BgUcrState::default(),
12648 alpha: 1.0,
12649 blend_mode: 0,
12650 };
12651 dev.fill_path(&path, ¶ms);
12652
12653 dev.erase_page();
12654
12655 let pixel = dev.pixmap().pixel(50, 50).unwrap();
12657 assert_eq!(pixel.red(), 255);
12658 assert_eq!(pixel.green(), 255);
12659 assert_eq!(pixel.blue(), 255);
12660 }
12661
12662 #[test]
12663 fn test_show_page() {
12664 let mut dev = SkiaDevice::new(10, 10);
12665 let path = std::env::temp_dir().join("stet_test_output.png");
12666 let path_str = path.to_string_lossy();
12667 let result = dev.show_page(&path_str);
12668 assert!(result.is_ok());
12669 assert!(path.exists());
12670 std::fs::remove_file(&path).ok();
12671 }
12672
12673 #[test]
12674 fn test_transform() {
12675 let mut dev = SkiaDevice::new(200, 200);
12676 let mut path = PsPath::new();
12678 path.segments.push(PathSegment::MoveTo(0.0, 0.0));
12679 path.segments.push(PathSegment::LineTo(10.0, 0.0));
12680 path.segments.push(PathSegment::LineTo(10.0, 10.0));
12681 path.segments.push(PathSegment::LineTo(0.0, 10.0));
12682 path.segments.push(PathSegment::ClosePath);
12683
12684 let params = FillParams {
12685 color: DeviceColor::from_rgb(0.0, 1.0, 0.0),
12686 fill_rule: FillRule::NonZeroWinding,
12687 ctm: Matrix::translate(100.0, 100.0),
12688 is_text_glyph: false,
12689 overprint: false,
12690 overprint_mode: 0,
12691 opm_paired: false,
12692 painted_channels: 0,
12693 is_device_cmyk: false,
12694 spot_color: None,
12695 rendering_intent: 0,
12696 transfer: TransferState::default(),
12697 halftone: HalftoneState::default(),
12698 bg_ucr: BgUcrState::default(),
12699 alpha: 1.0,
12700 blend_mode: 0,
12701 };
12702 dev.fill_path(&path, ¶ms);
12703
12704 let pixel = dev.pixmap().pixel(105, 105).unwrap();
12706 assert_eq!(pixel.green(), 255);
12707 assert_eq!(pixel.red(), 0);
12708 }
12709
12710 fn make_test_fill_at(x: f64, y: f64, w: f64, h: f64) -> DisplayElement {
12711 let mut path = PsPath::new();
12712 path.segments.push(PathSegment::MoveTo(x, y));
12713 path.segments.push(PathSegment::LineTo(x + w, y));
12714 path.segments.push(PathSegment::LineTo(x + w, y + h));
12715 path.segments.push(PathSegment::LineTo(x, y + h));
12716 path.segments.push(PathSegment::ClosePath);
12717 DisplayElement::Fill {
12718 path,
12719 params: FillParams {
12720 color: DeviceColor::from_rgb(0.0, 0.0, 0.0),
12721 fill_rule: FillRule::NonZeroWinding,
12722 ctm: Matrix::identity(),
12723 is_text_glyph: false,
12724 overprint: false,
12725 overprint_mode: 0,
12726 opm_paired: false,
12727 painted_channels: 0,
12728 is_device_cmyk: false,
12729 spot_color: None,
12730 rendering_intent: 0,
12731 transfer: TransferState::default(),
12732 halftone: HalftoneState::default(),
12733 bg_ucr: BgUcrState::default(),
12734 alpha: 1.0,
12735 blend_mode: 0,
12736 },
12737 }
12738 }
12739
12740 #[test]
12741 fn test_compute_paint_bounds_two_fills() {
12742 let mut list = DisplayList::new();
12743 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");
12747 assert!(
12748 (bounds.x_min - 10.0).abs() < 1e-9,
12749 "x_min was {}",
12750 bounds.x_min
12751 );
12752 assert!(
12753 (bounds.y_min - 20.0).abs() < 1e-9,
12754 "y_min was {}",
12755 bounds.y_min
12756 );
12757 assert!(
12758 (bounds.x_max - 150.0).abs() < 1e-9,
12759 "x_max was {}",
12760 bounds.x_max
12761 );
12762 assert!(
12763 (bounds.y_max - 75.0).abs() < 1e-9,
12764 "y_max was {}",
12765 bounds.y_max
12766 );
12767 }
12768
12769 #[test]
12770 fn test_compute_paint_bounds_empty_list() {
12771 let list = DisplayList::new();
12772 assert!(compute_paint_bounds(&list, 72.0).is_none());
12773 }
12774
12775 #[test]
12776 fn test_compute_paint_bounds_only_clip_returns_none() {
12777 let mut list = DisplayList::new();
12778 list.push(DisplayElement::InitClip);
12779 assert!(compute_paint_bounds(&list, 72.0).is_none());
12782 }
12783
12784 #[test]
12785 fn test_rasterize_mask_anchors_to_paint_bounds() {
12786 use stet_graphics::display_list::{SoftMaskParams, SoftMaskSubtype};
12787
12788 let mut mask = DisplayList::new();
12791 let mut path = PsPath::new();
12792 path.segments.push(PathSegment::MoveTo(200.0, 300.0));
12793 path.segments.push(PathSegment::LineTo(250.0, 300.0));
12794 path.segments.push(PathSegment::LineTo(250.0, 340.0));
12795 path.segments.push(PathSegment::LineTo(200.0, 340.0));
12796 path.segments.push(PathSegment::ClosePath);
12797 mask.push(DisplayElement::Fill {
12798 path,
12799 params: FillParams {
12800 color: DeviceColor::from_rgb(1.0, 1.0, 1.0),
12801 fill_rule: FillRule::NonZeroWinding,
12802 ctm: Matrix::identity(),
12803 is_text_glyph: false,
12804 overprint: false,
12805 overprint_mode: 0,
12806 opm_paired: false,
12807 painted_channels: 0,
12808 is_device_cmyk: false,
12809 spot_color: None,
12810 rendering_intent: 0,
12811 transfer: TransferState::default(),
12812 halftone: HalftoneState::default(),
12813 bg_ucr: BgUcrState::default(),
12814 alpha: 1.0,
12815 blend_mode: 0,
12816 },
12817 });
12818
12819 let params = SoftMaskParams {
12820 subtype: SoftMaskSubtype::Luminosity,
12821 bbox: [0.0, 0.0, 100.0, 100.0],
12824 backdrop_color: None, transfer_invert: false,
12826 has_nested_mask_scope: false,
12827 parent_clip_bbox: None,
12828 };
12829
12830 let raster =
12831 rasterize_mask(&mask, ¶ms, None, false, 72.0, 1.0, 1.0).expect("expected raster");
12832
12833 assert_eq!(raster.origin_x, 199);
12836 assert_eq!(raster.origin_y, 299);
12837 assert_eq!(raster.width, 52);
12839 assert_eq!(raster.height, 42);
12840 assert_eq!(raster.scale_x, 1.0);
12841 assert_eq!(raster.scale_y, 1.0);
12842
12843 let mx = 225 - raster.origin_x;
12847 let my = 320 - raster.origin_y;
12848 assert!(mx >= 0 && (mx as u32) < raster.width);
12849 assert!(my >= 0 && (my as u32) < raster.height);
12850 let center_value = raster.data[(my as usize) * raster.width as usize + mx as usize];
12851 assert_eq!(
12852 center_value, 255,
12853 "center of painted mask should be opaque white (lum=255)"
12854 );
12855
12856 let mx_out = 300 - raster.origin_x;
12860 let in_bounds = mx_out >= 0 && (mx_out as u32) < raster.width;
12861 assert!(!in_bounds, "page x=300 should be outside the mask raster");
12862 assert_eq!(
12863 out_of_bounds_mask_value(¶ms),
12864 0,
12865 "black backdrop → out-of-bounds = 0"
12866 );
12867 }
12868
12869 #[test]
12870 fn test_band_local_to_mask_formula() {
12871 let raster_origin_x = 200i32;
12882 let raster_origin_y = 300i32;
12883
12884 let sample = |vp_x_dev: f32,
12886 vp_y_dev: f32,
12887 scale: f32,
12888 crop_x: i32,
12889 crop_y: i32,
12890 x: i32,
12891 y: i32|
12892 -> (i32, i32) {
12893 let vp_x_pixels = (vp_x_dev * scale).round() as i32;
12894 let vp_y_pixels = (vp_y_dev * scale).round() as i32;
12895 let page_x = vp_x_pixels + crop_x + x;
12896 let page_y = vp_y_pixels + crop_y + y;
12897 let mx = page_x - raster_origin_x;
12898 let my = page_y - raster_origin_y;
12899 (mx, my)
12900 };
12901
12902 let (mx, my) = sample(0.0, 0.0, 1.0, 220, 310, 0, 0);
12907 assert_eq!((mx, my), (20, 10), "top band: smask top-left");
12908
12909 let (mx, my) = sample(0.0, 0.0, 1.0, 220, 310, 5, 5);
12911 assert_eq!((mx, my), (25, 15), "top band: 5px into smask");
12912
12913 let (mx, my) = sample(0.0, 305.0, 1.0, 220, 5, 0, 0);
12921 assert_eq!((mx, my), (20, 10), "mid band: smask top-left");
12922
12923 let vp_x_pixels = (100.0_f32 * 2.0).round() as i32;
12935 let crop_x = ((220.0_f32 - 100.0) * 2.0).floor() as i32;
12936 let page_x_for_x_zero = vp_x_pixels + crop_x;
12937 assert_eq!(page_x_for_x_zero, 440, "viewport scale-2: page-x at x=0");
12938 }
12939
12940 fn x_path() -> PsPath {
12943 let mut p = PsPath::new();
12944 p.segments.push(PathSegment::MoveTo(10.0, 10.0));
12945 p.segments.push(PathSegment::LineTo(20.0, 20.0));
12946 p.segments.push(PathSegment::LineTo(30.0, 10.0));
12947 p.segments.push(PathSegment::LineTo(20.0, 0.0));
12948 p.segments.push(PathSegment::ClosePath);
12949 p
12950 }
12951
12952 fn x_path_perturbed() -> PsPath {
12953 let mut p = PsPath::new();
12956 p.segments.push(PathSegment::MoveTo(10.001, 10.0));
12957 p.segments.push(PathSegment::LineTo(20.0, 19.999));
12958 p.segments.push(PathSegment::LineTo(30.002, 10.001));
12959 p.segments.push(PathSegment::LineTo(19.999, 0.0));
12960 p.segments.push(PathSegment::ClosePath);
12961 p
12962 }
12963
12964 fn fill(path: PsPath, alpha: f64, blend: u8) -> DisplayElement {
12965 DisplayElement::Fill {
12966 path,
12967 params: FillParams {
12968 color: DeviceColor::from_rgb(0.0, 0.0, 0.0),
12969 fill_rule: FillRule::NonZeroWinding,
12970 ctm: Matrix::identity(),
12971 is_text_glyph: false,
12972 overprint: false,
12973 overprint_mode: 0,
12974 opm_paired: false,
12975 painted_channels: 0,
12976 is_device_cmyk: false,
12977 spot_color: None,
12978 rendering_intent: 0,
12979 transfer: TransferState::default(),
12980 halftone: HalftoneState::default(),
12981 bg_ucr: BgUcrState::default(),
12982 alpha,
12983 blend_mode: blend,
12984 },
12985 }
12986 }
12987
12988 fn rect_path(x0: f64, y0: f64, x1: f64, y1: f64) -> PsPath {
12989 let mut p = PsPath::new();
12990 p.segments.push(PathSegment::MoveTo(x0, y0));
12991 p.segments.push(PathSegment::LineTo(x1, y0));
12992 p.segments.push(PathSegment::LineTo(x1, y1));
12993 p.segments.push(PathSegment::LineTo(x0, y1));
12994 p.segments.push(PathSegment::ClosePath);
12995 p
12996 }
12997
12998 fn clip_elem(path: PsPath) -> DisplayElement {
12999 DisplayElement::Clip {
13000 path,
13001 params: ClipParams {
13002 fill_rule: FillRule::NonZeroWinding,
13003 ctm: Matrix::identity(),
13004 stroke_params: None,
13005 },
13006 }
13007 }
13008
13009 fn group_elem(
13010 inner: Vec<DisplayElement>,
13011 bbox: [f64; 4],
13012 isolated: bool,
13013 alpha: f64,
13014 blend: u8,
13015 ) -> DisplayElement {
13016 let mut dl = DisplayList::new();
13017 for e in inner {
13018 dl.push(e);
13019 }
13020 DisplayElement::Group {
13021 elements: dl,
13022 params: stet_graphics::display_list::GroupParams {
13023 bbox,
13024 isolated,
13025 knockout: false,
13026 blend_mode: blend,
13027 alpha,
13028 color_space: stet_graphics::display_list::GroupColorSpace::Inherited,
13029 },
13030 }
13031 }
13032
13033 fn dl(elements: Vec<DisplayElement>) -> DisplayList {
13034 let mut d = DisplayList::new();
13035 for e in elements {
13036 d.push(e);
13037 }
13038 d
13039 }
13040
13041 #[test]
13042 fn obscured_skip_fires_on_matching_fill_plus_iso_group() {
13043 let parent = fill(x_path(), 1.0, 0);
13046 let inner = vec![fill(x_path_perturbed(), 1.0, 0)];
13047 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13048 let d = dl(vec![
13049 parent,
13050 clip_elem(rect_path(0.0, -5.0, 40.0, 30.0)),
13051 grp,
13052 ]);
13053 assert_eq!(compute_obscured_fill_skips(&d), vec![0]);
13054 }
13055
13056 #[test]
13057 fn obscured_skip_does_not_fire_on_non_isolated_group() {
13058 let parent = fill(x_path(), 1.0, 0);
13059 let inner = vec![fill(x_path(), 1.0, 0)];
13060 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], false, 1.0, 0);
13061 let d = dl(vec![parent, grp]);
13062 assert!(compute_obscured_fill_skips(&d).is_empty());
13063 }
13064
13065 #[test]
13066 fn obscured_skip_does_not_fire_on_partial_alpha_group() {
13067 let parent = fill(x_path(), 1.0, 0);
13068 let inner = vec![fill(x_path(), 1.0, 0)];
13069 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 0.5, 0);
13070 let d = dl(vec![parent, grp]);
13071 assert!(compute_obscured_fill_skips(&d).is_empty());
13072 }
13073
13074 #[test]
13075 fn obscured_skip_does_not_fire_on_non_normal_blend() {
13076 let parent = fill(x_path(), 1.0, 0);
13077 let inner = vec![fill(x_path(), 1.0, 0)];
13078 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 10);
13081 let d = dl(vec![parent, grp]);
13082 assert!(compute_obscured_fill_skips(&d).is_empty());
13083 }
13084
13085 #[test]
13086 fn obscured_skip_does_not_fire_when_paths_differ() {
13087 let parent = fill(rect_path(0.0, 0.0, 5.0, 5.0), 1.0, 0);
13088 let inner = vec![fill(x_path(), 1.0, 0)];
13089 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13090 let d = dl(vec![parent, grp]);
13091 assert!(compute_obscured_fill_skips(&d).is_empty());
13092 }
13093
13094 #[test]
13095 fn obscured_skip_does_not_fire_when_group_bbox_too_small() {
13096 let big = rect_path(0.0, 0.0, 100.0, 100.0);
13100 let parent = fill(big.clone(), 1.0, 0);
13101 let inner = vec![fill(big, 1.0, 0)];
13102 let grp = group_elem(inner, [0.0, 0.0, 10.0, 10.0], true, 1.0, 0);
13104 let d = dl(vec![parent, grp]);
13105 assert!(compute_obscured_fill_skips(&d).is_empty());
13106 }
13107
13108 #[test]
13109 fn obscured_skip_does_not_fire_when_intervening_clip_too_small() {
13110 let parent = fill(x_path(), 1.0, 0);
13114 let narrow_clip = clip_elem(rect_path(12.0, 5.0, 18.0, 15.0));
13115 let inner = vec![fill(x_path(), 1.0, 0)];
13116 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13117 let d = dl(vec![parent, narrow_clip, grp]);
13118 assert!(compute_obscured_fill_skips(&d).is_empty());
13119 }
13120
13121 #[test]
13122 fn obscured_skip_does_not_fire_when_inner_clip_too_small() {
13123 let parent = fill(x_path(), 1.0, 0);
13125 let inner = vec![
13126 clip_elem(rect_path(12.0, 5.0, 18.0, 15.0)),
13127 fill(x_path(), 1.0, 0),
13128 ];
13129 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13130 let d = dl(vec![parent, grp]);
13131 assert!(compute_obscured_fill_skips(&d).is_empty());
13132 }
13133
13134 #[test]
13135 fn obscured_skip_fires_when_inner_clip_is_wider_than_parent_path() {
13136 let parent = fill(x_path(), 1.0, 0);
13139 let inner = vec![
13140 clip_elem(rect_path(-10.0, -10.0, 40.0, 30.0)),
13141 fill(x_path_perturbed(), 1.0, 0),
13142 ];
13143 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13144 let d = dl(vec![parent, grp]);
13145 assert_eq!(compute_obscured_fill_skips(&d), vec![0]);
13146 }
13147
13148 #[test]
13149 fn obscured_skip_does_not_fire_on_partial_alpha_parent() {
13150 let parent = fill(x_path(), 0.5, 0);
13153 let inner = vec![fill(x_path(), 1.0, 0)];
13154 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13155 let d = dl(vec![parent, grp]);
13156 assert!(compute_obscured_fill_skips(&d).is_empty());
13157 }
13158}