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
18#[cfg(feature = "ps-device")]
19use stet_core::device::OutputDevice;
20use stet_fonts::geometry::{Matrix, PathSegment, PsPath};
21use stet_graphics::color::{DeviceColor, FillRule, LineCap, LineJoin};
22#[cfg(feature = "ps-device")]
23use stet_graphics::device::PageSinkFactory;
24use stet_graphics::device::{
25 AxialShadingParams, ClipParams, FillParams, ImageColorSpace, ImageParams, MeshShadingParams,
26 PatchShadingParams, RadialShadingParams, ShadingColorSpace, ShadingVertex, StrokeParams,
27 TintLookupTable,
28};
29use stet_graphics::icc::IccCache;
30use stet_graphics::layer_set::LayerSet;
31
32#[derive(Clone, Copy)]
34struct ClipRect {
35 x0: u32,
36 y0: u32, x1: u32,
38 y1: u32, }
40
41impl ClipRect {
42 fn intersect(&self, other: &ClipRect) -> ClipRect {
44 ClipRect {
45 x0: self.x0.max(other.x0),
46 y0: self.y0.max(other.y0),
47 x1: self.x1.min(other.x1),
48 y1: self.y1.min(other.y1),
49 }
50 }
51
52 fn is_empty(&self) -> bool {
53 self.x0 >= self.x1 || self.y0 >= self.y1
54 }
55
56 fn is_full_page(&self, w: u32, h: u32) -> bool {
58 self.x0 == 0 && self.y0 == 0 && self.x1 == w && self.y1 == h
59 }
60
61 fn make_mask(self, w: u32, h: u32) -> Option<Mask> {
63 if self.is_empty() {
64 return None;
65 }
66 let mut mask = Mask::new(w, h)?;
67 let data = mask.data_mut();
68 let stride = w as usize;
69 for y in self.y0..self.y1 {
70 let row_start = y as usize * stride + self.x0 as usize;
71 let row_end = y as usize * stride + self.x1 as usize;
72 data[row_start..row_end].fill(255);
73 }
74 Some(mask)
75 }
76}
77
78enum ClipRegion {
80 Rect(ClipRect),
81 Mask(Mask),
82}
83
84#[cfg(feature = "ps-device")]
90pub struct SkiaDevice {
91 pixmap: Pixmap,
92 page_w: u32,
95 page_h: u32,
96 dpi: f64,
98 clip_region: Option<ClipRegion>,
99 clip_mask_cache: HashMap<u64, Mask>,
102 clip_mask_seen: HashSet<u64>,
103 spare_mask: Option<Mask>,
105 pending_render: Option<std::sync::mpsc::Receiver<Result<(), String>>>,
108 sink_factory: Box<dyn PageSinkFactory>,
110 system_cmyk_bytes: Option<std::sync::Arc<Vec<u8>>>,
112 render_icc_cache: Option<IccCache>,
114 no_aa: bool,
116 use_viewport_path: bool,
121 layer_set: LayerSet,
126}
127
128#[cfg(feature = "ps-device")]
129impl SkiaDevice {
130 pub fn new(width: u32, height: u32) -> Self {
136 Self::with_sink_factory(width, height, Box::new(crate::PngSinkFactory))
137 }
138
139 pub fn with_sink_factory(
141 width: u32,
142 height: u32,
143 sink_factory: Box<dyn PageSinkFactory>,
144 ) -> Self {
145 let width = width.max(1);
161 let height = height.max(1);
162 let dpi = height as f64 * 72.0 / 792.0;
165
166 let pixmap = Pixmap::new(1, 1).expect("Failed to create placeholder pixmap");
170 Self {
171 pixmap,
172 page_w: width,
173 page_h: height,
174 dpi,
175 clip_region: None,
176 clip_mask_cache: HashMap::new(),
177 clip_mask_seen: HashSet::new(),
178 spare_mask: None,
179 pending_render: None,
180 sink_factory,
181 system_cmyk_bytes: None,
182 render_icc_cache: None,
183 no_aa: false,
184 use_viewport_path: false,
185 layer_set: LayerSet::new(),
186 }
187 }
188
189 pub fn set_use_viewport_path(&mut self, on: bool) {
192 self.use_viewport_path = on;
193 }
194
195 pub fn set_layer_set(&mut self, layer_set: LayerSet) {
202 self.layer_set = layer_set;
203 }
204
205 pub fn layer_set(&self) -> &LayerSet {
207 &self.layer_set
208 }
209
210 fn ensure_full_pixmap(&mut self) {
213 if self.pixmap.width() != self.page_w || self.pixmap.height() != self.page_h {
214 let Some(pixmap) = Pixmap::new(self.page_w, self.page_h) else {
220 eprintln!(
221 "Warning: could not allocate a {}x{} page pixmap; \
222 rendering into the existing {}x{} buffer instead",
223 self.page_w,
224 self.page_h,
225 self.pixmap.width(),
226 self.pixmap.height()
227 );
228 return;
229 };
230 self.pixmap = pixmap;
231 self.pixmap.fill(Color::WHITE);
232 }
233 }
234
235 pub fn pixmap(&self) -> &Pixmap {
237 &self.pixmap
238 }
239
240 pub fn set_system_cmyk_bytes(&mut self, bytes: std::sync::Arc<Vec<u8>>) {
242 self.system_cmyk_bytes = Some(bytes);
243 }
244
245 pub fn set_no_aa(&mut self, no_aa: bool) {
247 self.no_aa = no_aa;
248 }
249}
250
251fn to_transform(m: &Matrix) -> Transform {
253 Transform::from_row(
254 m.a as f32,
255 m.b as f32,
256 m.c as f32,
257 m.d as f32,
258 m.tx as f32,
259 m.ty as f32,
260 )
261}
262
263fn to_paint(color: &DeviceColor) -> Paint<'static> {
265 to_paint_alpha(color, 1.0, 0, false)
266}
267
268fn to_paint_alpha(color: &DeviceColor, alpha: f64, blend_mode: u8, no_aa: bool) -> Paint<'static> {
270 let mut paint = Paint::default();
271 let a = (alpha * 255.0).round().clamp(0.0, 255.0) as u8;
272 paint.set_color_rgba8(
273 (color.r * 255.0).round().clamp(0.0, 255.0) as u8,
274 (color.g * 255.0).round().clamp(0.0, 255.0) as u8,
275 (color.b * 255.0).round().clamp(0.0, 255.0) as u8,
276 a,
277 );
278 paint.anti_alias = !no_aa;
279 paint.blend_mode = u8_to_blend_mode(blend_mode);
280 paint
281}
282
283fn u8_to_blend_mode(mode: u8) -> BlendMode {
285 match mode {
286 1 => BlendMode::Multiply,
287 2 => BlendMode::Screen,
288 3 => BlendMode::Overlay,
289 4 => BlendMode::Darken,
290 5 => BlendMode::Lighten,
291 6 => BlendMode::ColorDodge,
292 7 => BlendMode::ColorBurn,
293 8 => BlendMode::HardLight,
294 9 => BlendMode::SoftLight,
295 10 => BlendMode::Difference,
296 11 => BlendMode::Exclusion,
297 12 => BlendMode::Hue,
298 13 => BlendMode::Saturation,
299 14 => BlendMode::Color,
300 15 => BlendMode::Luminosity,
301 _ => BlendMode::SourceOver,
302 }
303}
304
305const MAX_PATH_COORD: f32 = 1e6;
311
312fn build_skia_path(path: &PsPath) -> Option<stet_tiny_skia::Path> {
313 let mut pb = PathBuilder::new();
314
315 for seg in &path.segments {
316 match seg {
317 PathSegment::MoveTo(x, y) => {
318 pb.move_to(*x as f32, *y as f32);
319 }
320 PathSegment::LineTo(x, y) => {
321 pb.line_to(*x as f32, *y as f32);
322 }
323 PathSegment::CurveTo {
324 x1,
325 y1,
326 x2,
327 y2,
328 x3,
329 y3,
330 } => {
331 pb.cubic_to(
332 *x1 as f32, *y1 as f32, *x2 as f32, *y2 as f32, *x3 as f32, *y3 as f32,
333 );
334 }
335 PathSegment::ClosePath => {
336 pb.close();
337 }
338 }
339 }
340
341 let result = pb.finish()?;
342
343 let b = result.bounds();
347 if b.left().abs() > MAX_PATH_COORD
348 || b.top().abs() > MAX_PATH_COORD
349 || b.right().abs() > MAX_PATH_COORD
350 || b.bottom().abs() > MAX_PATH_COORD
351 {
352 return None;
353 }
354
355 Some(result)
356}
357
358fn is_degenerate_fill(path: &PsPath) -> bool {
370 let mut x_min = f64::INFINITY;
371 let mut x_max = f64::NEG_INFINITY;
372 let mut y_min = f64::INFINITY;
373 let mut y_max = f64::NEG_INFINITY;
374
375 for seg in &path.segments {
376 let (x, y) = match seg {
377 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => (*x, *y),
378 PathSegment::CurveTo { .. } => return false,
380 PathSegment::ClosePath => continue,
381 };
382 x_min = x_min.min(x);
383 x_max = x_max.max(x);
384 y_min = y_min.min(y);
385 y_max = y_max.max(y);
386 }
387
388 if x_min > x_max {
389 return false; }
391
392 let w = x_max - x_min;
393 let h = y_max - y_min;
394
395 let eps = 1e-6;
399 (w < eps && h > eps) || (h < eps && w > eps)
400}
401
402fn to_fill_rule(rule: &FillRule) -> SkiaFillRule {
407 match rule {
408 FillRule::NonZeroWinding => SkiaFillRule::Winding,
409 FillRule::EvenOdd => SkiaFillRule::EvenOdd,
410 _ => SkiaFillRule::Winding,
411 }
412}
413
414fn to_line_cap(cap: LineCap) -> SkiaLineCap {
416 match cap {
417 LineCap::Butt => SkiaLineCap::Butt,
418 LineCap::Round => SkiaLineCap::Round,
419 LineCap::Square => SkiaLineCap::Square,
420 _ => SkiaLineCap::Butt,
421 }
422}
423
424fn to_line_join(join: LineJoin) -> SkiaLineJoin {
426 match join {
427 LineJoin::Miter => SkiaLineJoin::Miter,
428 LineJoin::Round => SkiaLineJoin::Round,
429 LineJoin::Bevel => SkiaLineJoin::Bevel,
430 _ => SkiaLineJoin::Miter,
431 }
432}
433
434fn detect_rect(path: &PsPath, page_w: u32, page_h: u32) -> Option<ClipRect> {
437 let segs = &path.segments;
438 let (move_to, lines, _has_close) = match segs.len() {
442 5 => {
443 if !matches!(segs[4], PathSegment::ClosePath) {
445 return None;
446 }
447 (&segs[0], &segs[1..4], true)
448 }
449 6 => {
450 if !matches!(segs[5], PathSegment::ClosePath) {
452 return None;
453 }
454 (&segs[0], &segs[1..5], true)
455 }
456 4 => {
457 (&segs[0], &segs[1..4], false)
459 }
460 _ => return None,
461 };
462
463 let PathSegment::MoveTo(mx, my) = move_to else {
464 return None;
465 };
466
467 let mut pts = vec![(*mx, *my)];
469 for seg in lines {
470 match seg {
471 PathSegment::LineTo(x, y) => pts.push((*x, *y)),
472 _ => return None,
473 }
474 }
475
476 if pts.len() == 5 {
478 let (fx, fy) = pts[0];
479 let (lx, ly) = pts[4];
480 if (fx - lx).abs() > 0.01 || (fy - ly).abs() > 0.01 {
481 return None;
482 }
483 pts.truncate(4);
484 }
485
486 for i in 0..4 {
488 let (x1, y1) = pts[i];
489 let (x2, y2) = pts[(i + 1) % 4];
490 let dx = (x2 - x1).abs();
491 let dy = (y2 - y1).abs();
492 if dx > 0.01 && dy > 0.01 {
493 return None; }
495 }
496
497 let min_x = pts.iter().map(|p| p.0).fold(f64::INFINITY, f64::min);
499 let min_y = pts.iter().map(|p| p.1).fold(f64::INFINITY, f64::min);
500 let max_x = pts.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max);
501 let max_y = pts.iter().map(|p| p.1).fold(f64::NEG_INFINITY, f64::max);
502
503 let x0 = (min_x.floor().max(0.0) as u32).min(page_w);
505 let y0 = (min_y.floor().max(0.0) as u32).min(page_h);
506 let x1 = (max_x.ceil().max(0.0) as u32).min(page_w);
507 let y1 = (max_y.ceil().max(0.0) as u32).min(page_h);
508
509 Some(ClipRect { x0, y0, x1, y1 })
510}
511
512fn intersect_mask_with_rect(mask: &mut Mask, rect: &ClipRect, w: u32, h: u32) {
514 let data = mask.data_mut();
515 let stride = w as usize;
516
517 if rect.y0 > 0 {
519 let end = (rect.y0 as usize * stride).min(data.len());
520 data[..end].fill(0);
521 }
522
523 if rect.y1 < h {
525 let start = (rect.y1 as usize * stride).min(data.len());
526 data[start..].fill(0);
527 }
528
529 for y in rect.y0..rect.y1.min(h) {
531 let row_start = y as usize * stride;
532 if rect.x0 > 0 {
534 let end = row_start + rect.x0 as usize;
535 data[row_start..end].fill(0);
536 }
537 if rect.x1 < w {
539 let start = row_start + rect.x1 as usize;
540 let end = row_start + stride;
541 data[start..end].fill(0);
542 }
543 }
544}
545
546fn resolve_clip_mask<'a>(
551 clip_region: &'a Option<ClipRegion>,
552 temp_mask: &'a mut Option<Mask>,
553 w: u32,
554 h: u32,
555) -> Option<Option<&'a Mask>> {
556 match clip_region {
557 None => Some(None),
558 Some(ClipRegion::Mask(m)) => Some(Some(m)),
559 Some(ClipRegion::Rect(rect)) => {
560 if rect.is_empty() {
561 return None; }
563 if rect.is_full_page(w, h) {
564 return Some(None); }
566 *temp_mask = rect.make_mask(w, h);
567 Some(temp_mask.as_ref())
568 }
569 }
570}
571
572fn hash_clip_path(path: &PsPath, fill_rule: &FillRule) -> u64 {
575 let mut hasher = std::collections::hash_map::DefaultHasher::new();
576 std::mem::discriminant(fill_rule).hash(&mut hasher);
577 for seg in &path.segments {
578 match seg {
579 PathSegment::MoveTo(x, y) => {
580 0u8.hash(&mut hasher);
581 x.to_bits().hash(&mut hasher);
582 y.to_bits().hash(&mut hasher);
583 }
584 PathSegment::LineTo(x, y) => {
585 1u8.hash(&mut hasher);
586 x.to_bits().hash(&mut hasher);
587 y.to_bits().hash(&mut hasher);
588 }
589 PathSegment::CurveTo {
590 x1,
591 y1,
592 x2,
593 y2,
594 x3,
595 y3,
596 } => {
597 2u8.hash(&mut hasher);
598 x1.to_bits().hash(&mut hasher);
599 y1.to_bits().hash(&mut hasher);
600 x2.to_bits().hash(&mut hasher);
601 y2.to_bits().hash(&mut hasher);
602 x3.to_bits().hash(&mut hasher);
603 y3.to_bits().hash(&mut hasher);
604 }
605 PathSegment::ClosePath => {
606 3u8.hash(&mut hasher);
607 }
608 }
609 }
610 hasher.finish()
611}
612
613fn intersect_masks(dst: &mut Mask, src: &Mask) {
615 let dst_data = dst.data_mut();
616 let src_data = src.data();
617 for (d, s) in dst_data.iter_mut().zip(src_data.iter()) {
618 *d = ((*d as u16 * *s as u16 + 127) / 255) as u8;
619 }
620}
621
622use stet_graphics::display_list::{DisplayElement, DisplayList};
625
626struct BandState {
628 clip_region: Option<ClipRegion>,
629 spare_mask: Option<Mask>,
630 clip_mask_cache: HashMap<u64, Mask>,
632 clip_mask_seen: HashSet<u64>,
634 mask_pool: Vec<Mask>,
636 cmyk_buffer: Option<Vec<f32>>,
640 op_bg_snapshot: Option<Vec<u8>>,
647 op_touched: Option<Vec<u8>>,
652 spot_mask: Option<Vec<u8>>,
660}
661
662const MAX_POOL_MASKS: usize = 8;
665
666impl BandState {
667 #[allow(dead_code)]
669 fn recycle_cache(&mut self) {
670 for (_, mask) in self.clip_mask_cache.drain() {
671 if self.mask_pool.len() < MAX_POOL_MASKS {
672 self.mask_pool.push(mask);
673 }
674 }
676 }
677
678 fn recycle_mask(&mut self, mask: Mask) {
680 if self.mask_pool.len() < MAX_POOL_MASKS {
681 self.mask_pool.push(mask);
682 }
683 }
684
685 fn take_mask(&mut self, w: u32, h: u32) -> Mask {
687 self.spare_mask
688 .take()
689 .or_else(|| self.mask_pool.pop())
690 .unwrap_or_else(|| Mask::new(w, h).expect("Failed to create mask"))
691 }
692
693 fn take_op_buffers(&mut self, w: u32, h: u32) -> (Vec<u8>, Vec<u8>) {
698 let n = w as usize * h as usize;
699 let bg = self
700 .op_bg_snapshot
701 .take()
702 .unwrap_or_else(|| vec![0u8; n * 4]);
703 let touched = self.op_touched.take().unwrap_or_else(|| vec![0u8; n]);
704 (bg, touched)
705 }
706
707 fn restore_op_buffers(&mut self, bg: Vec<u8>, touched: Vec<u8>) {
709 self.op_bg_snapshot = Some(bg);
710 self.op_touched = Some(touched);
711 }
712
713 fn take_spot_mask(&mut self, w: u32, h: u32) -> Vec<u8> {
715 let n = w as usize * h as usize;
716 self.spot_mask.take().unwrap_or_else(|| vec![0u8; n])
717 }
718
719 fn restore_spot_mask(&mut self, mask: Vec<u8>) {
721 self.spot_mask = Some(mask);
722 }
723
724 #[allow(dead_code)]
728 fn invalidate_op_snapshot(
729 &mut self,
730 bbox_x0: usize,
731 bbox_y0: usize,
732 bbox_x1: usize,
733 bbox_y1: usize,
734 stride: usize,
735 ) {
736 if let Some(touched) = self.op_touched.as_mut() {
737 for y in bbox_y0..bbox_y1 {
738 let row = y * stride;
739 for x in bbox_x0..bbox_x1 {
740 touched[row + x] = 0;
741 }
742 }
743 }
744 }
745}
746
747struct RenderContext<'a> {
752 vp_x: f32,
754 vp_y: f32,
756 scale_x: f32,
758 scale_y: f32,
760 out_w: u32,
762 out_h: u32,
764 effective_dpi: f64,
766 icc: Option<&'a IccCache>,
768 image_cache: Option<&'a ImageCache>,
770 preprocessed: Option<&'a [Option<PreprocessedImage>]>,
772 elem_idx: usize,
774 no_aa: bool,
776 opm_zero_transparent: bool,
778 knockout_painter_pass: KnockoutPainterPass,
794 parent_group_isolated: bool,
802 alpha_extraction_pass: bool,
807 layer_set: &'a LayerSet,
810}
811
812#[derive(Clone, Copy, PartialEq, Eq)]
815enum KnockoutPainterPass {
816 None,
818 ColorPass,
820 CoveragePass,
823}
824
825impl RenderContext<'_> {
826 fn transform(&self, m: &Matrix) -> Transform {
828 viewport_transform(
829 to_transform(m),
830 self.vp_x,
831 self.vp_y,
832 self.scale_x,
833 self.scale_y,
834 )
835 }
836}
837
838struct YBBox {
840 y_min: f64,
841 y_max: f64,
842}
843
844struct ClipEpoch {
849 start_idx: usize,
851 end_idx: usize,
853 paint_bbox: Option<YBBox>,
856 has_erase_page: bool,
858}
859
860fn select_band_height(w: u32, h: u32) -> u32 {
863 if w == 0 || h == 0 {
864 return h;
865 }
866 let per_row = w as u64 * 6;
868 let budget = 2 * 1024 * 1024u64; let max_rows = budget / per_row;
870
871 let band = if max_rows >= h as u64 {
873 h
874 } else {
875 let mut p = 1u32;
876 while (p as u64) * 2 <= max_rows {
877 p *= 2;
878 }
879 p.clamp(128, h)
885 };
886
887 if h.div_ceil(band) <= 2 {
889 return h;
890 }
891 band
892}
893
894fn contains_clip_op(list: &DisplayList) -> bool {
901 list.elements().iter().any(|e| match e {
902 DisplayElement::Clip { .. } | DisplayElement::InitClip => true,
903 DisplayElement::OcgGroup { elements, .. } => contains_clip_op(elements),
904 DisplayElement::Group { elements, .. } => contains_clip_op(elements),
905 DisplayElement::SoftMasked { content, .. } => contains_clip_op(content),
906 _ => false,
907 })
908}
909
910fn precompute_bboxes(list: &DisplayList, dpi: f64) -> Vec<Option<YBBox>> {
916 list.elements()
917 .iter()
918 .map(|elem| match elem {
919 DisplayElement::Fill { path, params } => fill_device_y_bbox(path, ¶ms.ctm),
920 DisplayElement::Stroke { path, params } => stroke_device_y_bbox(path, params, dpi),
921 DisplayElement::Image { params, .. } => image_y_bbox(params),
922 DisplayElement::AxialShading { params } => {
923 shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
924 }
925 DisplayElement::RadialShading { params } => {
926 shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
927 }
928 DisplayElement::MeshShading { params } => {
929 shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
930 }
931 DisplayElement::PatchShading { params } => {
932 shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
933 }
934 DisplayElement::PatternFill { params } => pattern_fill_y_bbox(params),
935 DisplayElement::Group { params, .. } => Some(YBBox {
936 y_min: params.bbox[1],
937 y_max: params.bbox[3],
938 }),
939 DisplayElement::SoftMasked { params, .. } => Some(YBBox {
940 y_min: params.bbox[1],
941 y_max: params.bbox[3],
942 }),
943 DisplayElement::OcgGroup {
944 elements,
945 visibility,
946 } => {
947 if !visibility.default_visible() && !contains_clip_op(elements) {
953 return None;
954 }
955 let child_bboxes = precompute_bboxes(elements, dpi);
956 let mut y_min = f64::INFINITY;
957 let mut y_max = f64::NEG_INFINITY;
958 for cb in child_bboxes.into_iter().flatten() {
959 y_min = y_min.min(cb.y_min);
960 y_max = y_max.max(cb.y_max);
961 }
962 if y_min <= y_max {
963 Some(YBBox { y_min, y_max })
964 } else {
965 None
966 }
967 }
968 _ => None, })
970 .collect()
971}
972
973fn shading_y_bbox_from_bbox(bbox: &Option<[f64; 4]>, ctm: &Matrix) -> Option<YBBox> {
977 if let Some(bbox) = bbox {
978 let corners = [
979 (bbox[0], bbox[1]),
980 (bbox[2], bbox[1]),
981 (bbox[0], bbox[3]),
982 (bbox[2], bbox[3]),
983 ];
984 let mut y_min = f64::INFINITY;
985 let mut y_max = f64::NEG_INFINITY;
986 for (x, y) in &corners {
987 let (_, dy) = ctm.transform_point(*x, *y);
988 y_min = y_min.min(dy);
989 y_max = y_max.max(dy);
990 }
991 Some(YBBox { y_min, y_max })
992 } else {
993 Some(YBBox {
996 y_min: 0.0,
997 y_max: 1e9,
998 })
999 }
1000}
1001
1002fn stroke_device_y_bbox(path: &PsPath, params: &StrokeParams, dpi: f64) -> Option<YBBox> {
1009 let m = ¶ms.ctm;
1010 let is_identity =
1011 m.a == 1.0 && m.b == 0.0 && m.c == 0.0 && m.d == 1.0 && m.tx == 0.0 && m.ty == 0.0;
1012
1013 let effective_lw = params.line_width.max(hairline_min_width(¶ms.ctm, dpi));
1015
1016 if is_identity {
1017 return path_y_bbox(path).map(|mut bbox| {
1019 let expand = effective_lw * params.miter_limit * 0.5;
1020 bbox.y_min -= expand;
1021 bbox.y_max += expand;
1022 bbox
1023 });
1024 }
1025
1026 let (mut x_min, mut x_max) = (f64::INFINITY, f64::NEG_INFINITY);
1029 let (mut y_min, mut y_max) = (f64::INFINITY, f64::NEG_INFINITY);
1030 for seg in &path.segments {
1031 match seg {
1032 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => {
1033 x_min = x_min.min(*x);
1034 x_max = x_max.max(*x);
1035 y_min = y_min.min(*y);
1036 y_max = y_max.max(*y);
1037 }
1038 PathSegment::CurveTo {
1039 x1,
1040 y1,
1041 x2,
1042 y2,
1043 x3,
1044 y3,
1045 } => {
1046 x_min = x_min.min(*x1).min(*x2).min(*x3);
1047 x_max = x_max.max(*x1).max(*x2).max(*x3);
1048 y_min = y_min.min(*y1).min(*y2).min(*y3);
1049 y_max = y_max.max(*y1).max(*y2).max(*y3);
1050 }
1051 PathSegment::ClosePath => {}
1052 }
1053 }
1054 if x_min > x_max {
1055 return None;
1056 }
1057
1058 let corners = [
1060 (x_min, y_min),
1061 (x_max, y_min),
1062 (x_min, y_max),
1063 (x_max, y_max),
1064 ];
1065 let mut dev_y_min = f64::INFINITY;
1066 let mut dev_y_max = f64::NEG_INFINITY;
1067 for (x, y) in &corners {
1068 let dy = m.b * x + m.d * y + m.ty;
1069 dev_y_min = dev_y_min.min(dy);
1070 dev_y_max = dev_y_max.max(dy);
1071 }
1072
1073 let col_y_len = (m.c * m.c + m.d * m.d).sqrt().max(1.0);
1076 let expand = effective_lw * col_y_len * params.miter_limit * 0.5;
1077 dev_y_min -= expand;
1078 dev_y_max += expand;
1079
1080 Some(YBBox {
1081 y_min: dev_y_min,
1082 y_max: dev_y_max,
1083 })
1084}
1085
1086fn fill_device_y_bbox(path: &PsPath, ctm: &Matrix) -> Option<YBBox> {
1091 let is_identity = ctm.a == 1.0
1092 && ctm.b == 0.0
1093 && ctm.c == 0.0
1094 && ctm.d == 1.0
1095 && ctm.tx == 0.0
1096 && ctm.ty == 0.0;
1097 if is_identity {
1098 return path_y_bbox(path);
1099 }
1100 let bbox = path_full_bbox(path)?;
1101 let corners = [
1102 (bbox.x_min, bbox.y_min),
1103 (bbox.x_max, bbox.y_min),
1104 (bbox.x_min, bbox.y_max),
1105 (bbox.x_max, bbox.y_max),
1106 ];
1107 let mut dev_y_min = f64::INFINITY;
1108 let mut dev_y_max = f64::NEG_INFINITY;
1109 for (x, y) in &corners {
1110 let dy = ctm.b * x + ctm.d * y + ctm.ty;
1111 dev_y_min = dev_y_min.min(dy);
1112 dev_y_max = dev_y_max.max(dy);
1113 }
1114 Some(YBBox {
1115 y_min: dev_y_min,
1116 y_max: dev_y_max,
1117 })
1118}
1119
1120fn path_y_bbox(path: &PsPath) -> Option<YBBox> {
1122 let mut y_min = f64::INFINITY;
1123 let mut y_max = f64::NEG_INFINITY;
1124 for seg in &path.segments {
1125 match seg {
1126 PathSegment::MoveTo(_, y) | PathSegment::LineTo(_, y) => {
1127 y_min = y_min.min(*y);
1128 y_max = y_max.max(*y);
1129 }
1130 PathSegment::CurveTo { y1, y2, y3, .. } => {
1131 y_min = y_min.min(*y1).min(*y2).min(*y3);
1132 y_max = y_max.max(*y1).max(*y2).max(*y3);
1133 }
1134 PathSegment::ClosePath => {}
1135 }
1136 }
1137 if y_min <= y_max {
1138 Some(YBBox { y_min, y_max })
1139 } else {
1140 None
1141 }
1142}
1143
1144fn image_y_bbox(params: &ImageParams) -> Option<YBBox> {
1146 let image_inv = params.image_matrix.invert()?;
1147 let combined = params.ctm.concat(&image_inv);
1148 let corners = [
1149 (0.0, 0.0),
1150 (params.width as f64, 0.0),
1151 (params.width as f64, params.height as f64),
1152 (0.0, params.height as f64),
1153 ];
1154 let mut y_min = f64::INFINITY;
1155 let mut y_max = f64::NEG_INFINITY;
1156 for (x, y) in &corners {
1157 let (_, dy) = combined.transform_point(*x, *y);
1158 y_min = y_min.min(dy);
1159 y_max = y_max.max(dy);
1160 }
1161 Some(YBBox { y_min, y_max })
1162}
1163
1164fn precompute_clip_seen(list: &DisplayList) -> HashSet<u64> {
1167 let mut counts: HashMap<u64, u32> = HashMap::new();
1168 for elem in list.elements() {
1169 if let DisplayElement::Clip { path, params } = elem {
1170 let hash = hash_clip_path(path, ¶ms.fill_rule);
1171 *counts.entry(hash).or_insert(0) += 1;
1172 }
1173 }
1174 counts
1175 .into_iter()
1176 .filter(|(_, c)| *c > 1)
1177 .map(|(h, _)| h)
1178 .collect()
1179}
1180
1181fn build_clip_epochs(list: &DisplayList, bboxes: &[Option<YBBox>]) -> Vec<ClipEpoch> {
1184 let elements = list.elements();
1185 let mut epochs = Vec::new();
1186 let mut epoch_start = 0;
1187 let mut y_min = f64::INFINITY;
1188 let mut y_max = f64::NEG_INFINITY;
1189 let mut has_erase = false;
1190
1191 for (i, element) in elements.iter().enumerate() {
1192 if matches!(element, DisplayElement::InitClip) && i > epoch_start {
1194 epochs.push(ClipEpoch {
1195 start_idx: epoch_start,
1196 end_idx: i,
1197 paint_bbox: if y_min <= y_max {
1198 Some(YBBox { y_min, y_max })
1199 } else {
1200 None
1201 },
1202 has_erase_page: has_erase,
1203 });
1204 epoch_start = i;
1205 y_min = f64::INFINITY;
1206 y_max = f64::NEG_INFINITY;
1207 has_erase = false;
1208 }
1209 if matches!(element, DisplayElement::ErasePage) {
1210 has_erase = true;
1211 }
1212 if let Some(ref bbox) = bboxes[i] {
1213 y_min = y_min.min(bbox.y_min);
1214 y_max = y_max.max(bbox.y_max);
1215 }
1216 }
1217 if epoch_start < elements.len() {
1219 epochs.push(ClipEpoch {
1220 start_idx: epoch_start,
1221 end_idx: elements.len(),
1222 paint_bbox: if y_min <= y_max {
1223 Some(YBBox { y_min, y_max })
1224 } else {
1225 None
1226 },
1227 has_erase_page: has_erase,
1228 });
1229 }
1230 epochs
1231}
1232
1233fn composite_onto_white(data: &mut [u8]) {
1240 for pixel in data.chunks_exact_mut(4) {
1241 let a = pixel[3] as u16;
1242 if a == 255 {
1243 continue; }
1245 let inv_a = 255 - a;
1246 pixel[0] = (pixel[0] as u16 + inv_a).min(255) as u8;
1247 pixel[1] = (pixel[1] as u16 + inv_a).min(255) as u8;
1248 pixel[2] = (pixel[2] as u16 + inv_a).min(255) as u8;
1249 pixel[3] = 255;
1250 }
1251}
1252
1253fn composite_non_isolated_group_cropped(
1262 target: &mut Pixmap,
1263 source: &Pixmap,
1264 backdrop: &[u8],
1265 params: &stet_graphics::display_list::GroupParams,
1266 clip_mask: Option<&stet_tiny_skia::Mask>,
1267 crop_x: i32,
1268 crop_y: i32,
1269) {
1270 let cw = source.width();
1271 let ch = source.height();
1272
1273 let Some(mut contribution) = Pixmap::new(cw, ch) else {
1275 return;
1276 };
1277 let src_data = source.data();
1278 let contrib_data = contribution.data_mut();
1279
1280 for (i, chunk) in contrib_data.chunks_exact_mut(4).enumerate() {
1281 let off = i * 4;
1282 if src_data[off] != backdrop[off]
1283 || src_data[off + 1] != backdrop[off + 1]
1284 || src_data[off + 2] != backdrop[off + 2]
1285 || src_data[off + 3] != backdrop[off + 3]
1286 {
1287 chunk.copy_from_slice(&src_data[off..off + 4]);
1288 }
1289 }
1290
1291 let paint = stet_tiny_skia::PixmapPaint {
1292 opacity: params.alpha as f32,
1293 blend_mode: u8_to_blend_mode(params.blend_mode),
1294 quality: stet_tiny_skia::FilterQuality::Nearest,
1295 };
1296 target.draw_pixmap(
1297 crop_x,
1298 crop_y,
1299 contribution.as_ref(),
1300 &paint,
1301 Transform::identity(),
1302 clip_mask,
1303 );
1304}
1305
1306fn composite_non_isolated_extracted(
1319 target: &mut Pixmap,
1320 source: &Pixmap,
1321 isolated: &Pixmap,
1322 backdrop: &[u8],
1323 params: &stet_graphics::display_list::GroupParams,
1324 clip_mask: Option<&stet_tiny_skia::Mask>,
1325 crop_x: i32,
1326 crop_y: i32,
1327) {
1328 let cw = source.width();
1329 let ch = source.height();
1330
1331 let Some(mut contribution) = Pixmap::new(cw, ch) else {
1332 return;
1333 };
1334 let src_data = source.data();
1335 let iso_data = isolated.data();
1336 let contrib_data = contribution.data_mut();
1337
1338 for i in 0..(cw as usize * ch as usize) {
1339 let off = i * 4;
1340 let alpha_g = iso_data[off + 3];
1341 if alpha_g == 0 {
1342 continue; }
1344
1345 let inv_alpha = 255 - alpha_g as i32;
1347 for c in 0..3 {
1348 let r = src_data[off + c] as i32;
1349 let b = backdrop[off + c] as i32;
1350 let raw = r - (b * inv_alpha + 127) / 255;
1351 contrib_data[off + c] = raw.clamp(0, 255) as u8;
1352 }
1353 contrib_data[off + 3] = alpha_g;
1354 }
1355
1356 let paint = stet_tiny_skia::PixmapPaint {
1357 opacity: params.alpha as f32,
1358 blend_mode: u8_to_blend_mode(params.blend_mode),
1359 quality: stet_tiny_skia::FilterQuality::Nearest,
1360 };
1361 target.draw_pixmap(
1362 crop_x,
1363 crop_y,
1364 contribution.as_ref(),
1365 &paint,
1366 Transform::identity(),
1367 clip_mask,
1368 );
1369}
1370
1371fn viewport_transform(t: Transform, vp_x: f32, vp_y: f32, scale_x: f32, scale_y: f32) -> Transform {
1376 Transform::from_row(
1378 t.sx * scale_x,
1379 t.ky * scale_y,
1380 t.kx * scale_x,
1381 t.sy * scale_y,
1382 (t.tx - vp_x) * scale_x,
1383 (t.ty - vp_y) * scale_y,
1384 )
1385}
1386
1387fn box_resample(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
1394 if dw == 0 || dh == 0 {
1395 return Vec::new();
1396 }
1397 let (sw, sh, dw, dh) = (sw as usize, sh as usize, dw as usize, dh as usize);
1398
1399 let ratio_x = sw as f32 / dw as f32;
1403 let mut tmp = vec![0.0f32; dw * sh * 4];
1404 let tmp_stride = dw * 4;
1405
1406 for y in 0..sh {
1407 let row_off = y * sw * 4;
1408 let dst_row = y * tmp_stride;
1409 for dx in 0..dw {
1410 let left_f = dx as f32 * ratio_x;
1411 let right_f = (dx + 1) as f32 * ratio_x;
1412 let left = (left_f as usize).min(sw - 1);
1413 let right = (right_f.ceil() as usize).min(sw);
1414 let inv_area = 1.0 / (right_f - left_f);
1415 let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0, 0.0, 0.0);
1416 for sx in left..right {
1417 let pixel_left = sx as f32;
1419 let pixel_right = (sx + 1) as f32;
1420 let w = pixel_right.min(right_f) - pixel_left.max(left_f);
1421 let i = row_off + sx * 4;
1422 r += src[i] as f32 * w;
1423 g += src[i + 1] as f32 * w;
1424 b += src[i + 2] as f32 * w;
1425 a += src[i + 3] as f32 * w;
1426 }
1427 let di = dst_row + dx * 4;
1428 tmp[di] = r * inv_area;
1429 tmp[di + 1] = g * inv_area;
1430 tmp[di + 2] = b * inv_area;
1431 tmp[di + 3] = a * inv_area;
1432 }
1433 }
1434
1435 let ratio_y = sh as f32 / dh as f32;
1437 let mut out = vec![0u8; dw * dh * 4];
1438 let out_stride = dw * 4;
1439
1440 for dy in 0..dh {
1441 let top_f = dy as f32 * ratio_y;
1442 let bottom_f = (dy + 1) as f32 * ratio_y;
1443 let top = (top_f as usize).min(sh - 1);
1444 let bottom = (bottom_f.ceil() as usize).min(sh);
1445 let inv_area = 1.0 / (bottom_f - top_f);
1446
1447 let n_rows = bottom - top;
1449 let mut row_weights_buf: [(usize, f32); 8] = [(0, 0.0); 8];
1450 let row_weights_vec: Vec<(usize, f32)>;
1451 let row_weights: &[(usize, f32)] = if n_rows <= 8 {
1452 for (i, sy) in (top..bottom).enumerate() {
1453 let pixel_top = sy as f32;
1454 let pixel_bottom = (sy + 1) as f32;
1455 let w = pixel_bottom.min(bottom_f) - pixel_top.max(top_f);
1456 row_weights_buf[i] = (sy, w);
1457 }
1458 &row_weights_buf[..n_rows]
1459 } else {
1460 row_weights_vec = (top..bottom)
1461 .map(|sy| {
1462 let pixel_top = sy as f32;
1463 let pixel_bottom = (sy + 1) as f32;
1464 let w = pixel_bottom.min(bottom_f) - pixel_top.max(top_f);
1465 (sy, w)
1466 })
1467 .collect();
1468 &row_weights_vec
1469 };
1470
1471 let dst_row = dy * out_stride;
1472 for dx in 0..dw {
1473 let col = dx * 4;
1474 let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0, 0.0, 0.0);
1475 for &(sy, w) in row_weights {
1476 let i = sy * tmp_stride + col;
1477 r += tmp[i] * w;
1478 g += tmp[i + 1] * w;
1479 b += tmp[i + 2] * w;
1480 a += tmp[i + 3] * w;
1481 }
1482 let di = dst_row + col;
1483 out[di] = (r * inv_area + 0.5).clamp(0.0, 255.0) as u8;
1484 out[di + 1] = (g * inv_area + 0.5).clamp(0.0, 255.0) as u8;
1485 out[di + 2] = (b * inv_area + 0.5).clamp(0.0, 255.0) as u8;
1486 out[di + 3] = (a * inv_area + 0.5).clamp(0.0, 255.0) as u8;
1487 }
1488 }
1489
1490 out
1491}
1492
1493fn bicubic_resample(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
1500 if dw == 0 || dh == 0 {
1501 return Vec::new();
1502 }
1503
1504 let (sw, sh, dw, dh) = (sw as usize, sh as usize, dw as usize, dh as usize);
1505 let ratio_x = sw as f32 / dw as f32;
1506 let ratio_y = sh as f32 / dh as f32;
1507
1508 let mut tmp = vec![0.0f32; dw * sh * 4];
1510 for y in 0..sh {
1511 let src_row = y * sw * 4;
1512 let dst_row = y * dw * 4;
1513 for dx in 0..dw {
1514 let sx = (dx as f32 + 0.5) * ratio_x - 0.5;
1515 let sx_floor = sx.floor() as i32;
1516 let fx = sx - sx_floor as f32;
1517 let w0 = catmull_rom(fx + 1.0);
1518 let w1 = catmull_rom(fx);
1519 let w2 = catmull_rom(1.0 - fx);
1520 let w3 = catmull_rom(2.0 - fx);
1521 let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0, 0.0, 0.0);
1522 for (k, w) in [
1523 (sx_floor - 1, w0),
1524 (sx_floor, w1),
1525 (sx_floor + 1, w2),
1526 (sx_floor + 2, w3),
1527 ] {
1528 let px = k.clamp(0, sw as i32 - 1) as usize;
1529 let i = src_row + px * 4;
1530 r += src[i] as f32 * w;
1531 g += src[i + 1] as f32 * w;
1532 b += src[i + 2] as f32 * w;
1533 a += src[i + 3] as f32 * w;
1534 }
1535 let di = dst_row + dx * 4;
1536 tmp[di] = r;
1537 tmp[di + 1] = g;
1538 tmp[di + 2] = b;
1539 tmp[di + 3] = a;
1540 }
1541 }
1542
1543 let mut out = vec![0u8; dw * dh * 4];
1546 let tmp_stride = dw * 4;
1547 let out_stride = dw * 4;
1548 for dy in 0..dh {
1549 let sy = (dy as f32 + 0.5) * ratio_y - 0.5;
1550 let sy_floor = sy.floor() as i32;
1551 let fy = sy - sy_floor as f32;
1552 let w0 = catmull_rom(fy + 1.0);
1553 let w1 = catmull_rom(fy);
1554 let w2 = catmull_rom(1.0 - fy);
1555 let w3 = catmull_rom(2.0 - fy);
1556 let py0 = (sy_floor - 1).clamp(0, sh as i32 - 1) as usize * tmp_stride;
1557 let py1 = sy_floor.clamp(0, sh as i32 - 1) as usize * tmp_stride;
1558 let py2 = (sy_floor + 1).clamp(0, sh as i32 - 1) as usize * tmp_stride;
1559 let py3 = (sy_floor + 2).clamp(0, sh as i32 - 1) as usize * tmp_stride;
1560 let dst_row = dy * out_stride;
1561 for dx in 0..dw {
1562 let col = dx * 4;
1563 let r = tmp[py0 + col] * w0
1564 + tmp[py1 + col] * w1
1565 + tmp[py2 + col] * w2
1566 + tmp[py3 + col] * w3;
1567 let g = tmp[py0 + col + 1] * w0
1568 + tmp[py1 + col + 1] * w1
1569 + tmp[py2 + col + 1] * w2
1570 + tmp[py3 + col + 1] * w3;
1571 let b = tmp[py0 + col + 2] * w0
1572 + tmp[py1 + col + 2] * w1
1573 + tmp[py2 + col + 2] * w2
1574 + tmp[py3 + col + 2] * w3;
1575 let a = tmp[py0 + col + 3] * w0
1576 + tmp[py1 + col + 3] * w1
1577 + tmp[py2 + col + 3] * w2
1578 + tmp[py3 + col + 3] * w3;
1579 let di = dst_row + col;
1580 out[di] = r.round().clamp(0.0, 255.0) as u8;
1581 out[di + 1] = g.round().clamp(0.0, 255.0) as u8;
1582 out[di + 2] = b.round().clamp(0.0, 255.0) as u8;
1583 out[di + 3] = a.round().clamp(0.0, 255.0) as u8;
1584 }
1585 }
1586
1587 out
1588}
1589
1590#[inline]
1592fn catmull_rom(t: f32) -> f32 {
1593 let t = t.abs();
1594 if t < 1.0 {
1595 (1.5 * t - 2.5) * t * t + 1.0
1596 } else if t < 2.0 {
1597 ((-0.5 * t + 2.5) * t - 4.0) * t + 2.0
1598 } else {
1599 0.0
1600 }
1601}
1602
1603pub fn build_icc_cache_for_list(
1621 list: &DisplayList,
1622 system_cmyk_bytes: Option<&std::sync::Arc<Vec<u8>>>,
1623 proofing_enabled: bool,
1624) -> IccCache {
1625 let mut cache = IccCache::new();
1626 let mut seen = HashSet::new();
1627
1628 if let Some(cmyk_bytes) = system_cmyk_bytes
1631 && let Some(hash) = cache.register_profile(cmyk_bytes)
1632 {
1633 seen.insert(hash);
1634 cache.set_default_cmyk_hash(hash);
1636 cache.prepare_reverse_cmyk();
1640 cache.prepare_lab_to_oi_cmyk();
1644 }
1645
1646 cache.set_proofing_enabled(proofing_enabled);
1651
1652 fn scan_elements(
1654 elements: &[DisplayElement],
1655 seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
1656 cache: &mut IccCache,
1657 ) {
1658 for element in elements {
1659 if let DisplayElement::Group { elements: sub, .. } = element {
1661 scan_elements(sub.elements(), seen, cache);
1662 }
1663 if let DisplayElement::SoftMasked { content, mask, .. } = element {
1664 scan_elements(content.elements(), seen, cache);
1665 scan_elements(mask.elements(), seen, cache);
1666 }
1667 if let DisplayElement::OcgGroup { elements: sub, .. } = element {
1668 scan_elements(sub.elements(), seen, cache);
1669 }
1670 let shading_cs = match element {
1672 DisplayElement::AxialShading { params } => Some(¶ms.color_space),
1673 DisplayElement::RadialShading { params } => Some(¶ms.color_space),
1674 DisplayElement::MeshShading { params } => Some(¶ms.color_space),
1675 DisplayElement::PatchShading { params } => Some(¶ms.color_space),
1676 _ => None,
1677 };
1678 if let Some(stet_graphics::device::ShadingColorSpace::ICCBased {
1679 n,
1680 profile_hash,
1681 profile_data,
1682 }) = shading_cs
1683 {
1684 if seen.insert(*profile_hash) {
1685 cache.register_profile_with_n(profile_data, Some(*n));
1686 }
1687 }
1688 if let DisplayElement::Image { params, .. } = element {
1690 match ¶ms.color_space {
1691 ImageColorSpace::ICCBased {
1692 n,
1693 profile_hash,
1694 profile_data,
1695 } if seen.insert(*profile_hash) => {
1696 cache.register_profile_with_n(profile_data, Some(*n));
1697 }
1698 ImageColorSpace::Indexed { base, .. }
1699 if matches!(base.as_ref(), ImageColorSpace::ICCBased { .. }) =>
1700 {
1701 if let ImageColorSpace::ICCBased {
1702 n,
1703 profile_hash,
1704 profile_data,
1705 } = base.as_ref()
1706 {
1707 if seen.insert(*profile_hash) {
1708 cache.register_profile_with_n(profile_data, Some(*n));
1709 }
1710 }
1711 }
1712 _ => {}
1713 }
1714 }
1715 }
1716 }
1717 scan_elements(list.elements(), &mut seen, &mut cache);
1718
1719 cache
1720}
1721
1722fn register_shading_icc_profiles(list: &DisplayList, cache: &mut IccCache) {
1727 fn register_image_iccs(
1728 cs: &ImageColorSpace,
1729 seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
1730 cache: &mut IccCache,
1731 ) {
1732 match cs {
1733 ImageColorSpace::ICCBased {
1734 n,
1735 profile_hash,
1736 profile_data,
1737 } => {
1738 if seen.insert(*profile_hash) {
1739 cache.register_profile_with_n(profile_data, Some(*n));
1740 }
1741 }
1742 ImageColorSpace::Indexed { base, .. } => register_image_iccs(base, seen, cache),
1743 ImageColorSpace::Separation { alt_space, .. }
1744 | ImageColorSpace::DeviceN { alt_space, .. } => {
1745 register_image_iccs(alt_space, seen, cache)
1746 }
1747 _ => {}
1748 }
1749 }
1750 fn scan(
1751 elements: &[DisplayElement],
1752 seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
1753 cache: &mut IccCache,
1754 ) {
1755 for element in elements {
1756 if let DisplayElement::Group { elements: sub, .. } = element {
1757 scan(sub.elements(), seen, cache);
1758 }
1759 if let DisplayElement::SoftMasked { content, mask, .. } = element {
1760 scan(content.elements(), seen, cache);
1761 scan(mask.elements(), seen, cache);
1762 }
1763 if let DisplayElement::OcgGroup { elements: sub, .. } = element {
1764 scan(sub.elements(), seen, cache);
1765 }
1766 let shading_cs = match element {
1767 DisplayElement::AxialShading { params } => Some(¶ms.color_space),
1768 DisplayElement::RadialShading { params } => Some(¶ms.color_space),
1769 DisplayElement::MeshShading { params } => Some(¶ms.color_space),
1770 DisplayElement::PatchShading { params } => Some(¶ms.color_space),
1771 _ => None,
1772 };
1773 if let Some(stet_graphics::device::ShadingColorSpace::ICCBased {
1774 n,
1775 profile_hash,
1776 profile_data,
1777 }) = shading_cs
1778 && seen.insert(*profile_hash)
1779 {
1780 cache.register_profile_with_n(profile_data, Some(*n));
1781 }
1782 if let DisplayElement::Image { params, .. } = element {
1783 register_image_iccs(¶ms.color_space, seen, cache);
1784 }
1785 }
1786 }
1787 let mut seen = HashSet::new();
1788 scan(list.elements(), &mut seen, cache);
1789}
1790
1791fn samples_to_rgba(
1795 data: &[u8],
1796 params: &ImageParams,
1797 icc: Option<&IccCache>,
1798 opm_zero_transparent: bool,
1799) -> Vec<u8> {
1800 let w = params.width as usize;
1801 let h = params.height as usize;
1802 let npixels = w * h;
1803 let bpc = params.bits_per_component;
1804 match ¶ms.color_space {
1805 ImageColorSpace::PreconvertedRGBA => {
1806 data.to_vec()
1808 }
1809 ImageColorSpace::DeviceGray => {
1810 let mut rgba = vec![255u8; npixels * 4];
1811 if bpc == 16 {
1812 for i in 0..npixels {
1813 let g = data.get(i * 2).copied().unwrap_or(0);
1814 let pi = i * 4;
1815 rgba[pi] = g;
1816 rgba[pi + 1] = g;
1817 rgba[pi + 2] = g;
1818 }
1819 } else {
1820 for i in 0..npixels {
1821 let g = data.get(i).copied().unwrap_or(0);
1822 let pi = i * 4;
1823 rgba[pi] = g;
1824 rgba[pi + 1] = g;
1825 rgba[pi + 2] = g;
1826 }
1827 }
1828 rgba
1829 }
1830 ImageColorSpace::DeviceRGB => {
1831 let mut rgba = vec![255u8; npixels * 4];
1832 if bpc == 16 {
1833 for i in 0..npixels {
1836 let si = i * 6;
1837 let pi = i * 4;
1838 rgba[pi] = data.get(si).copied().unwrap_or(0);
1839 rgba[pi + 1] = data.get(si + 2).copied().unwrap_or(0);
1840 rgba[pi + 2] = data.get(si + 4).copied().unwrap_or(0);
1841 }
1842 } else {
1843 for i in 0..npixels {
1844 let si = i * 3;
1845 let pi = i * 4;
1846 rgba[pi] = data.get(si).copied().unwrap_or(0);
1847 rgba[pi + 1] = data.get(si + 1).copied().unwrap_or(0);
1848 rgba[pi + 2] = data.get(si + 2).copied().unwrap_or(0);
1849 }
1850 }
1851 rgba
1852 }
1853 ImageColorSpace::DeviceCMYK => {
1854 if let Some(cache) = icc
1858 && let Some(cmyk_hash) = cache.default_cmyk_hash()
1859 {
1860 let avail_pixels = data.len() / 4;
1861 let icc_pixels = avail_pixels.min(npixels);
1862 if icc_pixels > 0
1863 && let Some(rgb) = cache.convert_image_8bit(cmyk_hash, data, icc_pixels)
1864 {
1865 let mut rgba = vec![255u8; npixels * 4];
1866 for i in 0..icc_pixels {
1867 rgba[i * 4] = rgb[i * 3];
1868 rgba[i * 4 + 1] = rgb[i * 3 + 1];
1869 rgba[i * 4 + 2] = rgb[i * 3 + 2];
1870 if opm_zero_transparent {
1872 let si = i * 4;
1873 if data[si] == 0
1874 && data[si + 1] == 0
1875 && data[si + 2] == 0
1876 && data[si + 3] == 0
1877 {
1878 rgba[i * 4 + 3] = 0;
1879 }
1880 }
1881 }
1882 return rgba;
1884 }
1885 }
1886 let mut rgba = vec![255u8; npixels * 4];
1888 for i in 0..npixels {
1889 let si = i * 4;
1890 let c = data.get(si).copied().unwrap_or(0) as f64 / 255.0;
1891 let m = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0;
1892 let y = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0;
1893 let k = data.get(si + 3).copied().unwrap_or(0) as f64 / 255.0;
1894 let r = (1.0 - c.min(1.0)) * (1.0 - k.min(1.0));
1895 let g = (1.0 - m.min(1.0)) * (1.0 - k.min(1.0));
1896 let b = (1.0 - y.min(1.0)) * (1.0 - k.min(1.0));
1897 let pi = i * 4;
1898 rgba[pi] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
1899 rgba[pi + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
1900 rgba[pi + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
1901 if opm_zero_transparent
1903 && data.get(si).copied().unwrap_or(0) == 0
1904 && data.get(si + 1).copied().unwrap_or(0) == 0
1905 && data.get(si + 2).copied().unwrap_or(0) == 0
1906 && data.get(si + 3).copied().unwrap_or(0) == 0
1907 {
1908 rgba[pi + 3] = 0;
1909 }
1910 }
1911 rgba
1912 }
1913 ImageColorSpace::ICCBased {
1914 n,
1915 profile_hash,
1916 profile_data,
1917 } => {
1918 let intent = stet_graphics::icc::intent_from_pdf_byte(params.rendering_intent);
1929 if let Some(cache) = icc
1930 && cache.has_profile(profile_hash)
1931 && let Some(rgb) =
1932 cache.convert_image_8bit_with_intent(profile_hash, data, npixels, intent)
1933 {
1934 let mut rgba = vec![255u8; npixels * 4];
1935 for i in 0..npixels {
1936 rgba[i * 4] = rgb[i * 3];
1937 rgba[i * 4 + 1] = rgb[i * 3 + 1];
1938 rgba[i * 4 + 2] = rgb[i * 3 + 2];
1939 if opm_zero_transparent && *n == 4 {
1941 let si = i * *n as usize;
1942 if si + 3 < data.len()
1943 && data[si] == 0
1944 && data[si + 1] == 0
1945 && data[si + 2] == 0
1946 && data[si + 3] == 0
1947 {
1948 rgba[i * 4 + 3] = 0;
1949 }
1950 }
1951 }
1952 return rgba;
1953 }
1954 let _ = (profile_hash, profile_data);
1956 let fallback = match n {
1957 1 => ImageColorSpace::DeviceGray,
1958 4 => ImageColorSpace::DeviceCMYK,
1959 _ => ImageColorSpace::DeviceRGB,
1960 };
1961 let p = ImageParams {
1962 color_space: fallback,
1963 bits_per_component: 8,
1964 ..params.clone()
1965 };
1966 samples_to_rgba(data, &p, icc, opm_zero_transparent)
1967 }
1968 ImageColorSpace::Indexed {
1969 base,
1970 hival,
1971 lookup,
1972 } => {
1973 let base_ncomp = base.num_components() as usize;
1974 let mut expanded = Vec::with_capacity(npixels * base_ncomp);
1976 for i in 0..npixels {
1977 let idx = data.get(i).copied().unwrap_or(0) as usize;
1978 let idx = idx.min(*hival as usize);
1979 let offset = idx * base_ncomp;
1980 for c in 0..base_ncomp {
1981 expanded.push(lookup.get(offset + c).copied().unwrap_or(0));
1982 }
1983 }
1984 let p = ImageParams {
1985 color_space: *base.clone(),
1986 bits_per_component: 8,
1987 ..params.clone()
1988 };
1989 samples_to_rgba(&expanded, &p, icc, opm_zero_transparent)
1990 }
1991 ImageColorSpace::CIEBasedABC { params: cie_params } => {
1992 let mut rgba = vec![255u8; npixels * 4];
1993 for i in 0..npixels {
1994 let si = i * 3;
1995 let a = data.get(si).copied().unwrap_or(0) as f64 / 255.0;
1996 let b = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0;
1997 let c = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0;
1998 let color = DeviceColor::from_cie_abc(a, b, c, cie_params);
1999 let pi = i * 4;
2000 rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
2001 rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
2002 rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
2003 }
2004 rgba
2005 }
2006 ImageColorSpace::CIEBasedA { params: cie_params } => {
2007 let mut rgba = vec![255u8; npixels * 4];
2008 for i in 0..npixels {
2009 let val = data.get(i).copied().unwrap_or(0) as f64 / 255.0;
2010 let color = DeviceColor::from_cie_a(val, cie_params);
2011 let pi = i * 4;
2012 rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
2013 rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
2014 rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
2015 }
2016 rgba
2017 }
2018 ImageColorSpace::Lab { range, .. } => {
2019 let mut rgba = vec![255u8; npixels * 4];
2020 let a_span = range[1] - range[0];
2021 let b_span = range[3] - range[2];
2022 for i in 0..npixels {
2023 let si = i * 3;
2024 let l = data.get(si).copied().unwrap_or(0) as f64 / 255.0 * 100.0;
2025 let a = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0 * a_span + range[0];
2026 let b = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0 * b_span + range[2];
2027 let color = DeviceColor::from_lab(l, a, b, range);
2028 let pi = i * 4;
2029 rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
2030 rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
2031 rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
2032 }
2033 rgba
2034 }
2035 ImageColorSpace::Separation {
2036 alt_space,
2037 tint_table,
2038 ..
2039 } => {
2040 if matches!(alt_space.as_ref(), ImageColorSpace::DeviceCMYK)
2043 && let Some(rgba) = tint_separation_via_icc(data, npixels, tint_table, icc)
2044 {
2045 return rgba;
2046 }
2047 let mut rgba = vec![255u8; npixels * 4];
2048 let no = tint_table.num_outputs as usize;
2049 let mut alt_comps = vec![0.0f32; no];
2050 for i in 0..npixels {
2051 let tint = data.get(i).copied().unwrap_or(0) as f32 / 255.0;
2052 tint_table.lookup_1d(tint, &mut alt_comps);
2053 let (r, g, b) = alt_comps_to_rgb(&alt_comps, alt_space);
2054 let pi = i * 4;
2055 rgba[pi] = r;
2056 rgba[pi + 1] = g;
2057 rgba[pi + 2] = b;
2058 }
2059 rgba
2060 }
2061 ImageColorSpace::DeviceN {
2062 alt_space,
2063 tint_table,
2064 ..
2065 } => {
2066 let ni = tint_table.num_inputs as usize;
2067 let no = tint_table.num_outputs as usize;
2068 if matches!(alt_space.as_ref(), ImageColorSpace::DeviceCMYK)
2070 && let Some(rgba) = tint_devicen_via_icc(data, npixels, ni, tint_table, icc)
2071 {
2072 return rgba;
2073 }
2074 let mut rgba = vec![255u8; npixels * 4];
2075 let mut inputs = vec![0.0f32; ni];
2076 let mut alt_comps = vec![0.0f32; no];
2077 for i in 0..npixels {
2078 let si = i * ni;
2079 for (c, inp) in inputs.iter_mut().enumerate() {
2080 *inp = data.get(si + c).copied().unwrap_or(0) as f32 / 255.0;
2081 }
2082 tint_table.lookup_nd(&inputs, &mut alt_comps);
2083 let (r, g, b) = alt_comps_to_rgb(&alt_comps, alt_space);
2084 let pi = i * 4;
2085 rgba[pi] = r;
2086 rgba[pi + 1] = g;
2087 rgba[pi + 2] = b;
2088 }
2089 rgba
2090 }
2091 ImageColorSpace::Mask {
2092 color, polarity, ..
2093 } => {
2094 let mut rgba = vec![0u8; npixels * 4];
2095 let r = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
2096 let g = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
2097 let b = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
2098 let bytes_per_row = (w).div_ceil(8);
2099 for row in 0..h {
2100 for col in 0..w {
2101 let byte_idx = row * bytes_per_row + col / 8;
2102 let bit_offset = 7 - (col % 8);
2103 let bit = if byte_idx < data.len() {
2104 (data[byte_idx] >> bit_offset) & 1
2105 } else {
2106 0
2107 };
2108 let paint = if *polarity { bit == 1 } else { bit == 0 };
2109 if paint {
2110 let pi = (row * w + col) * 4;
2111 rgba[pi] = r;
2112 rgba[pi + 1] = g;
2113 rgba[pi + 2] = b;
2114 rgba[pi + 3] = 255;
2115 }
2116 }
2117 }
2118 rgba
2119 }
2120 _ => vec![0u8; npixels * 4],
2121 }
2122}
2123
2124fn tint_separation_via_icc(
2127 data: &[u8],
2128 npixels: usize,
2129 tint_table: &TintLookupTable,
2130 icc: Option<&IccCache>,
2131) -> Option<Vec<u8>> {
2132 let cache = icc?;
2133 let cmyk_hash = cache.default_cmyk_hash()?;
2134 let mut cmyk_data = vec![0u8; npixels * 4];
2136 let mut alt_comps = [0.0f32; 4];
2137 for i in 0..npixels {
2138 let tint = data.get(i).copied().unwrap_or(0) as f32 / 255.0;
2139 tint_table.lookup_1d(tint, &mut alt_comps);
2140 let si = i * 4;
2141 cmyk_data[si] = (alt_comps[0].clamp(0.0, 1.0) * 255.0).round() as u8;
2142 cmyk_data[si + 1] = (alt_comps[1].clamp(0.0, 1.0) * 255.0).round() as u8;
2143 cmyk_data[si + 2] = (alt_comps[2].clamp(0.0, 1.0) * 255.0).round() as u8;
2144 cmyk_data[si + 3] = (alt_comps[3].clamp(0.0, 1.0) * 255.0).round() as u8;
2145 }
2146 let rgb = cache.convert_image_8bit(cmyk_hash, &cmyk_data, npixels)?;
2147 let mut rgba = vec![255u8; npixels * 4];
2148 for i in 0..npixels {
2149 rgba[i * 4] = rgb[i * 3];
2150 rgba[i * 4 + 1] = rgb[i * 3 + 1];
2151 rgba[i * 4 + 2] = rgb[i * 3 + 2];
2152 }
2153 Some(rgba)
2154}
2155
2156fn tint_devicen_via_icc(
2158 data: &[u8],
2159 npixels: usize,
2160 ni: usize,
2161 tint_table: &TintLookupTable,
2162 icc: Option<&IccCache>,
2163) -> Option<Vec<u8>> {
2164 let cache = icc?;
2165 let cmyk_hash = cache.default_cmyk_hash()?;
2166 let mut cmyk_data = vec![0u8; npixels * 4];
2167 let mut inputs = vec![0.0f32; ni];
2168 let mut alt_comps = [0.0f32; 4];
2169 for i in 0..npixels {
2170 let si = i * ni;
2171 for (c, inp) in inputs.iter_mut().enumerate() {
2172 *inp = data.get(si + c).copied().unwrap_or(0) as f32 / 255.0;
2173 }
2174 tint_table.lookup_nd(&inputs, &mut alt_comps);
2175 let di = i * 4;
2176 cmyk_data[di] = (alt_comps[0].clamp(0.0, 1.0) * 255.0).round() as u8;
2177 cmyk_data[di + 1] = (alt_comps[1].clamp(0.0, 1.0) * 255.0).round() as u8;
2178 cmyk_data[di + 2] = (alt_comps[2].clamp(0.0, 1.0) * 255.0).round() as u8;
2179 cmyk_data[di + 3] = (alt_comps[3].clamp(0.0, 1.0) * 255.0).round() as u8;
2180 }
2181 let rgb = cache.convert_image_8bit(cmyk_hash, &cmyk_data, npixels)?;
2182 let mut rgba = vec![255u8; npixels * 4];
2183 for i in 0..npixels {
2184 rgba[i * 4] = rgb[i * 3];
2185 rgba[i * 4 + 1] = rgb[i * 3 + 1];
2186 rgba[i * 4 + 2] = rgb[i * 3 + 2];
2187 }
2188 Some(rgba)
2189}
2190
2191fn alt_comps_to_rgb(comps: &[f32], alt_space: &ImageColorSpace) -> (u8, u8, u8) {
2193 match alt_space {
2194 ImageColorSpace::DeviceGray => {
2195 let g = (comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2196 (g, g, g)
2197 }
2198 ImageColorSpace::DeviceRGB => {
2199 let r = (comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2200 let g = (comps.get(1).copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2201 let b = (comps.get(2).copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
2202 (r, g, b)
2203 }
2204 ImageColorSpace::DeviceCMYK => {
2205 let c = comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0);
2206 let m = comps.get(1).copied().unwrap_or(0.0).clamp(0.0, 1.0);
2207 let y = comps.get(2).copied().unwrap_or(0.0).clamp(0.0, 1.0);
2208 let k = comps.get(3).copied().unwrap_or(0.0).clamp(0.0, 1.0);
2209 let r = ((1.0 - (c + k).min(1.0)) * 255.0).round() as u8;
2210 let g = ((1.0 - (m + k).min(1.0)) * 255.0).round() as u8;
2211 let b = ((1.0 - (y + k).min(1.0)) * 255.0).round() as u8;
2212 (r, g, b)
2213 }
2214 _ => (0, 0, 0),
2215 }
2216}
2217
2218fn apply_mask_color_rgba(rgba: &mut [u8], sample_data: &[u8], params: &ImageParams) {
2220 let mask_color = match ¶ms.mask_color {
2221 Some(mc) => mc,
2222 None => return,
2223 };
2224 let ncomp = params.color_space.num_components() as usize;
2225 let npixels = params.width as usize * params.height as usize;
2226 let is_range = mask_color.len() == 2 * ncomp;
2227
2228 for i in 0..npixels {
2229 let si = i * ncomp;
2230 let matched = if is_range {
2231 (0..ncomp).all(|c| {
2232 let sample = sample_data.get(si + c).copied().unwrap_or(0);
2233 let min_val = mask_color.get(c * 2).copied().unwrap_or(0);
2234 let max_val = mask_color.get(c * 2 + 1).copied().unwrap_or(0);
2235 sample >= min_val && sample <= max_val
2236 })
2237 } else {
2238 (0..ncomp).all(|c| {
2239 let sample = sample_data.get(si + c).copied().unwrap_or(0);
2240 let target = mask_color.get(c).copied().unwrap_or(0);
2241 sample == target
2242 })
2243 };
2244 if matched {
2245 let pi = i * 4;
2246 if pi + 3 < rgba.len() {
2247 rgba[pi] = 0;
2248 rgba[pi + 1] = 0;
2249 rgba[pi + 2] = 0;
2250 rgba[pi + 3] = 0;
2251 }
2252 }
2253 }
2254}
2255
2256fn image_filter_quality(transform: Transform, interpolate: bool) -> stet_tiny_skia::FilterQuality {
2262 let eff_sx = (transform.sx * transform.sx + transform.ky * transform.ky).sqrt();
2263 let eff_sy = (transform.kx * transform.kx + transform.sy * transform.sy).sqrt();
2264 let min_scale = eff_sx.min(eff_sy);
2265 if (eff_sx - 1.0).abs() < 0.01 && (eff_sy - 1.0).abs() < 0.01 {
2267 stet_tiny_skia::FilterQuality::Nearest
2268 } else if !interpolate && min_scale >= 0.95 {
2269 stet_tiny_skia::FilterQuality::Nearest
2271 } else {
2272 stet_tiny_skia::FilterQuality::Bilinear
2273 }
2274}
2275
2276fn prescale_image(
2281 rgba_data: &[u8],
2282 w: u32,
2283 h: u32,
2284 transform: Transform,
2285 interpolate: bool,
2286) -> Option<(Vec<u8>, u32, u32, Transform)> {
2287 let scale_x = (transform.sx * transform.sx + transform.ky * transform.ky).sqrt();
2289 let scale_y = (transform.kx * transform.kx + transform.sy * transform.sy).sqrt();
2290 let min_scale = scale_x.min(scale_y);
2291
2292 if min_scale > 1.05 {
2296 if interpolate {
2297 let is_axis_aligned = transform.kx.abs() < 1e-4 && transform.ky.abs() < 1e-4;
2298 if is_axis_aligned && w >= 2 && h >= 2 {
2299 let dw = (w as f32 * transform.sx.abs()).round().max(1.0) as u32;
2300 let dh = (h as f32 * transform.sy.abs()).round().max(1.0) as u32;
2301 if dw > w || dh > h {
2302 let resampled = bicubic_resample(rgba_data, w, h, dw, dh);
2303 let new_sx = transform.sx * w as f32 / dw as f32;
2304 let new_sy = transform.sy * h as f32 / dh as f32;
2305 let adjusted = Transform::from_row(
2306 new_sx,
2307 transform.ky,
2308 transform.kx,
2309 new_sy,
2310 transform.tx,
2311 transform.ty,
2312 );
2313 return Some((resampled, dw, dh, adjusted));
2314 }
2315 }
2316 }
2317 return None;
2318 }
2319
2320 if min_scale >= 0.95 {
2322 return None;
2323 }
2324
2325 let is_axis_aligned = transform.kx.abs() < 1e-4 && transform.ky.abs() < 1e-4;
2328 if is_axis_aligned && w >= 2 && h >= 2 {
2329 let dw = (w as f32 * transform.sx.abs()).ceil().max(1.0) as u32;
2330 let dh = (h as f32 * transform.sy.abs()).ceil().max(1.0) as u32;
2331 if dw < w || dh < h {
2332 let resampled = box_resample(rgba_data, w, h, dw, dh);
2333 let new_sx = transform.sx * w as f32 / dw as f32;
2335 let new_sy = transform.sy * h as f32 / dh as f32;
2336 let adjusted = Transform::from_row(
2337 new_sx,
2338 transform.ky,
2339 transform.kx,
2340 new_sy,
2341 transform.tx,
2342 transform.ty,
2343 );
2344 return Some((resampled, dw, dh, adjusted));
2345 }
2346 }
2347
2348 let factor = (1.0 / min_scale) as u32;
2350 if factor < 2 || w < factor || h < factor {
2351 return None;
2352 }
2353 let nw = w / factor;
2354 let nh = h / factor;
2355 if nw == 0 || nh == 0 {
2356 return None;
2357 }
2358 let area = factor * factor;
2359 let half = area / 2;
2360 let stride = w as usize * 4;
2361 let mut out = vec![0u8; (nw * nh * 4) as usize];
2362 for dy in 0..nh {
2363 for dx in 0..nw {
2364 let (mut r, mut g, mut b, mut a) = (0u32, 0u32, 0u32, 0u32);
2365 let sy0 = (dy * factor) as usize;
2366 let sx0 = (dx * factor) as usize;
2367 for iy in 0..factor as usize {
2368 let row = (sy0 + iy) * stride + sx0 * 4;
2369 for ix in 0..factor as usize {
2370 let i = row + ix * 4;
2371 r += rgba_data[i] as u32;
2372 g += rgba_data[i + 1] as u32;
2373 b += rgba_data[i + 2] as u32;
2374 a += rgba_data[i + 3] as u32;
2375 }
2376 }
2377 let di = (dy * nw + dx) as usize * 4;
2378 out[di] = ((r + half) / area) as u8;
2379 out[di + 1] = ((g + half) / area) as u8;
2380 out[di + 2] = ((b + half) / area) as u8;
2381 out[di + 3] = ((a + half) / area) as u8;
2382 }
2383 }
2384 let f = factor as f32;
2385 let adjusted = Transform::from_row(
2386 transform.sx * f,
2387 transform.ky * f,
2388 transform.kx * f,
2389 transform.sy * f,
2390 transform.tx,
2391 transform.ty,
2392 );
2393 Some((out, nw, nh, adjusted))
2394}
2395
2396fn translate_clip_rect(rect: &ClipRect, y_start: u32, band_h: u32) -> ClipRect {
2398 ClipRect {
2399 x0: rect.x0,
2400 y0: rect.y0.saturating_sub(y_start).min(band_h),
2401 x1: rect.x1,
2402 y1: rect.y1.saturating_sub(y_start).min(band_h),
2403 }
2404}
2405
2406fn enforce_min_image_size(transform: Transform, img_w: u32, img_h: u32) -> Transform {
2413 let eff_w =
2415 ((transform.sx * img_w as f32).powi(2) + (transform.ky * img_w as f32).powi(2)).sqrt();
2416 let eff_h =
2417 ((transform.kx * img_h as f32).powi(2) + (transform.sy * img_h as f32).powi(2)).sqrt();
2418
2419 if eff_w >= 1.0 && eff_h >= 1.0 {
2420 return transform;
2421 }
2422
2423 let ratio = eff_w.max(eff_h) / eff_w.min(eff_h).max(0.001);
2427 if ratio < 3.0 {
2428 return transform;
2429 }
2430
2431 let mut t = transform;
2432 if eff_w < 1.0 && eff_w > 0.001 {
2433 let boost = 1.0 / eff_w;
2434 t.sx *= boost;
2435 t.ky *= boost;
2436 }
2437 if eff_h < 1.0 && eff_h > 0.001 {
2438 let boost = 1.0 / eff_h;
2439 t.kx *= boost;
2440 t.sy *= boost;
2441 }
2442 t
2443}
2444
2445fn hairline_min_width(ctm: &Matrix, dpi: f64) -> f64 {
2449 let (a, b, c, d) = (ctm.a, ctm.b, ctm.c, ctm.d);
2450 let sum_sq = a * a + b * b + c * c + d * d;
2451 let diff = ((a * a + b * b - c * c - d * d).powi(2) + 4.0 * (a * c + b * d).powi(2)).sqrt();
2452 let s_max = (0.5 * (sum_sq + diff)).max(0.0).sqrt();
2453 let min_px = if dpi <= 150.0 { 0.5 } else { 1.0 };
2454 if s_max > 1e-10 {
2455 min_px / s_max
2456 } else {
2457 min_px
2458 }
2459}
2460
2461fn is_k_only_src(color: &DeviceColor) -> bool {
2467 if let Some((c, m, y, _k)) = color.native_cmyk {
2468 c == 0.0 && m == 0.0 && y == 0.0
2469 } else {
2470 false
2471 }
2472}
2473
2474fn needs_gray_promotion(
2483 overprint: bool,
2484 painted_channels: u8,
2485 is_device_cmyk: bool,
2486 color: &DeviceColor,
2487) -> Option<f64> {
2488 if !overprint
2489 || painted_channels != 0
2490 || is_device_cmyk
2491 || color.native_cmyk.is_some()
2492 || color.process_cmyk.is_some()
2493 {
2494 return None;
2495 }
2496 let r = color.r;
2497 if (r - color.g).abs() > f64::EPSILON || (r - color.b).abs() > f64::EPSILON {
2498 return None;
2499 }
2500 Some(r.clamp(0.0, 1.0))
2501}
2502
2503fn maybe_promote_gray_fill<'a>(
2506 params: &'a FillParams,
2507 buf: &'a mut Option<FillParams>,
2508) -> &'a FillParams {
2509 if let Some(gray) = needs_gray_promotion(
2510 params.overprint,
2511 params.painted_channels,
2512 params.is_device_cmyk,
2513 ¶ms.color,
2514 ) {
2515 let mut promoted = params.clone();
2516 promoted.is_device_cmyk = true;
2517 promoted.painted_channels = stet_graphics::device::CMYK_K;
2518 promoted.color.native_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2519 promoted.color.process_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2520 *buf = Some(promoted);
2521 return buf.as_ref().unwrap();
2522 }
2523 params
2524}
2525
2526fn maybe_promote_gray_stroke<'a>(
2528 params: &'a StrokeParams,
2529 buf: &'a mut Option<StrokeParams>,
2530) -> &'a StrokeParams {
2531 if let Some(gray) = needs_gray_promotion(
2532 params.overprint,
2533 params.painted_channels,
2534 params.is_device_cmyk,
2535 ¶ms.color,
2536 ) {
2537 let mut promoted = params.clone();
2538 promoted.is_device_cmyk = true;
2539 promoted.painted_channels = stet_graphics::device::CMYK_K;
2540 promoted.color.native_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2541 promoted.color.process_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
2542 *buf = Some(promoted);
2543 return buf.as_ref().unwrap();
2544 }
2545 params
2546}
2547
2548fn build_stroke(params: &StrokeParams, dpi: f64) -> Stroke {
2552 let min_lw = hairline_min_width(¶ms.ctm, dpi);
2553 let mut stroke = Stroke {
2554 width: (params.line_width as f32).max(min_lw as f32),
2555 line_cap: to_line_cap(params.line_cap),
2556 line_join: to_line_join(params.line_join),
2557 miter_limit: params.miter_limit as f32,
2558 ..Stroke::default()
2559 };
2560 if !params.dash_pattern.array.is_empty() {
2561 let mut dash_array: Vec<f32> = params
2562 .dash_pattern
2563 .array
2564 .iter()
2565 .map(|&v| v as f32)
2566 .collect();
2567 if dash_array.len() % 2 == 1 {
2570 let clone = dash_array.clone();
2571 dash_array.extend_from_slice(&clone);
2572 }
2573 if let Some(dash) = StrokeDash::new(dash_array, params.dash_pattern.offset as f32) {
2574 stroke.dash = Some(dash);
2575 }
2576 }
2577 stroke
2578}
2579
2580fn ctm_is_device_space(ctm: &Matrix) -> bool {
2597 (ctm.a.abs() - 1.0).abs() < 0.01
2598 && ctm.b.abs() < 0.01
2599 && ctm.c.abs() < 0.01
2600 && (ctm.d.abs() - 1.0).abs() < 0.01
2601}
2602
2603fn stroke_adjust_path_viewport(
2609 path: &PsPath,
2610 device_width: f64,
2611 scale_x: f64,
2612 scale_y: f64,
2613 vp_x: f64,
2614 vp_y: f64,
2615) -> PsPath {
2616 let use_half_pixel = device_width < 1.5 || (device_width.round() as i32) % 2 == 1;
2617
2618 let snap_x = |v: f64| -> f64 {
2620 let out = (v - vp_x) * scale_x;
2621 let snapped = if use_half_pixel {
2622 out.floor() + 0.5
2623 } else {
2624 out.round()
2625 };
2626 snapped / scale_x + vp_x
2627 };
2628 let snap_y = |v: f64| -> f64 {
2629 let out = (v - vp_y) * scale_y;
2630 let snapped = if use_half_pixel {
2631 out.floor() + 0.5
2632 } else {
2633 out.round()
2634 };
2635 snapped / scale_y + vp_y
2636 };
2637
2638 let mut result = PsPath::new();
2639 let mut prev_x = 0.0_f64;
2640 let mut prev_y = 0.0_f64;
2641
2642 for seg in &path.segments {
2643 match *seg {
2644 PathSegment::MoveTo(x, y) => {
2645 prev_x = x;
2646 prev_y = y;
2647 result.segments.push(PathSegment::MoveTo(x, y));
2648 }
2649 PathSegment::LineTo(x, y) => {
2650 let is_horizontal = (y - prev_y).abs() < 1e-6;
2651 let is_vertical = (x - prev_x).abs() < 1e-6;
2652
2653 if is_horizontal {
2654 let snapped_y = snap_y(y);
2655 if let Some(PathSegment::MoveTo(_, ly) | PathSegment::LineTo(_, ly)) =
2656 result.segments.last_mut()
2657 {
2658 *ly = snapped_y;
2659 }
2660 result.segments.push(PathSegment::LineTo(x, snapped_y));
2661 prev_x = x;
2662 prev_y = snapped_y;
2663 } else if is_vertical {
2664 let snapped_x = snap_x(x);
2665 if let Some(PathSegment::MoveTo(lx, _) | PathSegment::LineTo(lx, _)) =
2666 result.segments.last_mut()
2667 {
2668 *lx = snapped_x;
2669 }
2670 result.segments.push(PathSegment::LineTo(snapped_x, y));
2671 prev_x = snapped_x;
2672 prev_y = y;
2673 } else {
2674 result.segments.push(PathSegment::LineTo(x, y));
2675 prev_x = x;
2676 prev_y = y;
2677 }
2678 }
2679 PathSegment::CurveTo {
2680 x1,
2681 y1,
2682 x2,
2683 y2,
2684 x3,
2685 y3,
2686 } => {
2687 result.segments.push(PathSegment::CurveTo {
2688 x1,
2689 y1,
2690 x2,
2691 y2,
2692 x3,
2693 y3,
2694 });
2695 prev_x = x3;
2696 prev_y = y3;
2697 }
2698 PathSegment::ClosePath => {
2699 result.segments.push(PathSegment::ClosePath);
2700 }
2701 }
2702 }
2703 result
2704}
2705
2706fn render_element(
2712 pixmap: &mut Pixmap,
2713 band_state: &mut BandState,
2714 element: &DisplayElement,
2715 ctx: &RenderContext<'_>,
2716) {
2717 match element {
2718 DisplayElement::Fill { path, params } => {
2719 let mut promoted_fill: Option<FillParams> = None;
2725 let params = maybe_promote_gray_fill(params, &mut promoted_fill);
2726 let painted = params.painted_channels;
2747 let subset_channels = painted != 0 && painted != stet_graphics::device::CMYK_ALL;
2748 let opm1_cmyk = params.is_device_cmyk && params.overprint_mode == 1;
2749 let custom_spot = painted == 0
2758 && !params.is_device_cmyk
2759 && params.color.native_cmyk.is_some()
2760 && params.color.process_cmyk.is_some();
2761 let is_k_only_cmyk =
2767 params.is_device_cmyk && params.overprint_mode == 0 && is_k_only_src(¶ms.color);
2768 let needs_overprint = params.overprint
2769 && band_state.cmyk_buffer.is_some()
2770 && params.blend_mode == 0
2771 && (subset_channels || opm1_cmyk || custom_spot || is_k_only_cmyk);
2772
2773 if needs_overprint {
2774 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2775 let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
2776 let spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2777 render_overprint_fill(
2778 pixmap,
2779 &mut cmyk_buf,
2780 &mut op_bg,
2781 &mut op_touched,
2782 &spot_mask,
2783 band_state,
2784 path,
2785 params,
2786 ctx.vp_x,
2787 ctx.vp_y,
2788 ctx.scale_x,
2789 ctx.scale_y,
2790 ctx.out_w,
2791 ctx.out_h,
2792 ctx.icc,
2793 ctx.no_aa,
2794 );
2795 band_state.cmyk_buffer = Some(cmyk_buf);
2796 band_state.restore_op_buffers(op_bg, op_touched);
2797 band_state.restore_spot_mask(spot_mask);
2798 } else {
2799 let Some(skia_path) = build_skia_path(path) else {
2800 return;
2801 };
2802 let mut temp_mask = None;
2803 let Some(mask_ref) = resolve_clip_mask(
2804 &band_state.clip_region,
2805 &mut temp_mask,
2806 ctx.out_w,
2807 ctx.out_h,
2808 ) else {
2809 return;
2810 };
2811 let paint =
2812 to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, ctx.no_aa);
2813 let transform = ctx.transform(¶ms.ctm);
2814
2815 if is_degenerate_fill(path) {
2821 let stroke = Stroke {
2822 width: 1.0,
2823 ..Stroke::default()
2824 };
2825 pixmap.stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
2826 } else {
2827 let fill_rule = to_fill_rule(¶ms.fill_rule);
2828 pixmap.fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
2829 }
2830
2831 if band_state.cmyk_buffer.is_some() {
2833 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2834 let mut spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2835 update_cmyk_buffer_for_fill(
2836 &mut cmyk_buf,
2837 &mut spot_mask,
2838 path,
2839 params,
2840 ctx.vp_x,
2841 ctx.vp_y,
2842 ctx.scale_x,
2843 ctx.scale_y,
2844 ctx.out_w,
2845 ctx.out_h,
2846 &band_state.clip_region,
2847 ctx.no_aa,
2848 ctx.icc,
2849 );
2850 band_state.cmyk_buffer = Some(cmyk_buf);
2851 band_state.restore_spot_mask(spot_mask);
2852 }
2853 }
2854 }
2855 DisplayElement::Stroke { path, params } => {
2856 let mut promoted_stroke: Option<StrokeParams> = None;
2857 let params = maybe_promote_gray_stroke(params, &mut promoted_stroke);
2858 let transform = ctx.transform(¶ms.ctm);
2859 let vp_ctm = Matrix {
2862 a: transform.sx as f64,
2863 b: transform.ky as f64,
2864 c: transform.kx as f64,
2865 d: transform.sy as f64,
2866 tx: 0.0,
2867 ty: 0.0,
2868 };
2869 let vp_params = StrokeParams {
2870 ctm: vp_ctm,
2871 ..params.clone()
2872 };
2873 let stroke = build_stroke(&vp_params, ctx.effective_dpi);
2874
2875 let adjusted;
2877 let draw_path = if params.stroke_adjust
2878 && stroke.width <= 2.0
2879 && ctm_is_device_space(¶ms.ctm)
2880 {
2881 adjusted = stroke_adjust_path_viewport(
2882 path,
2883 stroke.width as f64,
2884 ctx.scale_x as f64,
2885 ctx.scale_y as f64,
2886 ctx.vp_x as f64,
2887 ctx.vp_y as f64,
2888 );
2889 &adjusted
2890 } else {
2891 path
2892 };
2893
2894 let painted = params.painted_channels;
2902 let subset_channels = painted != 0 && painted != stet_graphics::device::CMYK_ALL;
2903 let opm1_cmyk = params.is_device_cmyk && params.overprint_mode == 1;
2904 let custom_spot = painted == 0
2909 && !params.is_device_cmyk
2910 && params.color.native_cmyk.is_some()
2911 && params.color.process_cmyk.is_some();
2912 let is_k_only_cmyk =
2913 params.is_device_cmyk && params.overprint_mode == 0 && is_k_only_src(¶ms.color);
2914 let needs_overprint = params.overprint
2915 && band_state.cmyk_buffer.is_some()
2916 && params.blend_mode == 0
2917 && (subset_channels || opm1_cmyk || custom_spot || is_k_only_cmyk);
2918
2919 let Some(skia_path) = build_skia_path(draw_path) else {
2920 return;
2921 };
2922 let mut temp_mask = None;
2923 let Some(mask_ref) = resolve_clip_mask(
2924 &band_state.clip_region,
2925 &mut temp_mask,
2926 ctx.out_w,
2927 ctx.out_h,
2928 ) else {
2929 return;
2930 };
2931
2932 if needs_overprint {
2933 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2938 let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
2939 let spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2940 render_overprint_stroke(
2941 pixmap,
2942 &mut cmyk_buf,
2943 &mut op_bg,
2944 &mut op_touched,
2945 &spot_mask,
2946 band_state,
2947 &skia_path,
2948 &stroke,
2949 transform,
2950 params,
2951 ctx.out_w,
2952 ctx.out_h,
2953 ctx.icc,
2954 ctx.no_aa,
2955 );
2956 band_state.cmyk_buffer = Some(cmyk_buf);
2957 band_state.restore_op_buffers(op_bg, op_touched);
2958 band_state.restore_spot_mask(spot_mask);
2959 } else {
2960 let paint =
2961 to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, ctx.no_aa);
2962 pixmap.stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
2963
2964 if band_state.cmyk_buffer.is_some() {
2965 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
2966 let mut spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
2967 update_cmyk_buffer_for_stroke(
2968 &mut cmyk_buf,
2969 &mut spot_mask,
2970 draw_path,
2971 params,
2972 &stroke,
2973 transform,
2974 ctx.out_w,
2975 ctx.out_h,
2976 &band_state.clip_region,
2977 ctx.no_aa,
2978 ctx.icc,
2979 );
2980 band_state.cmyk_buffer = Some(cmyk_buf);
2981 band_state.restore_spot_mask(spot_mask);
2982 }
2983 }
2984 }
2985 DisplayElement::Clip { path, params } => {
2986 clip_path_unified(band_state, path, params, ctx);
2987 }
2988 DisplayElement::InitClip => {
2989 if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
2990 band_state.recycle_mask(mask);
2991 }
2992 band_state.clip_region = None;
2993 }
2994 DisplayElement::ErasePage => {
2995 pixmap.fill(Color::TRANSPARENT);
2996 if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
2997 band_state.recycle_mask(mask);
2998 }
2999 band_state.clip_region = None;
3000 }
3001 DisplayElement::Image {
3002 sample_data,
3003 params,
3004 } => {
3005 let iw = params.width;
3006 let ih = params.height;
3007 if iw == 0 || ih == 0 {
3008 return;
3009 }
3010
3011 let needs_overprint = params.overprint
3012 && band_state.cmyk_buffer.is_some()
3013 && image_supports_overprint(¶ms.color_space);
3014
3015 if needs_overprint {
3016 let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
3017 let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
3018 render_overprint_image(
3019 pixmap,
3020 &mut cmyk_buf,
3021 &mut op_bg,
3022 &mut op_touched,
3023 band_state,
3024 sample_data,
3025 params,
3026 ctx.vp_x,
3027 ctx.vp_y,
3028 ctx.scale_x,
3029 ctx.scale_y,
3030 ctx.out_w,
3031 ctx.out_h,
3032 ctx.icc,
3033 );
3034 band_state.cmyk_buffer = Some(cmyk_buf);
3035 band_state.restore_op_buffers(op_bg, op_touched);
3036 } else if let Some(pp) = ctx
3037 .preprocessed
3038 .and_then(|pp| pp.get(ctx.elem_idx))
3039 .and_then(|e| e.as_ref())
3040 {
3041 let Some(image_inv) = params.image_matrix.invert() else {
3044 return;
3045 };
3046 let combined = params.ctm.concat(&image_inv);
3047 let raw_transform = ctx.transform(&combined);
3048 let transform = Transform::from_row(
3049 pp.adj_sx,
3050 pp.adj_ky,
3051 pp.adj_kx,
3052 pp.adj_sy,
3053 raw_transform.tx,
3054 raw_transform.ty,
3055 );
3056
3057 let Some(img_pixmap) =
3058 stet_tiny_skia::PixmapRef::from_bytes(&pp.data, pp.width, pp.height)
3059 else {
3060 return;
3061 };
3062 #[allow(unused_assignments)]
3063 let mut temp_mask = None;
3064 let mask_ref = match &band_state.clip_region {
3065 None => None,
3066 Some(ClipRegion::Mask(m)) => Some(m as &Mask),
3067 Some(ClipRegion::Rect(rect)) => {
3068 if rect.is_empty() {
3069 return;
3070 } else if rect.is_full_page(ctx.out_w, ctx.out_h) {
3071 None
3072 } else {
3073 temp_mask = rect.make_mask(ctx.out_w, ctx.out_h);
3074 temp_mask.as_ref()
3075 }
3076 }
3077 };
3078 let img_paint = stet_tiny_skia::PixmapPaint {
3079 quality: pp.quality,
3080 opacity: params.alpha as f32,
3081 blend_mode: u8_to_blend_mode(params.blend_mode),
3082 };
3083 pixmap.draw_pixmap(0, 0, img_pixmap, &img_paint, transform, mask_ref);
3084
3085 if let Some(ref mut cmyk_buf) = band_state.cmyk_buffer {
3091 update_cmyk_buffer_for_image(
3092 cmyk_buf,
3093 sample_data,
3094 pixmap.data(),
3095 params,
3096 ctx.vp_x,
3097 ctx.vp_y,
3098 ctx.scale_x,
3099 ctx.scale_y,
3100 ctx.out_w,
3101 ctx.out_h,
3102 &band_state.clip_region,
3103 ctx.icc,
3104 );
3105 }
3106 } else {
3107 let owned_rgba;
3109 let rgba_data: &[u8] = if let Some(cached) =
3110 ctx.image_cache.and_then(|c| c.get(ctx.elem_idx))
3111 {
3112 cached
3113 } else {
3114 owned_rgba = {
3115 let mut rgba =
3116 samples_to_rgba(sample_data, params, ctx.icc, ctx.opm_zero_transparent);
3117 if params.mask_color.is_some() {
3118 apply_mask_color_rgba(&mut rgba, sample_data, params);
3119 }
3120 rgba
3121 };
3122 &owned_rgba
3123 };
3124 let expected = (iw * ih * 4) as usize;
3125 if rgba_data.len() < expected {
3126 return;
3127 }
3128 let Some(image_inv) = params.image_matrix.invert() else {
3129 return;
3130 };
3131 let combined = params.ctm.concat(&image_inv);
3132 let raw_transform = enforce_min_image_size(ctx.transform(&combined), iw, ih);
3133
3134 let prescaled =
3139 prescale_image(rgba_data, iw, ih, raw_transform, params.interpolate);
3140 let (img_data, img_w, img_h, transform) = match &prescaled {
3141 Some((data, w, h, t)) => (data.as_slice(), *w, *h, *t),
3142 None => (rgba_data, iw, ih, raw_transform),
3143 };
3144
3145 let Some(img_pixmap) =
3146 stet_tiny_skia::PixmapRef::from_bytes(img_data, img_w, img_h)
3147 else {
3148 return;
3149 };
3150 #[allow(unused_assignments)]
3151 let mut temp_mask = None;
3152 let mask_ref = match &band_state.clip_region {
3153 None => None,
3154 Some(ClipRegion::Mask(m)) => Some(m as &Mask),
3155 Some(ClipRegion::Rect(rect)) => {
3156 if rect.is_empty() {
3157 return;
3158 } else if rect.is_full_page(ctx.out_w, ctx.out_h) {
3159 None
3160 } else {
3161 temp_mask = rect.make_mask(ctx.out_w, ctx.out_h);
3162 temp_mask.as_ref()
3163 }
3164 }
3165 };
3166 let img_paint = stet_tiny_skia::PixmapPaint {
3167 quality: image_filter_quality(transform, params.interpolate),
3168 opacity: params.alpha as f32,
3169 blend_mode: u8_to_blend_mode(params.blend_mode),
3170 };
3171 pixmap.draw_pixmap(0, 0, img_pixmap, &img_paint, transform, mask_ref);
3172
3173 if let Some(ref mut cmyk_buf) = band_state.cmyk_buffer {
3177 update_cmyk_buffer_for_image(
3178 cmyk_buf,
3179 sample_data,
3180 pixmap.data(),
3181 params,
3182 ctx.vp_x,
3183 ctx.vp_y,
3184 ctx.scale_x,
3185 ctx.scale_y,
3186 ctx.out_w,
3187 ctx.out_h,
3188 &band_state.clip_region,
3189 ctx.icc,
3190 );
3191 }
3192 }
3193 }
3194 DisplayElement::AxialShading { params } => {
3195 let mut temp_mask = None;
3196 let Some(mask_ref) = resolve_clip_mask(
3197 &band_state.clip_region,
3198 &mut temp_mask,
3199 ctx.out_w,
3200 ctx.out_h,
3201 ) else {
3202 return;
3203 };
3204 render_axial_shading(
3205 pixmap,
3206 params,
3207 ctx.vp_x,
3208 ctx.vp_y,
3209 ctx.scale_x,
3210 ctx.scale_y,
3211 mask_ref,
3212 ctx.no_aa,
3213 band_state.cmyk_buffer.as_deref_mut(),
3214 ctx.icc,
3215 );
3216 }
3217 DisplayElement::RadialShading { params } => {
3218 let mut temp_mask = None;
3219 let Some(mask_ref) = resolve_clip_mask(
3220 &band_state.clip_region,
3221 &mut temp_mask,
3222 ctx.out_w,
3223 ctx.out_h,
3224 ) else {
3225 return;
3226 };
3227 render_radial_shading(
3228 pixmap,
3229 params,
3230 ctx.vp_x,
3231 ctx.vp_y,
3232 ctx.scale_x,
3233 ctx.scale_y,
3234 mask_ref,
3235 ctx.no_aa,
3236 band_state.cmyk_buffer.as_deref_mut(),
3237 ctx.icc,
3238 );
3239 }
3240 DisplayElement::MeshShading { params } => {
3241 let mut temp_mask = None;
3242 let Some(mask_ref) = resolve_clip_mask(
3243 &band_state.clip_region,
3244 &mut temp_mask,
3245 ctx.out_w,
3246 ctx.out_h,
3247 ) else {
3248 return;
3249 };
3250 render_mesh_shading(
3251 pixmap,
3252 params,
3253 ctx.vp_x,
3254 ctx.vp_y,
3255 ctx.scale_x,
3256 ctx.scale_y,
3257 mask_ref,
3258 band_state.cmyk_buffer.as_deref_mut(),
3259 ctx.icc,
3260 );
3261 }
3262 DisplayElement::PatchShading { params } => {
3263 let mut temp_mask = None;
3264 let Some(mask_ref) = resolve_clip_mask(
3265 &band_state.clip_region,
3266 &mut temp_mask,
3267 ctx.out_w,
3268 ctx.out_h,
3269 ) else {
3270 return;
3271 };
3272 render_patch_shading(
3273 pixmap,
3274 params,
3275 ctx.vp_x,
3276 ctx.vp_y,
3277 ctx.scale_x,
3278 ctx.scale_y,
3279 mask_ref,
3280 band_state.cmyk_buffer.as_deref_mut(),
3281 ctx.icc,
3282 );
3283 }
3284 DisplayElement::PatternFill { params } => {
3285 render_pattern_fill(pixmap, band_state, params, ctx);
3286 }
3287 DisplayElement::Group { elements, params } => {
3288 render_group(pixmap, band_state, elements, params, ctx);
3289 }
3290 DisplayElement::SoftMasked {
3291 mask,
3292 content,
3293 params,
3294 mask_cache,
3295 } => {
3296 render_soft_masked(pixmap, band_state, mask, content, params, mask_cache, ctx);
3297 }
3298 DisplayElement::Text { .. } => {} DisplayElement::OcgGroup {
3300 elements,
3301 visibility,
3302 } => {
3303 let visible = ctx.layer_set.evaluate(visibility);
3309 for (idx, elem) in elements.elements().iter().enumerate() {
3310 if !visible
3311 && !matches!(elem, DisplayElement::Clip { .. } | DisplayElement::InitClip)
3312 {
3313 continue;
3314 }
3315 let elem_ctx = RenderContext {
3316 elem_idx: idx,
3317 ..*ctx
3318 };
3319 render_element(pixmap, band_state, elem, &elem_ctx);
3320 }
3321 }
3322 _ => {}
3323 }
3324}
3325
3326fn compute_group_crop(bbox: &[f64; 4], ctx: &RenderContext<'_>) -> Option<(i32, i32, u32, u32)> {
3331 let px_min = ((bbox[0] as f32 - ctx.vp_x) * ctx.scale_x).floor() as i32;
3333 let py_min = ((bbox[1] as f32 - ctx.vp_y) * ctx.scale_y).floor() as i32;
3334 let px_max = ((bbox[2] as f32 - ctx.vp_x) * ctx.scale_x).ceil() as i32;
3335 let py_max = ((bbox[3] as f32 - ctx.vp_y) * ctx.scale_y).ceil() as i32;
3336
3337 let x0 = px_min.max(0);
3339 let y0 = py_min.max(0);
3340 let x1 = px_max.min(ctx.out_w as i32);
3341 let y1 = py_max.min(ctx.out_h as i32);
3342
3343 if x0 >= x1 || y0 >= y1 {
3344 return None;
3345 }
3346
3347 let crop_w = (x1 - x0) as u32;
3348 let crop_h = (y1 - y0) as u32;
3349
3350 let crop_pixels = crop_w as u64 * crop_h as u64;
3352 let full_pixels = ctx.out_w as u64 * ctx.out_h as u64;
3353 if crop_pixels * 4 >= full_pixels * 3 {
3354 return None;
3355 }
3356
3357 Some((x0, y0, crop_w, crop_h))
3358}
3359
3360fn blend_cmyk_separable_channel(cb: f64, cs: f64, mode: u8) -> f64 {
3364 let cbi = 1.0 - cb;
3365 let csi = 1.0 - cs;
3366 let result_inv = match mode {
3367 1 => cbi * csi, 2 => cbi + csi - cbi * csi, 3 => {
3370 if cbi <= 0.5 {
3372 2.0 * cbi * csi
3373 } else {
3374 1.0 - 2.0 * (1.0 - cbi) * (1.0 - csi)
3375 }
3376 }
3377 4 => cbi.min(csi), 5 => cbi.max(csi), 6 => {
3380 if csi >= 1.0 {
3382 1.0
3383 } else {
3384 (cbi / (1.0 - csi)).min(1.0)
3385 }
3386 }
3387 7 => {
3388 if csi <= 0.0 {
3390 0.0
3391 } else {
3392 1.0 - ((1.0 - cbi) / csi).min(1.0)
3393 }
3394 }
3395 8 => {
3396 if csi <= 0.5 {
3398 2.0 * cbi * csi
3399 } else {
3400 1.0 - 2.0 * (1.0 - cbi) * (1.0 - csi)
3401 }
3402 }
3403 9 => {
3404 let d = if cbi <= 0.25 {
3406 ((16.0 * cbi - 12.0) * cbi + 4.0) * cbi
3407 } else {
3408 cbi.sqrt()
3409 };
3410 if csi <= 0.5 {
3411 cbi - (1.0 - 2.0 * csi) * cbi * (1.0 - cbi)
3412 } else {
3413 cbi + (2.0 * csi - 1.0) * (d - cbi)
3414 }
3415 }
3416 10 => (cbi - csi).abs(), 11 => cbi + csi - 2.0 * cbi * csi, _ => csi, };
3420 1.0 - result_inv.clamp(0.0, 1.0)
3421}
3422
3423fn blend_cmyk_nonseparable(cb: [f64; 4], cs: [f64; 4], mode: u8) -> [f64; 4] {
3429 fn lum(c: [f64; 3]) -> f64 {
3430 0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
3431 }
3432 fn clip_color(mut c: [f64; 3]) -> [f64; 3] {
3433 let l = lum(c);
3434 let n = c[0].min(c[1]).min(c[2]);
3435 let x = c[0].max(c[1]).max(c[2]);
3436 if n < 0.0 {
3437 for ci in c.iter_mut() {
3438 *ci = l + (*ci - l) * l / (l - n);
3439 }
3440 }
3441 if x > 1.0 {
3442 for ci in c.iter_mut() {
3443 *ci = l + (*ci - l) * (1.0 - l) / (x - l);
3444 }
3445 }
3446 c
3447 }
3448 fn set_lum(c: [f64; 3], l: f64) -> [f64; 3] {
3449 let d = l - lum(c);
3450 clip_color([c[0] + d, c[1] + d, c[2] + d])
3451 }
3452 fn sat(c: [f64; 3]) -> f64 {
3453 c[0].max(c[1]).max(c[2]) - c[0].min(c[1]).min(c[2])
3454 }
3455 fn set_sat(c: [f64; 3], s: f64) -> [f64; 3] {
3456 let mut idx = [0usize, 1, 2];
3458 idx.sort_by(|a, b| {
3459 c[*a]
3460 .partial_cmp(&c[*b])
3461 .unwrap_or(std::cmp::Ordering::Equal)
3462 });
3463 let (i_min, i_mid, i_max) = (idx[0], idx[1], idx[2]);
3464 let mut out = c;
3465 if c[i_max] > c[i_min] {
3466 out[i_mid] = (c[i_mid] - c[i_min]) * s / (c[i_max] - c[i_min]);
3467 out[i_max] = s;
3468 } else {
3469 out[i_mid] = 0.0;
3470 out[i_max] = 0.0;
3471 }
3472 out[i_min] = 0.0;
3473 out
3474 }
3475
3476 let cb_rgb = [1.0 - cb[0], 1.0 - cb[1], 1.0 - cb[2]];
3477 let cs_rgb = [1.0 - cs[0], 1.0 - cs[1], 1.0 - cs[2]];
3478 let result_rgb = match mode {
3479 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,
3484 };
3485 let result_k = if mode == 15 { cs[3] } else { cb[3] };
3489 [
3490 (1.0 - result_rgb[0]).clamp(0.0, 1.0),
3491 (1.0 - result_rgb[1]).clamp(0.0, 1.0),
3492 (1.0 - result_rgb[2]).clamp(0.0, 1.0),
3493 result_k,
3494 ]
3495}
3496
3497fn ps_path_bbox(path: &PsPath) -> Option<(f64, f64, f64, f64)> {
3502 let mut it = path.segments.iter().filter_map(|seg| match *seg {
3503 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => Some(vec![(x, y)]),
3504 PathSegment::CurveTo {
3505 x1,
3506 y1,
3507 x2,
3508 y2,
3509 x3,
3510 y3,
3511 } => Some(vec![(x1, y1), (x2, y2), (x3, y3)]),
3512 PathSegment::ClosePath => None,
3513 });
3514 let first = it.next()?.into_iter().next()?;
3515 let (mut x0, mut y0) = first;
3516 let (mut x1, mut y1) = first;
3517 for seg_points in std::iter::once(vec![first]).chain(it) {
3518 for (x, y) in seg_points {
3519 x0 = x0.min(x);
3520 y0 = y0.min(y);
3521 x1 = x1.max(x);
3522 y1 = y1.max(y);
3523 }
3524 }
3525 Some((x0, y0, x1, y1))
3526}
3527
3528fn bbox_contains(outer: (f64, f64, f64, f64), inner: (f64, f64, f64, f64), tolerance: f64) -> bool {
3531 inner.0 >= outer.0 - tolerance
3532 && inner.1 >= outer.1 - tolerance
3533 && inner.2 <= outer.2 + tolerance
3534 && inner.3 <= outer.3 + tolerance
3535}
3536
3537fn compute_obscured_fill_skips(elements: &DisplayList) -> Vec<usize> {
3563 let mut skips = Vec::new();
3564 let els = elements.elements();
3565 for i in 0..els.len() {
3566 let DisplayElement::Fill {
3567 path: parent_path,
3568 params: parent_params,
3569 } = &els[i]
3570 else {
3571 continue;
3572 };
3573 if (parent_params.alpha - 1.0).abs() > 1e-6 || parent_params.blend_mode != 0 {
3574 continue;
3575 }
3576 let Some(parent_bbox) = ps_path_bbox(parent_path) else {
3577 continue;
3578 };
3579 let mut j = i + 1;
3583 let mut clips_ok = true;
3584 while j < els.len() {
3585 match &els[j] {
3586 DisplayElement::InitClip => {}
3587 DisplayElement::Clip {
3588 path: clip_path, ..
3589 } => match ps_path_bbox(clip_path) {
3590 Some(cb) if bbox_contains(cb, parent_bbox, 0.5) => {}
3591 _ => {
3592 clips_ok = false;
3593 break;
3594 }
3595 },
3596 _ => break,
3597 }
3598 j += 1;
3599 }
3600 if !clips_ok {
3601 continue;
3602 }
3603 let Some(DisplayElement::Group {
3604 elements: group_elements,
3605 params: group_params,
3606 }) = els.get(j)
3607 else {
3608 continue;
3609 };
3610 if !group_params.isolated
3611 || (group_params.alpha - 1.0).abs() > 1e-6
3612 || group_params.blend_mode != 0
3613 {
3614 continue;
3615 }
3616 let group_bbox = (
3620 group_params.bbox[0],
3621 group_params.bbox[1],
3622 group_params.bbox[2],
3623 group_params.bbox[3],
3624 );
3625 if !bbox_contains(group_bbox, parent_bbox, 0.5) {
3626 continue;
3627 }
3628 let inner_els = group_elements.elements();
3631 let mut k = 0;
3632 let mut inner_clips_ok = true;
3633 while k < inner_els.len() {
3634 match &inner_els[k] {
3635 DisplayElement::InitClip => {}
3636 DisplayElement::Clip {
3637 path: clip_path, ..
3638 } => match ps_path_bbox(clip_path) {
3639 Some(cb) if bbox_contains(cb, parent_bbox, 0.5) => {}
3640 _ => {
3641 inner_clips_ok = false;
3642 break;
3643 }
3644 },
3645 _ => break,
3646 }
3647 k += 1;
3648 }
3649 if !inner_clips_ok {
3650 continue;
3651 }
3652 let Some(DisplayElement::Fill {
3653 path: group_path,
3654 params: group_fill_params,
3655 }) = inner_els.get(k)
3656 else {
3657 continue;
3658 };
3659 if (group_fill_params.alpha - 1.0).abs() > 1e-6 || group_fill_params.blend_mode != 0 {
3660 continue;
3661 }
3662 if paths_approximately_equal(parent_path, group_path, 0.5) {
3663 skips.push(i);
3664 }
3665 }
3666 skips
3667}
3668
3669fn paths_approximately_equal(a: &PsPath, b: &PsPath, tolerance: f64) -> bool {
3675 if a.segments.len() != b.segments.len() {
3676 return false;
3677 }
3678 for (sa, sb) in a.segments.iter().zip(b.segments.iter()) {
3679 let close_pair = |(x1, y1): (f64, f64), (x2, y2): (f64, f64)| -> bool {
3680 (x1 - x2).abs() <= tolerance && (y1 - y2).abs() <= tolerance
3681 };
3682 match (sa, sb) {
3683 (PathSegment::MoveTo(x1, y1), PathSegment::MoveTo(x2, y2)) => {
3684 if !close_pair((*x1, *y1), (*x2, *y2)) {
3685 return false;
3686 }
3687 }
3688 (PathSegment::LineTo(x1, y1), PathSegment::LineTo(x2, y2)) => {
3689 if !close_pair((*x1, *y1), (*x2, *y2)) {
3690 return false;
3691 }
3692 }
3693 (
3694 PathSegment::CurveTo {
3695 x1: ax1,
3696 y1: ay1,
3697 x2: ax2,
3698 y2: ay2,
3699 x3: ax3,
3700 y3: ay3,
3701 },
3702 PathSegment::CurveTo {
3703 x1: bx1,
3704 y1: by1,
3705 x2: bx2,
3706 y2: by2,
3707 x3: bx3,
3708 y3: by3,
3709 },
3710 ) => {
3711 if !close_pair((*ax1, *ay1), (*bx1, *by1))
3712 || !close_pair((*ax2, *ay2), (*bx2, *by2))
3713 || !close_pair((*ax3, *ay3), (*bx3, *by3))
3714 {
3715 return false;
3716 }
3717 }
3718 (PathSegment::ClosePath, PathSegment::ClosePath) => {}
3719 _ => return false,
3720 }
3721 }
3722 true
3723}
3724
3725fn render_group(
3729 pixmap: &mut Pixmap,
3730 band_state: &mut BandState,
3731 elements: &DisplayList,
3732 params: &stet_graphics::display_list::GroupParams,
3733 ctx: &RenderContext<'_>,
3734) {
3735 if params.knockout {
3736 render_knockout_group(pixmap, band_state, elements, params, ctx);
3737 return;
3738 }
3739
3740 let crop = compute_group_crop(¶ms.bbox, ctx);
3741
3742 let (eff_w, eff_h, crop_x, crop_y, eff_vp_x, eff_vp_y) = match crop {
3743 Some((cx, cy, cw, ch)) => (
3744 cw,
3745 ch,
3746 cx,
3747 cy,
3748 ctx.vp_x + cx as f32 / ctx.scale_x,
3749 ctx.vp_y + cy as f32 / ctx.scale_y,
3750 ),
3751 None => (ctx.out_w, ctx.out_h, 0, 0, ctx.vp_x, ctx.vp_y),
3752 };
3753
3754 let Some(mut offscreen) = Pixmap::new(eff_w, eff_h) else {
3755 return;
3756 };
3757
3758 use stet_graphics::display_list::GroupColorSpace;
3766
3767 let needs_group_cmyk = has_overprint_elements(elements)
3773 || band_state.cmyk_buffer.is_some()
3774 || params.color_space == GroupColorSpace::DeviceCMYK
3775 || has_cmyk_group(elements);
3776
3777 let force_cmyk_compose =
3784 std::env::var_os("STET_FORCE_CMYK_COMPOSITE_BACK").as_deref() == Some("1".as_ref());
3785 let plan_cmyk_compose = match ctx.knockout_painter_pass {
3796 KnockoutPainterPass::CoveragePass => false,
3797 KnockoutPainterPass::ColorPass => {
3798 !params.isolated
3799 && params.blend_mode != 0
3800 && needs_group_cmyk
3801 && band_state.cmyk_buffer.is_some()
3802 && group_content_is_native_cmyk(elements)
3803 }
3804 KnockoutPainterPass::None if force_cmyk_compose => {
3805 !params.isolated
3823 && matches!(params.blend_mode, 10..=15)
3824 && needs_group_cmyk
3825 && band_state.cmyk_buffer.is_some()
3826 && group_content_is_native_cmyk(elements)
3827 }
3828 KnockoutPainterPass::None => {
3829 let inversion_sensitive = !params.isolated
3834 && matches!(params.blend_mode, 10..=15)
3835 && group_only_native_cmyk_fills(elements);
3836 let proofing_enabled = ctx.icc.is_some_and(|c| c.proofing_enabled());
3864 let effective_cs_is_cmyk = params.color_space == GroupColorSpace::DeviceCMYK
3873 || (params.color_space == GroupColorSpace::Inherited
3874 && band_state.cmyk_buffer.is_some());
3875 let cmyk_group_blend = !params.isolated
3876 && (ctx.parent_group_isolated || proofing_enabled)
3877 && params.blend_mode != 0
3878 && effective_cs_is_cmyk
3879 && needs_group_cmyk
3880 && band_state.cmyk_buffer.is_some()
3881 && group_content_is_native_cmyk(elements);
3882 inversion_sensitive || cmyk_group_blend
3883 }
3884 };
3885 let needs_alpha_extraction = !params.isolated
3890 && params.blend_mode != 0
3891 && !plan_cmyk_compose
3892 && !ctx.alpha_extraction_pass;
3893 let needs_backdrop_preload =
3894 !params.isolated && (params.blend_mode == 0 || plan_cmyk_compose || needs_alpha_extraction);
3895 let backdrop = if needs_backdrop_preload {
3896 let data = if crop.is_some() {
3897 copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h)
3898 } else {
3899 pixmap.data().to_vec()
3900 };
3901 offscreen.data_mut().copy_from_slice(&data);
3902 Some(data)
3903 } else {
3904 None
3905 };
3906 let group_cmyk = if needs_group_cmyk {
3907 let buf_size = eff_w as usize * eff_h as usize * 4;
3908 let mut buf = vec![0.0f32; buf_size];
3909 if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
3910 let parent_stride = ctx.out_w as usize * 4;
3911 let group_stride = eff_w as usize * 4;
3912 for gy in 0..eff_h as usize {
3913 let py = crop_y as usize + gy;
3914 if py < ctx.out_h as usize {
3915 let p_start = py * parent_stride + crop_x as usize * 4;
3916 let g_start = gy * group_stride;
3917 let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
3918 buf[g_start..g_start + copy_len]
3919 .copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
3920 }
3921 }
3922 }
3923 Some(buf)
3924 } else {
3925 None
3926 };
3927
3928 let backdrop_cmyk: Option<Vec<f32>> = if !params.isolated {
3933 group_cmyk.clone()
3934 } else {
3935 None
3936 };
3937
3938 let mut group_band = BandState {
3939 clip_region: None,
3940 spare_mask: None,
3941 clip_mask_cache: HashMap::new(),
3942 clip_mask_seen: HashSet::new(),
3943 mask_pool: Vec::new(),
3944 cmyk_buffer: group_cmyk,
3945 op_bg_snapshot: None,
3946 op_touched: None,
3947 spot_mask: None,
3948 };
3949
3950 let group_ctx = RenderContext {
3951 vp_x: eff_vp_x,
3952 vp_y: eff_vp_y,
3953 scale_x: ctx.scale_x,
3954 scale_y: ctx.scale_y,
3955 out_w: eff_w,
3956 out_h: eff_h,
3957 effective_dpi: ctx.effective_dpi,
3958 icc: ctx.icc,
3959 image_cache: None, preprocessed: None,
3961 elem_idx: 0,
3962 no_aa: ctx.no_aa,
3963 opm_zero_transparent: ctx.opm_zero_transparent,
3964 knockout_painter_pass: ctx.knockout_painter_pass,
3965 parent_group_isolated: params.isolated,
3967 alpha_extraction_pass: ctx.alpha_extraction_pass,
3968 layer_set: ctx.layer_set,
3969 };
3970
3971 let skip_indices = compute_obscured_fill_skips(elements);
3972 for (idx, elem) in elements.elements().iter().enumerate() {
3973 if skip_indices.contains(&idx) {
3974 continue;
3975 }
3976 let elem_ctx = RenderContext {
3977 elem_idx: idx,
3978 ..group_ctx
3979 };
3980 render_element(&mut offscreen, &mut group_band, elem, &elem_ctx);
3981 }
3982
3983 let alpha_offscreen = if needs_alpha_extraction {
3987 let mut iso = Pixmap::new(eff_w, eff_h);
3988 if let Some(ref mut iso_pm) = iso {
3989 let mut iso_band = BandState {
3990 clip_region: None,
3991 spare_mask: None,
3992 clip_mask_cache: HashMap::new(),
3993 clip_mask_seen: HashSet::new(),
3994 mask_pool: Vec::new(),
3995 cmyk_buffer: None,
3996 op_bg_snapshot: None,
3997 op_touched: None,
3998 spot_mask: None,
3999 };
4000 let iso_ctx = RenderContext {
4001 parent_group_isolated: true,
4002 alpha_extraction_pass: true,
4003 ..group_ctx
4004 };
4005 for (idx, elem) in elements.elements().iter().enumerate() {
4006 let elem_ctx = RenderContext {
4007 elem_idx: idx,
4008 ..iso_ctx
4009 };
4010 render_element(iso_pm, &mut iso_band, elem, &elem_ctx);
4011 }
4012 }
4013 iso
4014 } else {
4015 None
4016 };
4017
4018 let mut temp_mask = None;
4019 let mask_ref = match resolve_clip_mask(
4020 &band_state.clip_region,
4021 &mut temp_mask,
4022 ctx.out_w,
4023 ctx.out_h,
4024 ) {
4025 None => return, Some(m) => m,
4027 };
4028
4029 let coverage_params;
4034 let effective_params: &stet_graphics::display_list::GroupParams =
4035 if ctx.knockout_painter_pass == KnockoutPainterPass::CoveragePass {
4036 coverage_params = stet_graphics::display_list::GroupParams {
4037 alpha: 1.0,
4038 blend_mode: 0,
4039 ..params.clone()
4040 };
4041 &coverage_params
4042 } else {
4043 params
4044 };
4045
4046 let mut cmyk_compose_done = false;
4047 if let Some(backdrop) = &backdrop {
4048 let inner_cmyk = group_band.cmyk_buffer.as_deref();
4066 let pre_cmyk = backdrop_cmyk.as_deref();
4067 if plan_cmyk_compose && let (Some(inner), Some(pre)) = (inner_cmyk, pre_cmyk) {
4068 composite_non_isolated_cmyk(
4069 pixmap,
4070 band_state.cmyk_buffer.as_deref_mut(),
4071 &offscreen,
4072 inner,
4073 pre,
4074 backdrop,
4075 effective_params,
4076 mask_ref,
4077 crop_x,
4078 crop_y,
4079 ctx.icc,
4080 );
4081 cmyk_compose_done = true;
4082 } else if let Some(ref alpha_os) = alpha_offscreen {
4083 composite_non_isolated_extracted(
4084 pixmap,
4085 &offscreen,
4086 alpha_os,
4087 backdrop,
4088 effective_params,
4089 mask_ref,
4090 crop_x,
4091 crop_y,
4092 );
4093 } else {
4094 composite_non_isolated_group_cropped(
4095 pixmap,
4096 &offscreen,
4097 backdrop,
4098 effective_params,
4099 mask_ref,
4100 crop_x,
4101 crop_y,
4102 );
4103 }
4104 } else {
4105 let paint = stet_tiny_skia::PixmapPaint {
4106 opacity: effective_params.alpha as f32,
4107 blend_mode: u8_to_blend_mode(effective_params.blend_mode),
4108 quality: stet_tiny_skia::FilterQuality::Nearest,
4109 };
4110 pixmap.draw_pixmap(
4111 crop_x,
4112 crop_y,
4113 offscreen.as_ref(),
4114 &paint,
4115 Transform::identity(),
4116 mask_ref,
4117 );
4118 }
4119
4120 if !cmyk_compose_done
4126 && let (Some(group_cmyk), Some(parent_cmyk)) =
4127 (&group_band.cmyk_buffer, &mut band_state.cmyk_buffer)
4128 {
4129 copy_cmyk_buffer_to_parent(
4130 parent_cmyk,
4131 group_cmyk,
4132 offscreen.data(),
4133 crop_x as usize,
4134 crop_y as usize,
4135 eff_w as usize,
4136 eff_h as usize,
4137 ctx.out_w as usize,
4138 ctx.out_h as usize,
4139 );
4140 }
4141}
4142
4143#[allow(clippy::too_many_arguments)]
4155fn composite_non_isolated_cmyk(
4156 target: &mut Pixmap,
4157 parent_cmyk: Option<&mut [f32]>,
4158 source: &Pixmap,
4159 source_cmyk: &[f32],
4160 backdrop_cmyk: &[f32],
4161 backdrop_pixels: &[u8],
4162 params: &stet_graphics::display_list::GroupParams,
4163 clip_mask: Option<&stet_tiny_skia::Mask>,
4164 crop_x: i32,
4165 crop_y: i32,
4166 icc: Option<&IccCache>,
4167) {
4168 let cw = source.width() as usize;
4169 let ch = source.height() as usize;
4170 let target_w = target.width() as usize;
4171 let target_h = target.height() as usize;
4172
4173 let opacity = params.alpha.clamp(0.0, 1.0);
4174 let blend_mode = params.blend_mode;
4175 let is_nonseparable = matches!(blend_mode, 12..=15);
4176
4177 let target_data = target.data_mut();
4178 let target_stride = target_w * 4;
4179 let group_stride = cw * 4;
4180
4181 let clip_data = clip_mask.map(|m| m.data());
4182
4183 for gy in 0..ch {
4184 let ty = crop_y + gy as i32;
4185 if ty < 0 || ty as usize >= target_h {
4186 continue;
4187 }
4188 let ty = ty as usize;
4189 let group_row = gy * group_stride;
4190 let target_row = ty * target_stride;
4191
4192 for gx in 0..cw {
4193 let tx = crop_x + gx as i32;
4194 if tx < 0 || tx as usize >= target_w {
4195 continue;
4196 }
4197 let tx = tx as usize;
4198 let gi = group_row + gx * 4;
4199 let ti = target_row + tx * 4;
4200
4201 let bc = backdrop_cmyk[gi] as f64;
4203 let bm = backdrop_cmyk[gi + 1] as f64;
4204 let by_ = backdrop_cmyk[gi + 2] as f64;
4205 let bk = backdrop_cmyk[gi + 3] as f64;
4206 let sc = source_cmyk[gi] as f64;
4207 let sm = source_cmyk[gi + 1] as f64;
4208 let sy_ = source_cmyk[gi + 2] as f64;
4209 let sk = source_cmyk[gi + 3] as f64;
4210 if (sc - bc).abs() < 1.0 / 255.0
4211 && (sm - bm).abs() < 1.0 / 255.0
4212 && (sy_ - by_).abs() < 1.0 / 255.0
4213 && (sk - bk).abs() < 1.0 / 255.0
4214 {
4215 continue;
4216 }
4217
4218 let cov = if let Some(cd) = clip_data {
4220 cd[ty * target_w + tx] as f64 / 255.0
4221 } else {
4222 1.0
4223 };
4224 if cov <= 0.0 {
4225 continue;
4226 }
4227
4228 let backdrop_alpha = backdrop_pixels[gi + 3];
4247 let backdrop_transparent = backdrop_alpha == 0;
4248
4249 let mix = cov * opacity;
4250 let dst_a = target_data[ti + 3] as f64 / 255.0;
4251
4252 if backdrop_transparent {
4253 let src_data = source.data();
4266 let src_a_pm = src_data[gi + 3] as f64 / 255.0;
4267 if src_a_pm <= 0.0 {
4268 continue;
4269 }
4270 let (full_r, full_g, full_b) = icc
4275 .and_then(|i| i.convert_cmyk_readonly(sc, sm, sy_, sk))
4276 .unwrap_or_else(|| cmyk_to_rgb_plrm(sc, sm, sy_, sk));
4277 let alpha_s = mix;
4278 let inv_sa = 1.0 - alpha_s;
4279 let dst_r_pm = target_data[ti] as f64 / 255.0;
4280 let dst_g_pm = target_data[ti + 1] as f64 / 255.0;
4281 let dst_b_pm = target_data[ti + 2] as f64 / 255.0;
4282 let out_r = full_r * alpha_s + dst_r_pm * inv_sa;
4283 let out_g = full_g * alpha_s + dst_g_pm * inv_sa;
4284 let out_b = full_b * alpha_s + dst_b_pm * inv_sa;
4285 let out_a = alpha_s + dst_a * inv_sa;
4286 target_data[ti] = (out_r * 255.0).round().clamp(0.0, 255.0) as u8;
4287 target_data[ti + 1] = (out_g * 255.0).round().clamp(0.0, 255.0) as u8;
4288 target_data[ti + 2] = (out_b * 255.0).round().clamp(0.0, 255.0) as u8;
4289 target_data[ti + 3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4290 continue;
4291 }
4292
4293 let (rc, rm, ry, rk) = if is_nonseparable {
4295 let r = blend_cmyk_nonseparable([bc, bm, by_, bk], [sc, sm, sy_, sk], blend_mode);
4296 (r[0], r[1], r[2], r[3])
4297 } else {
4298 (
4299 blend_cmyk_separable_channel(bc, sc, blend_mode),
4300 blend_cmyk_separable_channel(bm, sm, blend_mode),
4301 blend_cmyk_separable_channel(by_, sy_, blend_mode),
4302 blend_cmyk_separable_channel(bk, sk, blend_mode),
4303 )
4304 };
4305
4306 let (new_r, new_g, new_b) = icc
4307 .and_then(|i| i.convert_cmyk_readonly(rc, rm, ry, rk))
4308 .unwrap_or_else(|| cmyk_to_rgb_plrm(rc, rm, ry, rk));
4309
4310 let alpha_s = mix;
4323 let alpha_b = dst_a;
4324 let out_a = alpha_s + alpha_b * (1.0 - alpha_s);
4325 if out_a <= 0.0 {
4326 continue;
4327 }
4328 let (dst_r, dst_g, dst_b) = if alpha_b > 0.0 {
4329 let inv_a = 1.0 / alpha_b;
4330 (
4331 (target_data[ti] as f64 / 255.0) * inv_a,
4332 (target_data[ti + 1] as f64 / 255.0) * inv_a,
4333 (target_data[ti + 2] as f64 / 255.0) * inv_a,
4334 )
4335 } else {
4336 (0.0, 0.0, 0.0)
4337 };
4338 let coef_b = alpha_s * alpha_b;
4346 let coef_s = alpha_s * (1.0 - alpha_b);
4347 let coef_d = (1.0 - alpha_s) * alpha_b;
4348 let out_r = (coef_s * new_r + coef_b * new_r + coef_d * dst_r) / out_a;
4349 let out_g = (coef_s * new_g + coef_b * new_g + coef_d * dst_g) / out_a;
4350 let out_b = (coef_s * new_b + coef_b * new_b + coef_d * dst_b) / out_a;
4351
4352 target_data[ti] = (out_r * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4353 target_data[ti + 1] = (out_g * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4354 target_data[ti + 2] = (out_b * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4355 target_data[ti + 3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
4356 }
4357 }
4358
4359 if let Some(parent_cmyk) = parent_cmyk {
4364 for gy in 0..ch {
4365 let ty = crop_y + gy as i32;
4366 if ty < 0 || ty as usize >= target_h {
4367 continue;
4368 }
4369 let ty = ty as usize;
4370 let group_row = gy * group_stride;
4371 let parent_row = ty * target_stride;
4372
4373 for gx in 0..cw {
4374 let tx = crop_x + gx as i32;
4375 if tx < 0 || tx as usize >= target_w {
4376 continue;
4377 }
4378 let tx = tx as usize;
4379 let gi = group_row + gx * 4;
4380 let pi = parent_row + tx * 4;
4381
4382 let bc = backdrop_cmyk[gi] as f64;
4383 let bm = backdrop_cmyk[gi + 1] as f64;
4384 let by_ = backdrop_cmyk[gi + 2] as f64;
4385 let bk = backdrop_cmyk[gi + 3] as f64;
4386 let sc = source_cmyk[gi] as f64;
4387 let sm = source_cmyk[gi + 1] as f64;
4388 let sy_ = source_cmyk[gi + 2] as f64;
4389 let sk = source_cmyk[gi + 3] as f64;
4390 if (sc - bc).abs() < 1.0 / 255.0
4391 && (sm - bm).abs() < 1.0 / 255.0
4392 && (sy_ - by_).abs() < 1.0 / 255.0
4393 && (sk - bk).abs() < 1.0 / 255.0
4394 {
4395 continue;
4396 }
4397
4398 let backdrop_transparent = backdrop_pixels[gi + 3] == 0;
4404 let (rc, rm, ry, rk) = if backdrop_transparent {
4405 (sc, sm, sy_, sk)
4406 } else if is_nonseparable {
4407 let r =
4408 blend_cmyk_nonseparable([bc, bm, by_, bk], [sc, sm, sy_, sk], blend_mode);
4409 (r[0], r[1], r[2], r[3])
4410 } else {
4411 (
4412 blend_cmyk_separable_channel(bc, sc, blend_mode),
4413 blend_cmyk_separable_channel(bm, sm, blend_mode),
4414 blend_cmyk_separable_channel(by_, sy_, blend_mode),
4415 blend_cmyk_separable_channel(bk, sk, blend_mode),
4416 )
4417 };
4418 parent_cmyk[pi] = rc as f32;
4419 parent_cmyk[pi + 1] = rm as f32;
4420 parent_cmyk[pi + 2] = ry as f32;
4421 parent_cmyk[pi + 3] = rk as f32;
4422 }
4423 }
4424 }
4425}
4426
4427fn render_knockout_group(
4432 pixmap: &mut Pixmap,
4433 band_state: &mut BandState,
4434 elements: &DisplayList,
4435 params: &stet_graphics::display_list::GroupParams,
4436 ctx: &RenderContext<'_>,
4437) {
4438 let crop = compute_group_crop(¶ms.bbox, ctx);
4439
4440 let (eff_w, eff_h, crop_x, crop_y, eff_vp_x, eff_vp_y) = match crop {
4441 Some((cx, cy, cw, ch)) => (
4442 cw,
4443 ch,
4444 cx,
4445 cy,
4446 ctx.vp_x + cx as f32 / ctx.scale_x,
4447 ctx.vp_y + cy as f32 / ctx.scale_y,
4448 ),
4449 None => (ctx.out_w, ctx.out_h, 0, 0, ctx.vp_x, ctx.vp_y),
4450 };
4451
4452 let Some(mut offscreen) = Pixmap::new(eff_w, eff_h) else {
4453 return;
4454 };
4455
4456 let initial_backdrop = if !params.isolated {
4457 if crop.is_some() {
4458 copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h)
4459 } else {
4460 pixmap.data().to_vec()
4461 }
4462 } else {
4463 vec![0u8; (eff_w * eff_h * 4) as usize]
4464 };
4465
4466 let Some(mut accumulated) = Pixmap::new(eff_w, eff_h) else {
4467 return;
4468 };
4469 accumulated.data_mut().copy_from_slice(&initial_backdrop);
4470
4471 let needs_cmyk = has_overprint_elements(elements) || band_state.cmyk_buffer.is_some();
4473 let initial_cmyk = if needs_cmyk {
4474 let buf_size = eff_w as usize * eff_h as usize * 4;
4475 let mut buf = vec![0.0f32; buf_size];
4476 if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
4477 let parent_stride = ctx.out_w as usize * 4;
4478 let group_stride = eff_w as usize * 4;
4479 for gy in 0..eff_h as usize {
4480 let py = crop_y as usize + gy;
4481 if py < ctx.out_h as usize {
4482 let p_start = py * parent_stride + crop_x as usize * 4;
4483 let g_start = gy * group_stride;
4484 let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
4485 buf[g_start..g_start + copy_len]
4486 .copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
4487 }
4488 }
4489 }
4490 Some(buf)
4491 } else {
4492 None
4493 };
4494
4495 let mut accumulated_cmyk = initial_cmyk.clone();
4496
4497 let group_ctx = RenderContext {
4502 vp_x: eff_vp_x,
4503 vp_y: eff_vp_y,
4504 scale_x: ctx.scale_x,
4505 scale_y: ctx.scale_y,
4506 out_w: eff_w,
4507 out_h: eff_h,
4508 effective_dpi: ctx.effective_dpi,
4509 icc: ctx.icc,
4510 image_cache: None,
4511 preprocessed: None,
4512 elem_idx: 0,
4513 no_aa: true,
4514 opm_zero_transparent: ctx.opm_zero_transparent,
4515 knockout_painter_pass: ctx.knockout_painter_pass,
4516 parent_group_isolated: true,
4520 alpha_extraction_pass: false,
4521 layer_set: ctx.layer_set,
4522 };
4523
4524 let mut ko_band = BandState {
4528 clip_region: None,
4529 spare_mask: None,
4530 clip_mask_cache: HashMap::new(),
4531 clip_mask_seen: HashSet::new(),
4532 mask_pool: Vec::new(),
4533 cmyk_buffer: None,
4534 op_bg_snapshot: None,
4535 op_touched: None,
4536 spot_mask: None,
4537 };
4538
4539 let mut coverage_offscreen: Option<Pixmap> = None;
4542
4543 for elem in elements.elements() {
4544 match elem {
4545 DisplayElement::Clip { .. } | DisplayElement::InitClip => {
4547 render_element(&mut offscreen, &mut ko_band, elem, &group_ctx);
4548 }
4549 DisplayElement::Group { .. } => {
4559 let pass1_ctx = RenderContext {
4566 knockout_painter_pass: KnockoutPainterPass::ColorPass,
4567 ..group_ctx
4568 };
4569 offscreen.data_mut().copy_from_slice(&initial_backdrop);
4570 ko_band.cmyk_buffer = initial_cmyk.clone();
4571 render_element(&mut offscreen, &mut ko_band, elem, &pass1_ctx);
4572 let pass1_cmyk = ko_band.cmyk_buffer.take();
4573
4574 let cov = match coverage_offscreen.as_mut() {
4579 Some(p) => {
4580 p.data_mut().fill(0);
4581 p
4582 }
4583 None => {
4584 let Some(p) = Pixmap::new(eff_w, eff_h) else {
4585 replace_changed_pixels(
4589 accumulated.data_mut(),
4590 offscreen.data(),
4591 &initial_backdrop,
4592 );
4593 if let (Some(p1), Some(acc)) = (&pass1_cmyk, &mut accumulated_cmyk) {
4594 replace_changed_cmyk(acc, p1, offscreen.data(), &initial_backdrop);
4595 }
4596 continue;
4597 };
4598 coverage_offscreen = Some(p);
4599 coverage_offscreen.as_mut().unwrap()
4600 }
4601 };
4602 ko_band.cmyk_buffer = None;
4603 let coverage_ctx = RenderContext {
4608 knockout_painter_pass: KnockoutPainterPass::CoveragePass,
4609 ..group_ctx
4610 };
4611 render_element(cov, &mut ko_band, elem, &coverage_ctx);
4612
4613 replace_with_coverage_mask(accumulated.data_mut(), offscreen.data(), cov.data());
4617
4618 if let (Some(p1_cmyk), Some(acc_cmyk)) = (&pass1_cmyk, &mut accumulated_cmyk) {
4619 replace_cmyk_with_coverage_mask(acc_cmyk, p1_cmyk, cov.data());
4620 }
4621 ko_band.cmyk_buffer = None;
4622 }
4623 _ => {
4627 offscreen.data_mut().copy_from_slice(&initial_backdrop);
4628
4629 ko_band.cmyk_buffer = initial_cmyk.clone();
4630
4631 render_element(&mut offscreen, &mut ko_band, elem, &group_ctx);
4632
4633 if let (Some(elem_cmyk), Some(acc_cmyk)) =
4634 (&ko_band.cmyk_buffer, &mut accumulated_cmyk)
4635 {
4636 replace_changed_cmyk(acc_cmyk, elem_cmyk, offscreen.data(), &initial_backdrop);
4637 }
4638 ko_band.cmyk_buffer = None;
4639
4640 replace_changed_pixels(accumulated.data_mut(), offscreen.data(), &initial_backdrop);
4641 }
4642 }
4643 }
4644
4645 let mut temp_mask = None;
4646 let mask_ref = resolve_clip_mask(
4647 &band_state.clip_region,
4648 &mut temp_mask,
4649 ctx.out_w,
4650 ctx.out_h,
4651 );
4652 let mask_ref = match mask_ref {
4653 None => return,
4654 Some(m) => m,
4655 };
4656
4657 composite_non_isolated_group_cropped(
4658 pixmap,
4659 &accumulated,
4660 &initial_backdrop,
4661 params,
4662 mask_ref,
4663 crop_x,
4664 crop_y,
4665 );
4666
4667 if let (Some(acc_cmyk), Some(parent_cmyk)) = (&accumulated_cmyk, &mut band_state.cmyk_buffer) {
4668 copy_cmyk_buffer_to_parent(
4669 parent_cmyk,
4670 acc_cmyk,
4671 accumulated.data(),
4672 crop_x as usize,
4673 crop_y as usize,
4674 eff_w as usize,
4675 eff_h as usize,
4676 ctx.out_w as usize,
4677 ctx.out_h as usize,
4678 );
4679 }
4680}
4681fn replace_with_coverage_mask(target: &mut [u8], source: &[u8], coverage: &[u8]) {
4689 for i in (0..target.len()).step_by(4) {
4690 let cov_a = coverage[i + 3];
4691 if cov_a == 0 {
4692 continue;
4693 }
4694 if cov_a == 255 {
4695 target[i..i + 4].copy_from_slice(&source[i..i + 4]);
4696 continue;
4697 }
4698 let a = cov_a as u32;
4699 let inv = 255 - a;
4700 for c in 0..4 {
4701 let s = source[i + c] as u32;
4702 let t = target[i + c] as u32;
4703 target[i + c] = ((s * a + t * inv + 127) / 255) as u8;
4704 }
4705 }
4706}
4707
4708fn replace_cmyk_with_coverage_mask(target: &mut [f32], source: &[f32], coverage: &[u8]) {
4712 let pixel_count = target.len() / 4;
4713 for i in 0..pixel_count {
4714 let pi = i * 4;
4715 let cov_a = coverage[pi + 3];
4716 if cov_a == 0 {
4717 continue;
4718 }
4719 if cov_a == 255 {
4720 target[pi..pi + 4].copy_from_slice(&source[pi..pi + 4]);
4721 continue;
4722 }
4723 let a = cov_a as f32 / 255.0;
4724 let inv = 1.0 - a;
4725 for c in 0..4 {
4726 target[pi + c] = source[pi + c] * a + target[pi + c] * inv;
4727 }
4728 }
4729}
4730
4731fn replace_changed_pixels(target: &mut [u8], source: &[u8], backdrop: &[u8]) {
4735 for i in (0..target.len()).step_by(4) {
4736 if source[i] != backdrop[i]
4737 || source[i + 1] != backdrop[i + 1]
4738 || source[i + 2] != backdrop[i + 2]
4739 || source[i + 3] != backdrop[i + 3]
4740 {
4741 target[i..i + 4].copy_from_slice(&source[i..i + 4]);
4742 }
4743 }
4744}
4745
4746#[allow(clippy::too_many_arguments)]
4750fn copy_cmyk_buffer_to_parent(
4751 parent_cmyk: &mut [f32],
4752 group_cmyk: &[f32],
4753 group_pixels: &[u8],
4754 crop_x: usize,
4755 crop_y: usize,
4756 group_w: usize,
4757 group_h: usize,
4758 parent_w: usize,
4759 parent_h: usize,
4760) {
4761 let parent_stride = parent_w * 4;
4762 let group_stride = group_w * 4;
4763 for gy in 0..group_h {
4764 let py = crop_y + gy;
4765 if py >= parent_h {
4766 break;
4767 }
4768 for gx in 0..group_w {
4769 let px = crop_x + gx;
4770 if px >= parent_w {
4771 break;
4772 }
4773 let g_pixel_idx = (gy * group_w + gx) * 4;
4778 let g_cmyk_idx = gy * group_stride + gx * 4;
4779 if group_pixels[g_pixel_idx + 3] > 0
4780 && (group_cmyk[g_cmyk_idx] != 0.0
4781 || group_cmyk[g_cmyk_idx + 1] != 0.0
4782 || group_cmyk[g_cmyk_idx + 2] != 0.0
4783 || group_cmyk[g_cmyk_idx + 3] != 0.0)
4784 {
4785 let p_cmyk_idx = py * parent_stride + px * 4;
4786 parent_cmyk[p_cmyk_idx..p_cmyk_idx + 4]
4787 .copy_from_slice(&group_cmyk[g_cmyk_idx..g_cmyk_idx + 4]);
4788 }
4789 }
4790 }
4791}
4792
4793fn replace_changed_cmyk(
4796 target_cmyk: &mut [f32],
4797 source_cmyk: &[f32],
4798 source_pixels: &[u8],
4799 backdrop_pixels: &[u8],
4800) {
4801 let pixel_count = target_cmyk.len() / 4;
4802 for i in 0..pixel_count {
4803 let pi = i * 4;
4804 if source_pixels[pi] != backdrop_pixels[pi]
4805 || source_pixels[pi + 1] != backdrop_pixels[pi + 1]
4806 || source_pixels[pi + 2] != backdrop_pixels[pi + 2]
4807 || source_pixels[pi + 3] != backdrop_pixels[pi + 3]
4808 {
4809 target_cmyk[pi..pi + 4].copy_from_slice(&source_cmyk[pi..pi + 4]);
4810 }
4811 }
4812}
4813
4814#[allow(clippy::too_many_arguments)]
4822fn render_soft_masked(
4823 pixmap: &mut Pixmap,
4824 band_state: &mut BandState,
4825 mask_list: &DisplayList,
4826 content_list: &DisplayList,
4827 params: &stet_graphics::display_list::SoftMaskParams,
4828 mask_cache: &Arc<Mutex<Option<Option<stet_graphics::display_list::MaskRaster>>>>,
4829 ctx: &RenderContext<'_>,
4830) {
4831 let use_inline_mask = ctx.vp_x != 0.0;
4848 let bbox = ¶ms.bbox;
4849 let smask_px_x0 = ((bbox[0] as f32 - ctx.vp_x) * ctx.scale_x).floor() as i32;
4850 let smask_px_y0 = ((bbox[1] as f32 - ctx.vp_y) * ctx.scale_y).floor() as i32;
4851 let smask_px_x1 = ((bbox[2] as f32 - ctx.vp_x) * ctx.scale_x).ceil() as i32;
4852 let smask_px_y1 = ((bbox[3] as f32 - ctx.vp_y) * ctx.scale_y).ceil() as i32;
4853
4854 let crop_x = smask_px_x0.max(0);
4856 let crop_y = smask_px_y0.max(0);
4857 let crop_x1 = smask_px_x1.min(ctx.out_w as i32);
4858 let crop_y1 = smask_px_y1.min(ctx.out_h as i32);
4859 if crop_x >= crop_x1 || crop_y >= crop_y1 {
4860 return;
4861 }
4862 let eff_w = (crop_x1 - crop_x) as u32;
4863 let eff_h = (crop_y1 - crop_y) as u32;
4864
4865 let eff_vp_x = ctx.vp_x + crop_x as f32 / ctx.scale_x;
4870 let eff_vp_y = ctx.vp_y + crop_y as f32 / ctx.scale_y;
4871
4872 let sub_ctx = RenderContext {
4873 vp_x: eff_vp_x,
4874 vp_y: eff_vp_y,
4875 scale_x: ctx.scale_x,
4876 scale_y: ctx.scale_y,
4877 out_w: eff_w,
4878 out_h: eff_h,
4879 effective_dpi: ctx.effective_dpi,
4880 icc: ctx.icc,
4881 image_cache: None,
4882 preprocessed: None,
4883 elem_idx: 0,
4884 no_aa: ctx.no_aa,
4885 opm_zero_transparent: ctx.opm_zero_transparent,
4886 knockout_painter_pass: ctx.knockout_painter_pass,
4887 parent_group_isolated: ctx.parent_group_isolated,
4888 alpha_extraction_pass: false,
4892 layer_set: ctx.layer_set,
4893 };
4894
4895 let mut mask_values_inline: Vec<u8> = Vec::new();
4899 if use_inline_mask {
4900 let Some(mut mask_pixmap) = Pixmap::new(eff_w, eff_h) else {
4901 return;
4902 };
4903 let mut mask_band = BandState {
4904 clip_region: None,
4905 spare_mask: None,
4906 clip_mask_cache: HashMap::new(),
4907 clip_mask_seen: HashSet::new(),
4908 mask_pool: Vec::new(),
4909 cmyk_buffer: None,
4910 op_bg_snapshot: None,
4911 op_touched: None,
4912 spot_mask: None,
4913 };
4914 for (idx, elem) in mask_list.elements().iter().enumerate() {
4915 let elem_ctx = RenderContext {
4916 elem_idx: idx,
4917 ..sub_ctx
4918 };
4919 render_element(&mut mask_pixmap, &mut mask_band, elem, &elem_ctx);
4920 }
4921 if params.has_nested_mask_scope
4922 && params.subtype == stet_graphics::display_list::SoftMaskSubtype::Luminosity
4923 {
4924 let bc = params.backdrop_color.as_ref();
4925 let bd_r = bc.map_or(0u8, |c| (c[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
4926 let bd_g = bc.map_or(0u8, |c| (c[1].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
4927 let bd_b = bc.map_or(0u8, |c| (c[2].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
4928 for chunk in mask_pixmap.data_mut().chunks_exact_mut(4) {
4929 let a = chunk[3] as u16;
4930 if a == 255 {
4931 continue;
4932 }
4933 let inv_a = 255 - a;
4934 chunk[0] = ((chunk[0] as u16 * 255 + bd_r as u16 * inv_a + 127) / 255) as u8;
4935 chunk[1] = ((chunk[1] as u16 * 255 + bd_g as u16 * inv_a + 127) / 255) as u8;
4936 chunk[2] = ((chunk[2] as u16 * 255 + bd_b as u16 * inv_a + 127) / 255) as u8;
4937 chunk[3] = 255;
4938 }
4939 }
4940 mask_values_inline = vec![0u8; (eff_w * eff_h) as usize];
4941 extract_soft_mask_values(mask_pixmap.data(), &mut mask_values_inline, params);
4942 }
4943
4944 let raster_owned: Option<stet_graphics::display_list::MaskRaster> = if use_inline_mask {
4946 None
4947 } else {
4948 let mut guard = mask_cache.lock().unwrap();
4949 let needs_build = match guard.as_ref() {
4950 None => true,
4951 Some(None) => false, Some(Some(r)) => {
4953 (r.scale_x - ctx.scale_x).abs() > 1e-4 || (r.scale_y - ctx.scale_y).abs() > 1e-4
4954 }
4955 };
4956 if needs_build {
4957 let built = rasterize_mask(
4958 mask_list,
4959 params,
4960 ctx.icc,
4961 ctx.no_aa,
4962 ctx.effective_dpi,
4963 ctx.scale_x,
4964 ctx.scale_y,
4965 ctx.layer_set,
4966 );
4967 *guard = Some(built);
4968 }
4969 guard.as_ref().and_then(|inner| inner.clone())
4970 };
4971
4972 let fallback_mask = out_of_bounds_mask_value(params) as i32;
4976
4977 let Some(mut content_pixmap) = Pixmap::new(eff_w, eff_h) else {
4981 return;
4982 };
4983 let backdrop = copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h);
4984 content_pixmap.data_mut().copy_from_slice(&backdrop);
4985
4986 let content_cmyk = if has_overprint_elements(content_list) || band_state.cmyk_buffer.is_some() {
4987 let buf_size = eff_w as usize * eff_h as usize * 4;
4988 let mut buf = vec![0.0f32; buf_size];
4989 if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
4990 let parent_stride = ctx.out_w as usize * 4;
4991 let group_stride = eff_w as usize * 4;
4992 for gy in 0..eff_h as usize {
4993 let py = crop_y as usize + gy;
4994 if py < ctx.out_h as usize {
4995 let p_start = py * parent_stride + crop_x as usize * 4;
4996 let g_start = gy * group_stride;
4997 let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
4998 buf[g_start..g_start + copy_len]
4999 .copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
5000 }
5001 }
5002 }
5003 Some(buf)
5004 } else {
5005 None
5006 };
5007 let backdrop_cmyk: Option<Vec<f32>> = content_cmyk.clone();
5014 let mut content_band = BandState {
5015 clip_region: None,
5016 spare_mask: None,
5017 clip_mask_cache: HashMap::new(),
5018 clip_mask_seen: HashSet::new(),
5019 mask_pool: Vec::new(),
5020 cmyk_buffer: content_cmyk,
5021 op_bg_snapshot: None,
5022 op_touched: None,
5023 spot_mask: None,
5024 };
5025 for (idx, elem) in content_list.elements().iter().enumerate() {
5026 let elem_ctx = RenderContext {
5027 elem_idx: idx,
5028 ..sub_ctx
5029 };
5030 render_element(&mut content_pixmap, &mut content_band, elem, &elem_ctx);
5031 }
5032
5033 let vp_x_pixels = (ctx.vp_x * ctx.scale_x).round() as i32;
5053 let vp_y_pixels = (ctx.vp_y * ctx.scale_y).round() as i32;
5054
5055 let mut temp_mask = None;
5056 let clip_ref = resolve_clip_mask(
5057 &band_state.clip_region,
5058 &mut temp_mask,
5059 ctx.out_w,
5060 ctx.out_h,
5061 );
5062 let clip_ref = match clip_ref {
5063 None => return,
5064 Some(m) => m,
5065 };
5066
5067 let use_cmyk_blend = ctx.icc.is_some()
5083 && backdrop_cmyk.is_some()
5084 && content_band.cmyk_buffer.is_some()
5085 && content_list_is_simple_native_cmyk(content_list);
5086
5087 let content_data = content_pixmap.data();
5088 let parent_data = pixmap.data_mut();
5089 let parent_stride = ctx.out_w as usize * 4;
5090 let content_stride = eff_w as usize * 4;
5091
5092 for y in 0..eff_h as usize {
5093 let py = crop_y as usize + y;
5094 if py >= ctx.out_h as usize {
5095 break;
5096 }
5097 let ci_row = y * content_stride;
5098 let pi_row = py * parent_stride;
5099 let page_y = vp_y_pixels + crop_y + y as i32;
5100
5101 for x in 0..eff_w as usize {
5102 let px = crop_x as usize + x;
5103 if px >= ctx.out_w as usize {
5104 break;
5105 }
5106
5107 if let Some(clip) = clip_ref {
5109 if clip.data()[py * ctx.out_w as usize + px] == 0 {
5110 continue;
5111 }
5112 }
5113
5114 let m = if use_inline_mask {
5117 mask_values_inline[y * eff_w as usize + x] as i32
5118 } else if let Some(ref raster) = raster_owned {
5119 let page_x = vp_x_pixels + crop_x + x as i32;
5120 let mx = page_x - raster.origin_x;
5121 let my = page_y - raster.origin_y;
5122 if mx >= 0 && (mx as u32) < raster.width && my >= 0 && (my as u32) < raster.height {
5123 raster.data[my as usize * raster.width as usize + mx as usize] as i32
5124 } else {
5125 fallback_mask
5126 }
5127 } else {
5128 fallback_mask
5129 };
5130 if m == 0 {
5131 continue;
5132 }
5133
5134 let ci = ci_row + x * 4;
5135 let pi = pi_row + px * 4;
5136
5137 let ci_cmyk = (y * eff_w as usize + x) * 4;
5147 let cmyk_path_ok = use_cmyk_blend && {
5148 let bc_cmyk = &backdrop_cmyk.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5149 let cc_cmyk = &content_band.cmyk_buffer.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5150 let icc_match = |cmyk: &[f32], rgb: &[u8]| -> bool {
5151 let (r, g, b) = ctx
5152 .icc
5153 .and_then(|i| {
5154 i.convert_cmyk_readonly(
5155 cmyk[0] as f64,
5156 cmyk[1] as f64,
5157 cmyk[2] as f64,
5158 cmyk[3] as f64,
5159 )
5160 })
5161 .unwrap_or_else(|| {
5162 cmyk_to_rgb_plrm(
5163 cmyk[0] as f64,
5164 cmyk[1] as f64,
5165 cmyk[2] as f64,
5166 cmyk[3] as f64,
5167 )
5168 });
5169 let r = (r * 255.0).round() as i32;
5170 let g = (g * 255.0).round() as i32;
5171 let b = (b * 255.0).round() as i32;
5172 (r - rgb[0] as i32).abs() <= 3
5173 && (g - rgb[1] as i32).abs() <= 3
5174 && (b - rgb[2] as i32).abs() <= 3
5175 };
5176 icc_match(bc_cmyk, &backdrop[ci..ci + 3])
5177 && icc_match(cc_cmyk, &content_data[ci..ci + 3])
5178 };
5179
5180 if cmyk_path_ok {
5181 let bc_cmyk = &backdrop_cmyk.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5183 let cc_cmyk = &content_band.cmyk_buffer.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
5184 let mf = m as f64 / 255.0;
5185 let rc = bc_cmyk[0] as f64 + mf * (cc_cmyk[0] as f64 - bc_cmyk[0] as f64);
5186 let rm = bc_cmyk[1] as f64 + mf * (cc_cmyk[1] as f64 - bc_cmyk[1] as f64);
5187 let ry = bc_cmyk[2] as f64 + mf * (cc_cmyk[2] as f64 - bc_cmyk[2] as f64);
5188 let rk = bc_cmyk[3] as f64 + mf * (cc_cmyk[3] as f64 - bc_cmyk[3] as f64);
5189 let (fr, fg, fb) = ctx
5190 .icc
5191 .and_then(|i| i.convert_cmyk_readonly(rc, rm, ry, rk))
5192 .unwrap_or_else(|| cmyk_to_rgb_plrm(rc, rm, ry, rk));
5193 parent_data[pi] = (fr * 255.0).round().clamp(0.0, 255.0) as u8;
5194 parent_data[pi + 1] = (fg * 255.0).round().clamp(0.0, 255.0) as u8;
5195 parent_data[pi + 2] = (fb * 255.0).round().clamp(0.0, 255.0) as u8;
5196 let content_a = content_data[ci + 3] as i32;
5198 let backdrop_a = backdrop[ci + 3] as i32;
5199 let delta = content_a - backdrop_a;
5200 if delta != 0 {
5201 let masked_delta = if delta > 0 {
5202 (delta * m + 128) / 255
5203 } else {
5204 (delta * m - 128) / 255
5205 };
5206 let result = (parent_data[pi + 3] as i32 + masked_delta).clamp(0, 255);
5207 parent_data[pi + 3] = result as u8;
5208 }
5209 } else {
5218 for c in 0..4 {
5219 let content_val = content_data[ci + c] as i32;
5220 let backdrop_val = backdrop[ci + c] as i32;
5221 let delta = content_val - backdrop_val;
5222 if delta != 0 {
5223 let masked_delta = if delta > 0 {
5224 (delta * m + 128) / 255
5225 } else {
5226 (delta * m - 128) / 255
5227 };
5228 let result = (parent_data[pi + c] as i32 + masked_delta).clamp(0, 255);
5229 parent_data[pi + c] = result as u8;
5230 }
5231 }
5232 }
5233 }
5234 }
5235
5236 if !use_cmyk_blend {
5240 if let (Some(content_cmyk), Some(parent_cmyk)) =
5241 (&content_band.cmyk_buffer, &mut band_state.cmyk_buffer)
5242 {
5243 copy_cmyk_buffer_to_parent(
5244 parent_cmyk,
5245 content_cmyk,
5246 content_pixmap.data(),
5247 crop_x as usize,
5248 crop_y as usize,
5249 eff_w as usize,
5250 eff_h as usize,
5251 ctx.out_w as usize,
5252 ctx.out_h as usize,
5253 );
5254 }
5255 }
5256}
5257fn extract_soft_mask_values(
5259 rgba: &[u8],
5260 out: &mut [u8],
5261 params: &stet_graphics::display_list::SoftMaskParams,
5262) {
5263 use stet_graphics::display_list::SoftMaskSubtype;
5264 let pixel_count = out.len();
5265
5266 match params.subtype {
5267 SoftMaskSubtype::Alpha => {
5268 for i in 0..pixel_count {
5269 let a = rgba[i * 4 + 3]; out[i] = if params.transfer_invert { 255 - a } else { a };
5271 }
5272 }
5273 SoftMaskSubtype::Luminosity => {
5274 let backdrop_lum = if let Some(bc) = ¶ms.backdrop_color {
5276 (0.2126 * bc[0] + 0.7152 * bc[1] + 0.0722 * bc[2]).clamp(0.0, 1.0)
5277 } else {
5278 0.0 };
5280 let backdrop_byte = (backdrop_lum * 255.0 + 0.5) as u8;
5281
5282 #[allow(clippy::needless_range_loop)]
5283 for i in 0..pixel_count {
5284 let off = i * 4;
5285 let a = rgba[off + 3];
5286 let lum_byte = if a == 0 {
5287 backdrop_byte
5288 } else if a < 255 {
5289 let af = a as f64;
5292 let bd = backdrop_lum * 255.0;
5293 let r = rgba[off] as f64 + bd * (255.0 - af) / 255.0;
5294 let g = rgba[off + 1] as f64 + bd * (255.0 - af) / 255.0;
5295 let b = rgba[off + 2] as f64 + bd * (255.0 - af) / 255.0;
5296 let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
5297 (lum + 0.5).clamp(0.0, 255.0) as u8
5298 } else {
5299 let lum = 0.2126 * rgba[off] as f64
5301 + 0.7152 * rgba[off + 1] as f64
5302 + 0.0722 * rgba[off + 2] as f64;
5303 (lum + 0.5).clamp(0.0, 255.0) as u8
5304 };
5305 out[i] = if params.transfer_invert {
5307 255 - lum_byte
5308 } else {
5309 lum_byte
5310 };
5311 }
5312 }
5313 }
5314}
5315
5316fn out_of_bounds_mask_value(params: &stet_graphics::display_list::SoftMaskParams) -> u8 {
5324 use stet_graphics::display_list::SoftMaskSubtype;
5325 let raw = match params.subtype {
5326 SoftMaskSubtype::Alpha => 0u8,
5327 SoftMaskSubtype::Luminosity => {
5328 let lum = if let Some(bc) = ¶ms.backdrop_color {
5329 (0.2126 * bc[0] + 0.7152 * bc[1] + 0.0722 * bc[2]).clamp(0.0, 1.0)
5330 } else {
5331 0.0
5332 };
5333 (lum * 255.0 + 0.5) as u8
5334 }
5335 };
5336 if params.transfer_invert {
5337 255 - raw
5338 } else {
5339 raw
5340 }
5341}
5342
5343const MAX_MASK_RASTER_PIXELS: u64 = 64 * 1024 * 1024;
5350
5351fn rasterize_mask(
5363 mask_list: &DisplayList,
5364 params: &stet_graphics::display_list::SoftMaskParams,
5365 icc: Option<&IccCache>,
5366 no_aa: bool,
5367 effective_dpi: f64,
5368 scale_x: f32,
5369 scale_y: f32,
5370 layer_set: &LayerSet,
5371) -> Option<stet_graphics::display_list::MaskRaster> {
5372 let mut bounds = compute_paint_bounds(mask_list, effective_dpi)?;
5379 if let Some(cap) = params.parent_clip_bbox {
5380 let cap_bbox = BBox2D {
5381 x_min: cap[0],
5382 y_min: cap[1],
5383 x_max: cap[2],
5384 y_max: cap[3],
5385 };
5386 bounds = intersect_bbox(&bounds, &cap_bbox)?;
5387 }
5388
5389 let px_x_min = (bounds.x_min as f32 * scale_x).floor() as i32 - 1;
5392 let px_y_min = (bounds.y_min as f32 * scale_y).floor() as i32 - 1;
5393 let px_x_max = (bounds.x_max as f32 * scale_x).ceil() as i32 + 1;
5394 let px_y_max = (bounds.y_max as f32 * scale_y).ceil() as i32 + 1;
5395 if px_x_min >= px_x_max || px_y_min >= px_y_max {
5396 return None;
5397 }
5398 let raster_w = (px_x_max - px_x_min) as u32;
5399 let raster_h = (px_y_max - px_y_min) as u32;
5400 if raster_w == 0 || raster_h == 0 {
5401 return None;
5402 }
5403 if (raster_w as u64) * (raster_h as u64) > MAX_MASK_RASTER_PIXELS {
5404 return None;
5405 }
5406
5407 let mut mask_pixmap = Pixmap::new(raster_w, raster_h)?;
5409
5410 let sub_ctx = RenderContext {
5414 vp_x: px_x_min as f32 / scale_x,
5415 vp_y: px_y_min as f32 / scale_y,
5416 scale_x,
5417 scale_y,
5418 out_w: raster_w,
5419 out_h: raster_h,
5420 effective_dpi,
5421 icc,
5422 image_cache: None,
5423 preprocessed: None,
5424 elem_idx: 0,
5425 no_aa,
5426 opm_zero_transparent: false,
5427 knockout_painter_pass: KnockoutPainterPass::None,
5428 parent_group_isolated: false,
5429 alpha_extraction_pass: false,
5430 layer_set,
5431 };
5432
5433 let mut mask_band = BandState {
5435 clip_region: None,
5436 spare_mask: None,
5437 clip_mask_cache: HashMap::new(),
5438 clip_mask_seen: HashSet::new(),
5439 mask_pool: Vec::new(),
5440 cmyk_buffer: None,
5441 op_bg_snapshot: None,
5442 op_touched: None,
5443 spot_mask: None,
5444 };
5445
5446 for (idx, elem) in mask_list.elements().iter().enumerate() {
5448 let elem_ctx = RenderContext {
5449 elem_idx: idx,
5450 ..sub_ctx
5451 };
5452 render_element(&mut mask_pixmap, &mut mask_band, elem, &elem_ctx);
5453 }
5454
5455 if params.has_nested_mask_scope
5463 && params.subtype == stet_graphics::display_list::SoftMaskSubtype::Luminosity
5464 {
5465 let bc = params.backdrop_color.as_ref();
5466 let bd_r = bc.map_or(0u8, |c| (c[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
5467 let bd_g = bc.map_or(0u8, |c| (c[1].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
5468 let bd_b = bc.map_or(0u8, |c| (c[2].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
5469 for chunk in mask_pixmap.data_mut().chunks_exact_mut(4) {
5470 let a = chunk[3] as u16;
5471 if a == 255 {
5472 continue;
5473 }
5474 let inv_a = 255 - a;
5475 chunk[0] = ((chunk[0] as u16 * 255 + bd_r as u16 * inv_a + 127) / 255) as u8;
5476 chunk[1] = ((chunk[1] as u16 * 255 + bd_g as u16 * inv_a + 127) / 255) as u8;
5477 chunk[2] = ((chunk[2] as u16 * 255 + bd_b as u16 * inv_a + 127) / 255) as u8;
5478 chunk[3] = 255;
5479 }
5480 }
5481
5482 let pixel_count = (raster_w * raster_h) as usize;
5484 let mut data = vec![0u8; pixel_count];
5485 extract_soft_mask_values(mask_pixmap.data(), &mut data, params);
5486
5487 Some(stet_graphics::display_list::MaskRaster {
5488 data,
5489 width: raster_w,
5490 height: raster_h,
5491 origin_x: px_x_min,
5492 origin_y: px_y_min,
5493 scale_x,
5494 scale_y,
5495 })
5496}
5497
5498fn transform_element_ctm(elem: &DisplayElement, pm: &Matrix) -> DisplayElement {
5502 match elem {
5503 DisplayElement::Fill { path, params } => {
5504 let mut p = params.clone();
5505 p.ctm = pm.concat(&p.ctm);
5506 DisplayElement::Fill {
5507 path: path.clone(),
5508 params: p,
5509 }
5510 }
5511 DisplayElement::Stroke { path, params } => {
5512 let mut p = params.clone();
5513 p.ctm = pm.concat(&p.ctm);
5514 DisplayElement::Stroke {
5515 path: path.clone(),
5516 params: p,
5517 }
5518 }
5519 DisplayElement::Clip { path, params } => {
5520 let mut p = params.clone();
5521 p.ctm = pm.concat(&p.ctm);
5522 if let Some(ref mut sp) = p.stroke_params {
5523 sp.ctm = pm.concat(&sp.ctm);
5524 }
5525 DisplayElement::Clip {
5526 path: path.clone(),
5527 params: p,
5528 }
5529 }
5530 DisplayElement::Image {
5531 sample_data,
5532 params,
5533 } => {
5534 let mut p = params.clone();
5535 p.ctm = pm.concat(&p.ctm);
5536 DisplayElement::Image {
5537 sample_data: sample_data.clone(),
5538 params: p,
5539 }
5540 }
5541 DisplayElement::MeshShading { params } => {
5542 let mut p = params.clone();
5543 p.ctm = pm.concat(&p.ctm);
5544 DisplayElement::MeshShading { params: p }
5545 }
5546 DisplayElement::PatchShading { params } => {
5547 let mut p = params.clone();
5548 p.ctm = pm.concat(&p.ctm);
5549 DisplayElement::PatchShading { params: p }
5550 }
5551 DisplayElement::AxialShading { params } => {
5552 let mut p = params.clone();
5553 p.ctm = pm.concat(&p.ctm);
5554 DisplayElement::AxialShading { params: p }
5555 }
5556 DisplayElement::RadialShading { params } => {
5557 let mut p = params.clone();
5558 p.ctm = pm.concat(&p.ctm);
5559 DisplayElement::RadialShading { params: p }
5560 }
5561 DisplayElement::Group { elements, params } => {
5562 let mut t = DisplayList::new();
5563 for child in elements.elements() {
5564 t.push(transform_element_ctm(child, pm));
5565 }
5566 let mut p = params.clone();
5567 let corners = [
5568 pm.transform_point(p.bbox[0], p.bbox[1]),
5569 pm.transform_point(p.bbox[2], p.bbox[1]),
5570 pm.transform_point(p.bbox[0], p.bbox[3]),
5571 pm.transform_point(p.bbox[2], p.bbox[3]),
5572 ];
5573 p.bbox = [
5574 corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min),
5575 corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min),
5576 corners
5577 .iter()
5578 .map(|c| c.0)
5579 .fold(f64::NEG_INFINITY, f64::max),
5580 corners
5581 .iter()
5582 .map(|c| c.1)
5583 .fold(f64::NEG_INFINITY, f64::max),
5584 ];
5585 DisplayElement::Group {
5586 elements: t,
5587 params: p,
5588 }
5589 }
5590 DisplayElement::SoftMasked {
5591 mask,
5592 content,
5593 params,
5594 ..
5595 } => {
5596 let mut t_mask = DisplayList::new();
5597 for child in mask.elements() {
5598 t_mask.push(transform_element_ctm(child, pm));
5599 }
5600 let mut t_content = DisplayList::new();
5601 for child in content.elements() {
5602 t_content.push(transform_element_ctm(child, pm));
5603 }
5604 let mut p = params.clone();
5605 let corners = [
5606 pm.transform_point(p.bbox[0], p.bbox[1]),
5607 pm.transform_point(p.bbox[2], p.bbox[1]),
5608 pm.transform_point(p.bbox[0], p.bbox[3]),
5609 pm.transform_point(p.bbox[2], p.bbox[3]),
5610 ];
5611 p.bbox = [
5612 corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min),
5613 corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min),
5614 corners
5615 .iter()
5616 .map(|c| c.0)
5617 .fold(f64::NEG_INFINITY, f64::max),
5618 corners
5619 .iter()
5620 .map(|c| c.1)
5621 .fold(f64::NEG_INFINITY, f64::max),
5622 ];
5623 if let Some(pcb) = p.parent_clip_bbox {
5630 let pcb_corners = [
5631 pm.transform_point(pcb[0], pcb[1]),
5632 pm.transform_point(pcb[2], pcb[1]),
5633 pm.transform_point(pcb[0], pcb[3]),
5634 pm.transform_point(pcb[2], pcb[3]),
5635 ];
5636 p.parent_clip_bbox = Some([
5637 pcb_corners
5638 .iter()
5639 .map(|c| c.0)
5640 .fold(f64::INFINITY, f64::min),
5641 pcb_corners
5642 .iter()
5643 .map(|c| c.1)
5644 .fold(f64::INFINITY, f64::min),
5645 pcb_corners
5646 .iter()
5647 .map(|c| c.0)
5648 .fold(f64::NEG_INFINITY, f64::max),
5649 pcb_corners
5650 .iter()
5651 .map(|c| c.1)
5652 .fold(f64::NEG_INFINITY, f64::max),
5653 ]);
5654 }
5655 DisplayElement::SoftMasked {
5659 mask: t_mask,
5660 content: t_content,
5661 params: p,
5662 mask_cache: Arc::new(Mutex::new(None)),
5663 }
5664 }
5665 DisplayElement::PatternFill { params } => {
5666 let mut p = params.clone();
5667 p.pattern_matrix = pm.concat(&p.pattern_matrix);
5668 p.path = transform_path_by_matrix(&p.path, pm);
5670 if let Some(ref mut sp) = p.stroke_params {
5671 sp.ctm = pm.concat(&sp.ctm);
5672 }
5673 DisplayElement::PatternFill { params: p }
5674 }
5675 DisplayElement::OcgGroup {
5676 elements,
5677 visibility,
5678 } => {
5679 let mut t = DisplayList::new();
5680 for child in elements.elements() {
5681 t.push(transform_element_ctm(child, pm));
5682 }
5683 DisplayElement::OcgGroup {
5684 elements: t,
5685 visibility: visibility.clone(),
5686 }
5687 }
5688 other => other.clone(),
5689 }
5690}
5691
5692fn transform_path_by_matrix(path: &PsPath, m: &Matrix) -> PsPath {
5694 use stet_fonts::geometry::PathSegment;
5695 let mut out = PsPath::new();
5696 for seg in &path.segments {
5697 out.segments.push(match *seg {
5698 PathSegment::MoveTo(x, y) => {
5699 let (nx, ny) = m.transform_point(x, y);
5700 PathSegment::MoveTo(nx, ny)
5701 }
5702 PathSegment::LineTo(x, y) => {
5703 let (nx, ny) = m.transform_point(x, y);
5704 PathSegment::LineTo(nx, ny)
5705 }
5706 PathSegment::CurveTo {
5707 x1,
5708 y1,
5709 x2,
5710 y2,
5711 x3,
5712 y3,
5713 } => {
5714 let (nx1, ny1) = m.transform_point(x1, y1);
5715 let (nx2, ny2) = m.transform_point(x2, y2);
5716 let (nx3, ny3) = m.transform_point(x3, y3);
5717 PathSegment::CurveTo {
5718 x1: nx1,
5719 y1: ny1,
5720 x2: nx2,
5721 y2: ny2,
5722 x3: nx3,
5723 y3: ny3,
5724 }
5725 }
5726 PathSegment::ClosePath => PathSegment::ClosePath,
5727 });
5728 }
5729 out
5730}
5731
5732fn bilinear_prescale(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
5739 let mut dst = vec![0u8; (dw * dh * 4) as usize];
5740 for dy in 0..dh {
5741 let sy_f = (dy as f64 + 0.5) * sh as f64 / dh as f64 - 0.5;
5742 let sy0 = sy_f.floor().max(0.0) as u32;
5743 let sy1 = (sy0 + 1).min(sh - 1);
5744 let fy = (sy_f - sy0 as f64) as f32;
5745 let ify = 1.0 - fy;
5746 for dx in 0..dw {
5747 let sx_f = (dx as f64 + 0.5) * sw as f64 / dw as f64 - 0.5;
5748 let sx0 = sx_f.floor().max(0.0) as u32;
5749 let sx1 = (sx0 + 1).min(sw - 1);
5750 let fx = (sx_f - sx0 as f64) as f32;
5751 let ifx = 1.0 - fx;
5752
5753 let i00 = (sy0 * sw + sx0) as usize * 4;
5754 let i10 = (sy0 * sw + sx1) as usize * 4;
5755 let i01 = (sy1 * sw + sx0) as usize * 4;
5756 let i11 = (sy1 * sw + sx1) as usize * 4;
5757 let di = (dy * dw + dx) as usize * 4;
5758 for c in 0..4 {
5759 dst[di + c] = (src[i00 + c] as f32 * ifx * ify
5760 + src[i10 + c] as f32 * fx * ify
5761 + src[i01 + c] as f32 * ifx * fy
5762 + src[i11 + c] as f32 * fx * fy)
5763 .round() as u8;
5764 }
5765 }
5766 }
5767 dst
5768}
5769
5770fn render_pattern_fill(
5771 pixmap: &mut Pixmap,
5772 band_state: &mut BandState,
5773 params: &stet_graphics::device::PatternFillParams,
5774 ctx: &RenderContext<'_>,
5775) {
5776 let mut temp_mask = None;
5777 let Some(mask_ref) = resolve_clip_mask(
5778 &band_state.clip_region,
5779 &mut temp_mask,
5780 ctx.out_w,
5781 ctx.out_h,
5782 ) else {
5783 return;
5784 };
5785
5786 let pm = ¶ms.pattern_matrix;
5787
5788 let (step_ux, step_uy) = pm.transform_delta(params.xstep, 0.0);
5790 let (step_vx, step_vy) = pm.transform_delta(0.0, params.ystep);
5791
5792 let step_u_len = (step_ux * step_ux + step_uy * step_uy).sqrt();
5793 let step_v_len = (step_vx * step_vx + step_vy * step_vy).sqrt();
5794 if step_u_len < 0.01 || step_v_len < 0.01 {
5795 return;
5796 }
5797
5798 let origin_x = pm.tx;
5799 let origin_y = pm.ty;
5800
5801 let dev_vp_x = ctx.vp_x as f64;
5803 let dev_vp_y = ctx.vp_y as f64;
5804 let dev_vp_w = ctx.out_w as f64 / ctx.scale_x as f64;
5805 let dev_vp_h = ctx.out_h as f64 / ctx.scale_y as f64;
5806
5807 let (mut min_x, mut min_y, mut max_x, mut max_y) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
5808 for seg in ¶ms.path.segments {
5809 let (x, y) = match seg {
5810 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => (*x, *y),
5811 PathSegment::CurveTo { x3, y3, .. } => (*x3, *y3),
5812 PathSegment::ClosePath => continue,
5813 };
5814 min_x = min_x.min(x);
5815 min_y = min_y.min(y);
5816 max_x = max_x.max(x);
5817 max_y = max_y.max(y);
5818 }
5819
5820 if let Some(ref sp) = params.stroke_params {
5824 let ctm = &sp.ctm;
5826 let corners = [
5827 ctm.transform_point(min_x, min_y),
5828 ctm.transform_point(max_x, min_y),
5829 ctm.transform_point(min_x, max_y),
5830 ctm.transform_point(max_x, max_y),
5831 ];
5832 min_x = f64::MAX;
5833 min_y = f64::MAX;
5834 max_x = f64::MIN;
5835 max_y = f64::MIN;
5836 for (cx, cy) in &corners {
5837 min_x = min_x.min(*cx);
5838 min_y = min_y.min(*cy);
5839 max_x = max_x.max(*cx);
5840 max_y = max_y.max(*cy);
5841 }
5842 let half_w = sp.line_width
5844 * 0.5
5845 * (ctm.a * ctm.a + ctm.b * ctm.b)
5846 .sqrt()
5847 .max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt());
5848 min_x -= half_w;
5849 min_y -= half_w;
5850 max_x += half_w;
5851 max_y += half_w;
5852 }
5853
5854 min_x = min_x.max(dev_vp_x);
5856 min_y = min_y.max(dev_vp_y);
5857 max_x = max_x.min(dev_vp_x + dev_vp_w);
5858 max_y = max_y.min(dev_vp_y + dev_vp_h);
5859 if min_x >= max_x || min_y >= max_y {
5860 return;
5861 }
5862
5863 let det = step_ux * step_vy - step_uy * step_vx;
5864 if det.abs() < 1e-10 {
5865 return;
5866 }
5867 let inv_det = 1.0 / det;
5868
5869 let mut tu_min = f64::MAX;
5870 let mut tu_max = f64::MIN;
5871 let mut tv_min = f64::MAX;
5872 let mut tv_max = f64::MIN;
5873 for &(cx, cy) in &[
5874 (min_x, min_y),
5875 (max_x, min_y),
5876 (min_x, max_y),
5877 (max_x, max_y),
5878 ] {
5879 let dx = cx - origin_x;
5880 let dy = cy - origin_y;
5881 let tu = (dx * step_vy - dy * step_vx) * inv_det;
5882 let tv = (-dx * step_uy + dy * step_ux) * inv_det;
5883 tu_min = tu_min.min(tu);
5884 tu_max = tu_max.max(tu);
5885 tv_min = tv_min.min(tv);
5886 tv_max = tv_max.max(tv);
5887 }
5888
5889 let tile_x_start = tu_min.floor() as i32 - 1;
5890 let tile_x_end = tu_max.ceil() as i32 + 1;
5891 let tile_y_start = tv_min.floor() as i32 - 1;
5892 let tile_y_end = tv_max.ceil() as i32 + 1;
5893
5894 let tile_count = (tile_x_end - tile_x_start) as i64 * (tile_y_end - tile_y_start) as i64;
5895 if tile_count > 10000 {
5896 return;
5897 }
5898
5899 let Some(mut tile_buf) = Pixmap::new(ctx.out_w, ctx.out_h) else {
5900 return;
5901 };
5902
5903 let sx_f = ctx.scale_x as f64;
5904 let sy_f = ctx.scale_y as f64;
5905
5906 if params.device_space_tile {
5907 for tv in tile_y_start..tile_y_end {
5913 for tu in tile_x_start..tile_x_end {
5914 let offset_x = tu as f64 * step_ux + tv as f64 * step_vx;
5915 let offset_y = tu as f64 * step_uy + tv as f64 * step_vy;
5916
5917 let tile_ctx = RenderContext {
5918 vp_x: ctx.vp_x - offset_x as f32,
5919 vp_y: ctx.vp_y - offset_y as f32,
5920 scale_x: ctx.scale_x,
5921 scale_y: ctx.scale_y,
5922 out_w: ctx.out_w,
5923 out_h: ctx.out_h,
5924 effective_dpi: ctx.effective_dpi,
5925 icc: ctx.icc,
5926 image_cache: None,
5927 preprocessed: None,
5928 elem_idx: 0,
5929 no_aa: ctx.no_aa,
5930 opm_zero_transparent: params.overprint_mode == 1,
5931 knockout_painter_pass: ctx.knockout_painter_pass,
5932 parent_group_isolated: ctx.parent_group_isolated,
5933 alpha_extraction_pass: ctx.alpha_extraction_pass,
5934 layer_set: ctx.layer_set,
5935 };
5936
5937 let mut tile_band = BandState {
5938 clip_region: None,
5939 spare_mask: None,
5940 clip_mask_cache: HashMap::new(),
5941 clip_mask_seen: HashSet::new(),
5942 mask_pool: Vec::new(),
5943 cmyk_buffer: None,
5944 op_bg_snapshot: None,
5945 op_touched: None,
5946 spot_mask: None,
5947 };
5948
5949 for (idx, elem) in params.tile.elements().iter().enumerate() {
5950 let elem_ctx = RenderContext {
5951 elem_idx: idx,
5952 ..tile_ctx
5953 };
5954 render_element(&mut tile_buf, &mut tile_band, elem, &elem_ctx);
5955 }
5956 }
5957 }
5958 } else if params.tile.elements().iter().any(|e| {
5959 !matches!(
5960 e,
5961 DisplayElement::Fill { .. }
5962 | DisplayElement::Stroke { .. }
5963 | DisplayElement::Image { .. }
5964 | DisplayElement::Clip { .. }
5965 | DisplayElement::InitClip
5966 )
5967 }) {
5968 let bbox = ¶ms.bbox;
5972 let corners_dev = [
5973 pm.transform_point(bbox[0], bbox[1]),
5974 pm.transform_point(bbox[2], bbox[1]),
5975 pm.transform_point(bbox[0], bbox[3]),
5976 pm.transform_point(bbox[2], bbox[3]),
5977 ];
5978 let (mut td_x0, mut td_y0) = (f64::MAX, f64::MAX);
5979 let (mut td_x1, mut td_y1) = (f64::MIN, f64::MIN);
5980 for (x, y) in &corners_dev {
5981 td_x0 = td_x0.min(*x);
5982 td_y0 = td_y0.min(*y);
5983 td_x1 = td_x1.max(*x);
5984 td_y1 = td_y1.max(*y);
5985 }
5986 let tile_pw = ((td_x1 - td_x0) * sx_f).ceil().max(1.0) as u32;
5987 let tile_ph = ((td_y1 - td_y0) * sy_f).ceil().max(1.0) as u32;
5988 let tile_pw = tile_pw.min(8192);
5989 let tile_ph = tile_ph.min(8192);
5990
5991 if let Some(mut one_tile) = Pixmap::new(tile_pw, tile_ph) {
5992 let tile_render_ctx = RenderContext {
5993 vp_x: td_x0 as f32,
5994 vp_y: td_y0 as f32,
5995 scale_x: ctx.scale_x,
5996 scale_y: ctx.scale_y,
5997 out_w: tile_pw,
5998 out_h: tile_ph,
5999 effective_dpi: ctx.effective_dpi,
6000 icc: ctx.icc,
6001 image_cache: None,
6002 preprocessed: None,
6003 elem_idx: 0,
6004 no_aa: ctx.no_aa,
6005 opm_zero_transparent: params.overprint_mode == 1,
6006 knockout_painter_pass: ctx.knockout_painter_pass,
6007 parent_group_isolated: ctx.parent_group_isolated,
6008 alpha_extraction_pass: ctx.alpha_extraction_pass,
6009 layer_set: ctx.layer_set,
6010 };
6011 let mut tile_bs = BandState {
6012 clip_region: None,
6013 spare_mask: None,
6014 clip_mask_cache: HashMap::new(),
6015 clip_mask_seen: HashSet::new(),
6016 mask_pool: Vec::new(),
6017 cmyk_buffer: None,
6018 op_bg_snapshot: None,
6019 op_touched: None,
6020 spot_mask: None,
6021 };
6022 for (idx, elem) in params.tile.elements().iter().enumerate() {
6023 let transformed = transform_element_ctm(elem, pm);
6024 let elem_ctx = RenderContext {
6025 elem_idx: idx,
6026 ..tile_render_ctx
6027 };
6028 render_element(&mut one_tile, &mut tile_bs, &transformed, &elem_ctx);
6029 }
6030 for tv in tile_y_start..tile_y_end {
6032 for tu in tile_x_start..tile_x_end {
6033 let offset_x = tu as f64 * step_ux + tv as f64 * step_vx;
6034 let offset_y = tu as f64 * step_uy + tv as f64 * step_vy;
6035 let px = ((td_x0 + offset_x - dev_vp_x) * sx_f) as i32;
6036 let py = ((td_y0 + offset_y - dev_vp_y) * sy_f) as i32;
6037 let paint = stet_tiny_skia::PixmapPaint {
6038 opacity: 1.0,
6039 blend_mode: BlendMode::SourceOver,
6040 quality: stet_tiny_skia::FilterQuality::Nearest,
6041 };
6042 tile_buf.draw_pixmap(
6043 px,
6044 py,
6045 one_tile.as_ref(),
6046 &paint,
6047 Transform::identity(),
6048 None,
6049 );
6050 }
6051 }
6052 }
6053 } else {
6054 struct PreprocessedImage {
6062 rgba: Vec<u8>,
6063 width: u32,
6064 height: u32,
6065 img_transform: Transform,
6068 }
6069 let tile_elements = params.tile.elements();
6070 let mut preprocessed: Vec<Option<PreprocessedImage>> =
6071 Vec::with_capacity(tile_elements.len());
6072 let tt_sx = (pm.a * sx_f) as f32;
6074 let tt_sy = (pm.d * sy_f) as f32;
6075 let tt_kx = (pm.c * sx_f) as f32;
6076 let tt_ky = (pm.b * sy_f) as f32;
6077 for elem in tile_elements {
6078 if let DisplayElement::Image {
6079 sample_data,
6080 params: ip,
6081 } = elem
6082 {
6083 let iw = ip.width;
6084 let ih = ip.height;
6085 if iw > 0 && ih > 0 {
6086 let mut rgba =
6087 samples_to_rgba(sample_data, ip, ctx.icc, ctx.opm_zero_transparent);
6088 if ip.mask_color.is_some() {
6089 apply_mask_color_rgba(&mut rgba, sample_data, ip);
6090 }
6091 let expected = (iw * ih * 4) as usize;
6092 if rgba.len() >= expected {
6093 if let Some(inv) = ip.image_matrix.invert() {
6094 let combined_mat = ip.ctm.concat(&inv);
6095 let t = to_transform(&combined_mat);
6096 let test = t.post_concat(Transform::from_row(
6099 tt_sx, tt_ky, tt_kx, tt_sy, 0.0, 0.0,
6100 ));
6101 let eff_sx = (test.sx * test.sx + test.ky * test.ky).sqrt();
6102 let eff_sy = (test.kx * test.kx + test.sy * test.sy).sqrt();
6103 if eff_sx < 0.99 || eff_sy < 0.99 {
6104 let tw = (iw as f32 * eff_sx).floor().max(1.0) as u32;
6108 let th = (ih as f32 * eff_sy).floor().max(1.0) as u32;
6109 let scaled = bilinear_prescale(&rgba, iw, ih, tw, th);
6110 let adj = Transform::from_scale(
6113 iw as f32 / tw as f32,
6114 ih as f32 / th as f32,
6115 );
6116 preprocessed.push(Some(PreprocessedImage {
6117 rgba: scaled,
6118 width: tw,
6119 height: th,
6120 img_transform: t.pre_concat(adj),
6121 }));
6122 } else {
6123 preprocessed.push(Some(PreprocessedImage {
6124 rgba,
6125 width: iw,
6126 height: ih,
6127 img_transform: t,
6128 }));
6129 }
6130 } else {
6131 preprocessed.push(None);
6132 }
6133 } else {
6134 preprocessed.push(None);
6135 }
6136 } else {
6137 preprocessed.push(None);
6138 }
6139 }
6142 }
6143
6144 for tv in tile_y_start..tile_y_end {
6145 for tu in tile_x_start..tile_x_end {
6146 let pat_offset_x = tu as f64 * params.xstep;
6147 let pat_offset_y = tv as f64 * params.ystep;
6148
6149 let tile_transform = Transform::from_row(
6150 tt_sx,
6151 tt_ky,
6152 tt_kx,
6153 tt_sy,
6154 ((pm.a * pat_offset_x + pm.c * pat_offset_y + pm.tx - dev_vp_x) * sx_f) as f32,
6155 ((pm.b * pat_offset_x + pm.d * pat_offset_y + pm.ty - dev_vp_y) * sy_f) as f32,
6156 );
6157
6158 let bbox_clip = {
6160 let bb = ¶ms.bbox;
6161 let mut bp = stet_tiny_skia::PathBuilder::new();
6162 bp.move_to(bb[0] as f32, bb[1] as f32);
6163 bp.line_to(bb[2] as f32, bb[1] as f32);
6164 bp.line_to(bb[2] as f32, bb[3] as f32);
6165 bp.line_to(bb[0] as f32, bb[3] as f32);
6166 bp.close();
6167 bp.finish().and_then(|sp| {
6168 let mut m = Mask::new(ctx.out_w, ctx.out_h)?;
6169 m.fill_path(
6170 &sp,
6171 stet_tiny_skia::FillRule::Winding,
6172 false,
6173 tile_transform,
6174 );
6175 Some(m)
6176 })
6177 };
6178 let mut tile_clip: Option<Mask> = bbox_clip;
6179 let mut img_idx = 0usize;
6180 for elem in tile_elements {
6181 let clip_ref = tile_clip.as_ref();
6182 match elem {
6183 DisplayElement::Clip { path, params: cp } => {
6184 if let Some(sp) = build_skia_path(path) {
6185 let t = to_transform(&cp.ctm);
6186 let combined = t.post_concat(tile_transform);
6187 let mut mask = Mask::new(ctx.out_w, ctx.out_h).expect("mask");
6188 mask.fill_path(&sp, to_fill_rule(&cp.fill_rule), false, combined);
6189 if let Some(prev) = tile_clip.take() {
6190 intersect_masks(&mut mask, &prev);
6191 }
6192 tile_clip = Some(mask);
6193 }
6194 }
6195 DisplayElement::InitClip => {
6196 tile_clip = None;
6197 }
6198 DisplayElement::Fill { path, params: fp } => {
6199 if let Some(sp) = build_skia_path(path) {
6200 let mut paint = if params.paint_type == 1 {
6201 to_paint(&fp.color)
6202 } else {
6203 to_paint(
6204 params
6205 .underlying_color
6206 .as_ref()
6207 .unwrap_or(&DeviceColor::black()),
6208 )
6209 };
6210 paint.anti_alias = false;
6211 let t = to_transform(&fp.ctm);
6212 let combined = t.post_concat(tile_transform);
6213 let fr = to_fill_rule(&fp.fill_rule);
6214 tile_buf.fill_path(&sp, &paint, fr, combined, clip_ref);
6215 }
6216 }
6217 DisplayElement::Stroke { path, params: sp } => {
6218 if let Some(skp) = build_skia_path(path) {
6219 let effective_ctm = pm.concat(&sp.ctm);
6223 let mut sp_adj = sp.clone();
6224 sp_adj.ctm = effective_ctm;
6225 let stroke = build_stroke(&sp_adj, ctx.effective_dpi);
6226 let paint = if params.paint_type == 1 {
6227 to_paint(&sp.color)
6228 } else {
6229 to_paint(
6230 params
6231 .underlying_color
6232 .as_ref()
6233 .unwrap_or(&DeviceColor::black()),
6234 )
6235 };
6236 let t = to_transform(&sp.ctm);
6237 let combined = t.post_concat(tile_transform);
6238 tile_buf.stroke_path(&skp, &paint, &stroke, combined, clip_ref);
6239 }
6240 }
6241 DisplayElement::Image { .. } => {
6242 if let Some(ref pi) = preprocessed[img_idx] {
6243 let combined = pi.img_transform.post_concat(tile_transform);
6244 if let Some(img_ref) = stet_tiny_skia::PixmapRef::from_bytes(
6245 &pi.rgba, pi.width, pi.height,
6246 ) {
6247 let paint = stet_tiny_skia::PixmapPaint {
6248 opacity: 1.0,
6249 blend_mode: BlendMode::SourceOver,
6250 quality: stet_tiny_skia::FilterQuality::Nearest,
6251 };
6252 tile_buf.draw_pixmap(0, 0, img_ref, &paint, combined, clip_ref);
6253 }
6254 }
6255 img_idx += 1;
6256 }
6257 _ => {}
6258 }
6259 }
6260 }
6261 }
6262 }
6263
6264 let Some(fill_skia_path) = build_skia_path(¶ms.path) else {
6266 return;
6267 };
6268 let fill_rule = to_fill_rule(¶ms.fill_rule);
6269 let mut fill_mask = Mask::new(ctx.out_w, ctx.out_h).expect("mask");
6270 let path_transform = viewport_transform(
6271 Transform::identity(),
6272 ctx.vp_x,
6273 ctx.vp_y,
6274 ctx.scale_x,
6275 ctx.scale_y,
6276 );
6277 if let Some(ref sp) = params.stroke_params {
6278 let stroke = build_stroke(sp, ctx.effective_dpi);
6282 let ctm_transform = to_transform(&sp.ctm);
6283 let combined = ctm_transform.post_concat(path_transform);
6284 let res_scale = stet_tiny_skia::PathStroker::compute_resolution_scale(&combined);
6285 let dashed;
6286 let stroke_path = if let Some(ref dash) = stroke.dash {
6287 dashed = fill_skia_path.dash(dash, res_scale);
6288 match dashed.as_ref() {
6289 Some(p) => p,
6290 None => &fill_skia_path,
6291 }
6292 } else {
6293 &fill_skia_path
6294 };
6295 if let Some(outline) = stroke_path.stroke(&stroke, res_scale) {
6296 fill_mask.fill_path(
6297 &outline,
6298 stet_tiny_skia::FillRule::Winding,
6299 !ctx.no_aa,
6300 combined,
6301 );
6302 }
6303 } else {
6304 fill_mask.fill_path(&fill_skia_path, fill_rule, !ctx.no_aa, path_transform);
6305 }
6306
6307 if let Some(clip_mask) = mask_ref {
6308 intersect_masks(&mut fill_mask, clip_mask);
6309 }
6310
6311 let img_paint = stet_tiny_skia::PixmapPaint::default();
6312 pixmap.draw_pixmap(
6313 0,
6314 0,
6315 tile_buf.as_ref(),
6316 &img_paint,
6317 Transform::identity(),
6318 Some(&fill_mask),
6319 );
6320}
6321
6322fn clip_path_unified(
6327 band_state: &mut BandState,
6328 path: &PsPath,
6329 params: &ClipParams,
6330 ctx: &RenderContext<'_>,
6331) {
6332 let is_unit_scale = ctx.scale_x == 1.0 && ctx.scale_y == 1.0;
6333
6334 if is_unit_scale {
6336 let y_start = ctx.vp_y as u32;
6337 let x_start = ctx.vp_x as u32;
6338
6339 if x_start == 0
6344 && params.stroke_params.is_none()
6345 && params.ctm.a == 1.0
6346 && params.ctm.d == 1.0
6347 && params.ctm.tx == 0.0
6348 && params.ctm.ty == 0.0
6349 && let Some(bbox) = path_y_bbox(path)
6350 && (bbox.y_max <= y_start as f64 || bbox.y_min >= (y_start + ctx.out_h) as f64)
6351 {
6352 if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
6353 band_state.recycle_mask(mask);
6354 }
6355 band_state.clip_region = Some(ClipRegion::Rect(ClipRect {
6356 x0: 0,
6357 y0: 0,
6358 x1: 0,
6359 y1: 0,
6360 }));
6361 return;
6362 }
6363
6364 let ctm_is_identity = params.ctm.a == 1.0
6368 && params.ctm.b == 0.0
6369 && params.ctm.c == 0.0
6370 && params.ctm.d == 1.0
6371 && params.ctm.tx == 0.0
6372 && params.ctm.ty == 0.0;
6373 if x_start == 0
6374 && ctm_is_identity
6375 && params.stroke_params.is_none()
6376 && let Some(dev_rect) = detect_rect(path, ctx.out_w, u32::MAX)
6377 {
6378 let new_rect = translate_clip_rect(&dev_rect, y_start, ctx.out_h);
6379 match band_state.clip_region.take() {
6380 None => {
6381 band_state.clip_region = Some(ClipRegion::Rect(new_rect));
6382 }
6383 Some(ClipRegion::Rect(existing)) => {
6384 band_state.clip_region = Some(ClipRegion::Rect(existing.intersect(&new_rect)));
6385 }
6386 Some(ClipRegion::Mask(mut mask)) => {
6387 intersect_mask_with_rect(&mut mask, &new_rect, ctx.out_w, ctx.out_h);
6388 band_state.clip_region = Some(ClipRegion::Mask(mask));
6389 }
6390 }
6391 return;
6392 }
6393 }
6394
6395 let fill_rule = to_fill_rule(¶ms.fill_rule);
6397 let path_hash = hash_clip_path(path, ¶ms.fill_rule);
6398 let prev_region = band_state.clip_region.take();
6399
6400 let mut mask = band_state.take_mask(ctx.out_w, ctx.out_h);
6401
6402 let path_mask = if let Some(cached) = band_state.clip_mask_cache.get(&path_hash) {
6403 mask.data_mut().copy_from_slice(cached.data());
6404 mask
6405 } else {
6406 let Some(skia_path) = build_skia_path(path) else {
6407 band_state.recycle_mask(mask);
6408 band_state.clip_region = prev_region;
6409 return;
6410 };
6411 mask.data_mut().fill(0);
6412 if let Some(ref sp) = params.stroke_params {
6413 let stroke = build_stroke(sp, ctx.effective_dpi);
6416 let transform = ctx.transform(&sp.ctm);
6417 let res_scale = stet_tiny_skia::PathStroker::compute_resolution_scale(&transform);
6418 let dashed;
6419 let stroke_path = if let Some(ref dash) = stroke.dash {
6420 dashed = skia_path.dash(dash, res_scale);
6421 match dashed.as_ref() {
6422 Some(p) => p,
6423 None => &skia_path,
6424 }
6425 } else {
6426 &skia_path
6427 };
6428 if let Some(outline) = stroke_path.stroke(&stroke, res_scale) {
6429 mask.fill_path(
6430 &outline,
6431 stet_tiny_skia::FillRule::Winding,
6432 false,
6433 transform,
6434 );
6435 }
6436 } else {
6437 let transform = ctx.transform(¶ms.ctm);
6438 mask.fill_path(&skia_path, fill_rule, false, transform);
6439 }
6440 if !band_state.clip_mask_seen.insert(path_hash) {
6441 band_state.clip_mask_cache.insert(path_hash, mask.clone());
6442 }
6443 mask
6444 };
6445
6446 match prev_region {
6447 None => {
6448 band_state.clip_region = Some(ClipRegion::Mask(path_mask));
6449 }
6450 Some(ClipRegion::Rect(rect)) => {
6451 if rect.is_empty() {
6452 band_state.recycle_mask(path_mask);
6453 band_state.clip_region = Some(ClipRegion::Rect(rect));
6456 } else {
6457 let mut mask = path_mask;
6458 intersect_mask_with_rect(&mut mask, &rect, ctx.out_w, ctx.out_h);
6459 band_state.clip_region = Some(ClipRegion::Mask(mask));
6460 }
6461 }
6462 Some(ClipRegion::Mask(mut existing)) => {
6463 intersect_masks(&mut existing, &path_mask);
6464 band_state.recycle_mask(path_mask);
6465 band_state.clip_region = Some(ClipRegion::Mask(existing));
6466 }
6467 }
6468}
6469#[cfg(feature = "ps-device")]
6474impl OutputDevice for SkiaDevice {
6475 fn fill_path(&mut self, path: &PsPath, params: &FillParams) {
6476 self.ensure_full_pixmap();
6477 let Some(skia_path) = build_skia_path(path) else {
6478 return;
6479 };
6480 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6481 let mut temp_mask = None;
6482 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6483 return; };
6485
6486 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, self.no_aa);
6487 let transform = to_transform(¶ms.ctm);
6488 let fill_rule = to_fill_rule(¶ms.fill_rule);
6489
6490 self.pixmap
6491 .fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
6492 }
6493
6494 fn stroke_path(&mut self, path: &PsPath, params: &StrokeParams) {
6495 self.ensure_full_pixmap();
6496 let stroke = build_stroke(params, self.dpi);
6497 let adjusted;
6498 let draw_path =
6499 if params.stroke_adjust && stroke.width <= 2.0 && ctm_is_device_space(¶ms.ctm) {
6500 adjusted =
6501 stroke_adjust_path_viewport(path, stroke.width as f64, 1.0, 1.0, 0.0, 0.0);
6502 &adjusted
6503 } else {
6504 path
6505 };
6506 let Some(skia_path) = build_skia_path(draw_path) else {
6507 return;
6508 };
6509 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, self.no_aa);
6510 let transform = to_transform(¶ms.ctm);
6511
6512 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6513 let mut temp_mask = None;
6514 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6515 return; };
6517
6518 self.pixmap
6519 .stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
6520 }
6521
6522 fn clip_path(&mut self, path: &PsPath, params: &ClipParams) {
6523 self.ensure_full_pixmap();
6524 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6525
6526 if let Some(new_rect) = detect_rect(path, w, h) {
6528 match self.clip_region.take() {
6529 None => {
6530 self.clip_region = Some(ClipRegion::Rect(new_rect));
6531 }
6532 Some(ClipRegion::Rect(existing)) => {
6533 self.clip_region = Some(ClipRegion::Rect(existing.intersect(&new_rect)));
6535 }
6536 Some(ClipRegion::Mask(mut mask)) => {
6537 intersect_mask_with_rect(&mut mask, &new_rect, w, h);
6539 self.clip_region = Some(ClipRegion::Mask(mask));
6540 }
6541 }
6542 return;
6543 }
6544
6545 let fill_rule = to_fill_rule(¶ms.fill_rule);
6547 let path_hash = hash_clip_path(path, ¶ms.fill_rule);
6548 let prev_region = self.clip_region.take();
6549
6550 macro_rules! take_spare {
6552 ($self:expr, $w:expr, $h:expr) => {
6553 $self
6554 .spare_mask
6555 .take()
6556 .unwrap_or_else(|| Mask::new($w, $h).expect("Failed to create mask"))
6557 };
6558 }
6559
6560 let path_mask = if let Some(cached) = self.clip_mask_cache.get(&path_hash) {
6562 let mut mask = take_spare!(self, w, h);
6564 mask.data_mut().copy_from_slice(cached.data());
6565 mask
6566 } else {
6567 let Some(skia_path) = build_skia_path(path) else {
6568 self.clip_region = prev_region;
6569 return;
6570 };
6571 let transform = to_transform(¶ms.ctm);
6572 let mut mask = take_spare!(self, w, h);
6573 mask.data_mut().fill(0); mask.fill_path(&skia_path, fill_rule, false, transform);
6575 if !self.clip_mask_seen.insert(path_hash) {
6577 self.clip_mask_cache.insert(path_hash, mask.clone());
6579 }
6580 mask
6581 };
6582
6583 match prev_region {
6584 None => {
6585 self.clip_region = Some(ClipRegion::Mask(path_mask));
6586 }
6587 Some(ClipRegion::Rect(rect)) => {
6588 if rect.is_empty() {
6589 self.spare_mask = Some(path_mask); } else {
6591 let mut mask = path_mask;
6592 intersect_mask_with_rect(&mut mask, &rect, w, h);
6593 self.clip_region = Some(ClipRegion::Mask(mask));
6594 }
6595 }
6596 Some(ClipRegion::Mask(mut existing)) => {
6597 intersect_masks(&mut existing, &path_mask);
6598 self.spare_mask = Some(path_mask); self.clip_region = Some(ClipRegion::Mask(existing));
6600 }
6601 }
6602 }
6603
6604 fn init_clip(&mut self) {
6605 if let Some(ClipRegion::Mask(mask)) = self.clip_region.take() {
6606 self.spare_mask = Some(mask);
6607 }
6608 self.clip_region = None;
6609 }
6610
6611 fn erase_page(&mut self) {
6612 self.pixmap.fill(Color::WHITE);
6615 if let Some(ClipRegion::Mask(mask)) = self.clip_region.take() {
6616 self.spare_mask = Some(mask);
6617 }
6618 self.clip_region = None;
6619 }
6620
6621 fn show_page(&mut self, output_path: &str) -> Result<(), String> {
6622 let w = self.pixmap.width();
6623 let h = self.pixmap.height();
6624 composite_onto_white(self.pixmap.data_mut());
6626 let mut sink = self.sink_factory.create_sink(output_path)?;
6627 sink.begin_page(w, h)?;
6628 sink.write_rows(self.pixmap.data(), h)?;
6629 sink.end_page()
6630 }
6631
6632 fn draw_image(&mut self, sample_data: &[u8], params: &ImageParams) {
6633 self.ensure_full_pixmap();
6634 let w = params.width;
6635 let h = params.height;
6636 if w == 0 || h == 0 {
6637 return;
6638 }
6639 let mut rgba_data =
6640 samples_to_rgba(sample_data, params, self.render_icc_cache.as_ref(), false);
6641 if params.mask_color.is_some() {
6642 apply_mask_color_rgba(&mut rgba_data, sample_data, params);
6643 }
6644 let expected = (w * h * 4) as usize;
6645 if rgba_data.len() < expected {
6646 return;
6647 }
6648
6649 let Some(image_inv) = params.image_matrix.invert() else {
6650 return;
6651 };
6652 let combined = params.ctm.concat(&image_inv);
6653 let raw_transform = enforce_min_image_size(to_transform(&combined), w, h);
6654
6655 let prescaled = prescale_image(&rgba_data, w, h, raw_transform, params.interpolate);
6656 let (img_data, img_w, img_h, transform) = match &prescaled {
6657 Some((data, pw, ph, t)) => (data.as_slice(), *pw, *ph, *t),
6658 None => (rgba_data.as_slice(), w, h, raw_transform),
6659 };
6660
6661 let Some(img_pixmap) = stet_tiny_skia::PixmapRef::from_bytes(img_data, img_w, img_h) else {
6662 return;
6663 };
6664
6665 let (pw, ph) = (self.pixmap.width(), self.pixmap.height());
6666 let mut temp_mask = None;
6667 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, pw, ph) else {
6668 return;
6669 };
6670
6671 let paint = stet_tiny_skia::PixmapPaint {
6672 quality: image_filter_quality(transform, params.interpolate),
6673 opacity: params.alpha as f32,
6674 blend_mode: u8_to_blend_mode(params.blend_mode),
6675 };
6676 self.pixmap
6677 .draw_pixmap(0, 0, img_pixmap, &paint, transform, mask_ref);
6678 }
6679
6680 fn paint_axial_shading(&mut self, params: &AxialShadingParams) {
6681 self.ensure_full_pixmap();
6682 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6683 let mut temp_mask = None;
6684 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6685 return;
6686 };
6687 render_axial_shading(
6688 &mut self.pixmap,
6689 params,
6690 0.0,
6691 0.0,
6692 1.0,
6693 1.0,
6694 mask_ref,
6695 self.no_aa,
6696 None,
6697 None,
6698 );
6699 }
6700
6701 fn paint_radial_shading(&mut self, params: &RadialShadingParams) {
6702 self.ensure_full_pixmap();
6703 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6704 let mut temp_mask = None;
6705 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6706 return;
6707 };
6708 render_radial_shading(
6709 &mut self.pixmap,
6710 params,
6711 0.0,
6712 0.0,
6713 1.0,
6714 1.0,
6715 mask_ref,
6716 self.no_aa,
6717 None,
6718 None,
6719 );
6720 }
6721
6722 fn paint_mesh_shading(&mut self, params: &MeshShadingParams) {
6723 self.ensure_full_pixmap();
6724 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6725 let mut temp_mask = None;
6726 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6727 return;
6728 };
6729 render_mesh_shading(
6730 &mut self.pixmap,
6731 params,
6732 0.0,
6733 0.0,
6734 1.0,
6735 1.0,
6736 mask_ref,
6737 None,
6738 None,
6739 );
6740 }
6741
6742 fn paint_patch_shading(&mut self, params: &PatchShadingParams) {
6743 self.ensure_full_pixmap();
6744 let (w, h) = (self.pixmap.width(), self.pixmap.height());
6745 let mut temp_mask = None;
6746 let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
6747 return;
6748 };
6749 render_patch_shading(
6750 &mut self.pixmap,
6751 params,
6752 0.0,
6753 0.0,
6754 1.0,
6755 1.0,
6756 mask_ref,
6757 None,
6758 None,
6759 );
6760 }
6761
6762 fn paint_pattern_fill(&mut self, params: &stet_graphics::device::PatternFillParams) {
6763 self.ensure_full_pixmap();
6764 let w = self.pixmap.width();
6765 let h = self.pixmap.height();
6766 let mut band_state = BandState {
6767 clip_region: self.clip_region.take(),
6768 spare_mask: self.spare_mask.take(),
6769 clip_mask_cache: HashMap::new(),
6770 clip_mask_seen: HashSet::new(),
6771 mask_pool: Vec::new(),
6772 cmyk_buffer: None,
6773 op_bg_snapshot: None,
6774 op_touched: None,
6775 spot_mask: None,
6776 };
6777 {
6778 let ctx = RenderContext {
6779 vp_x: 0.0,
6780 vp_y: 0.0,
6781 scale_x: 1.0,
6782 scale_y: 1.0,
6783 out_w: w,
6784 out_h: h,
6785 effective_dpi: self.dpi,
6786 icc: None,
6787 image_cache: None,
6788 preprocessed: None,
6789 elem_idx: 0,
6790 no_aa: self.no_aa,
6791 opm_zero_transparent: false,
6792 knockout_painter_pass: KnockoutPainterPass::None,
6793 parent_group_isolated: false,
6794 alpha_extraction_pass: false,
6795 layer_set: &self.layer_set,
6796 };
6797 render_pattern_fill(&mut self.pixmap, &mut band_state, params, &ctx);
6798 }
6799 self.clip_region = band_state.clip_region.take();
6800 if let Some(mask) = band_state.spare_mask.take() {
6801 self.spare_mask = Some(mask);
6802 }
6803 }
6804
6805 fn page_size(&self) -> (u32, u32) {
6806 (self.page_w, self.page_h)
6807 }
6808
6809 fn replay_and_show(&mut self, list: DisplayList, output_path: &str) -> Result<(), String> {
6810 self.join_pending()?;
6812
6813 let (page_w, page_h) = self.page_size();
6814
6815 if self.use_viewport_path {
6820 let icc_cache = build_icc_cache_for_list(&list, self.system_cmyk_bytes.as_ref(), false);
6821 let rgba = render_to_rgba_viewport(
6822 &list,
6823 page_w,
6824 page_h,
6825 self.dpi,
6826 Some(&icc_cache),
6827 self.no_aa,
6828 );
6829 let mut sink = self.sink_factory.create_sink(output_path)?;
6830 sink.begin_page(page_w, page_h)?;
6831 sink.write_rows(&rgba, page_h)?;
6832 sink.end_page()?;
6833 return Ok(());
6834 }
6835
6836 let band_h = select_band_height(page_w, page_h);
6837
6838 let icc_cache = build_icc_cache_for_list(&list, self.system_cmyk_bytes.as_ref(), false);
6840
6841 if band_h >= page_h {
6845 self.ensure_full_pixmap();
6846 let ctx = RenderContext {
6847 vp_x: 0.0,
6848 vp_y: 0.0,
6849 scale_x: 1.0,
6850 scale_y: 1.0,
6851 out_w: page_w,
6852 out_h: page_h,
6853 effective_dpi: self.dpi,
6854 icc: Some(&icc_cache),
6855 image_cache: None,
6856 preprocessed: None,
6857 elem_idx: 0,
6858 no_aa: self.no_aa,
6859 opm_zero_transparent: false,
6860 knockout_painter_pass: KnockoutPainterPass::None,
6861 parent_group_isolated: false,
6862 alpha_extraction_pass: false,
6863 layer_set: &self.layer_set,
6864 };
6865 let mut band_state = BandState {
6866 clip_region: None,
6867 spare_mask: None,
6868 clip_mask_cache: HashMap::new(),
6869 clip_mask_seen: HashSet::new(),
6870 mask_pool: Vec::new(),
6871 cmyk_buffer: None,
6872 op_bg_snapshot: None,
6873 op_touched: None,
6874 spot_mask: None,
6875 };
6876 for (idx, elem) in list.elements().iter().enumerate() {
6877 let elem_ctx = RenderContext {
6878 elem_idx: idx,
6879 ..ctx
6880 };
6881 render_element(&mut self.pixmap, &mut band_state, elem, &elem_ctx);
6882 }
6883 return self.show_page(output_path);
6884 }
6885
6886 if self.pixmap.width() > 1 {
6890 self.pixmap = Pixmap::new(1, 1).expect("Failed to create placeholder pixmap");
6891 }
6892
6893 let mut sink = self.sink_factory.create_sink(output_path)?;
6895 let dpi = self.dpi;
6896 let layer_set = self.layer_set.clone();
6897
6898 #[cfg(feature = "parallel")]
6899 {
6900 let no_aa = self.no_aa;
6904 let (tx, rx) = std::sync::mpsc::sync_channel(1);
6905 rayon::spawn(move || {
6906 let result = render_banded_to_sink(
6907 page_w, page_h, band_h, dpi, &list, &mut *sink, &icc_cache, no_aa, &layer_set,
6908 );
6909 let _ = tx.send(result);
6910 });
6911 self.pending_render = Some(rx);
6912 }
6913 #[cfg(not(feature = "parallel"))]
6914 {
6915 render_banded_to_sink(
6916 page_w, page_h, band_h, dpi, &list, &mut *sink, &icc_cache, self.no_aa, &layer_set,
6917 )?;
6918 }
6919
6920 Ok(())
6921 }
6922
6923 fn finish(&mut self) -> Result<(), String> {
6924 self.join_pending()
6925 }
6926}
6927
6928#[cfg(feature = "ps-device")]
6929impl Drop for SkiaDevice {
6930 fn drop(&mut self) {
6931 if let Some(rx) = self.pending_render.take() {
6933 let _ = rx.recv();
6934 }
6935 }
6936}
6937
6938#[cfg(feature = "ps-device")]
6939impl SkiaDevice {
6940 fn join_pending(&mut self) -> Result<(), String> {
6942 if let Some(rx) = self.pending_render.take() {
6943 match rx.recv() {
6944 Ok(result) => result?,
6945 Err(_) => return Err("Background render task failed".to_string()),
6946 }
6947 }
6948 Ok(())
6949 }
6950}
6951
6952fn has_cmyk_group(list: &DisplayList) -> bool {
6957 use stet_graphics::display_list::GroupColorSpace;
6958 for elem in list.elements() {
6959 match elem {
6960 DisplayElement::Group { elements, params } => {
6961 if params.color_space == GroupColorSpace::DeviceCMYK {
6962 return true;
6963 }
6964 if has_cmyk_group(elements) {
6965 return true;
6966 }
6967 }
6968 DisplayElement::SoftMasked { content, mask, .. } => {
6969 if has_cmyk_group(content) || has_cmyk_group(mask) {
6970 return true;
6971 }
6972 }
6973 DisplayElement::OcgGroup { elements, .. } => {
6974 if has_cmyk_group(elements) {
6975 return true;
6976 }
6977 }
6978 _ => {}
6979 }
6980 }
6981 false
6982}
6983
6984fn group_only_native_cmyk_fills(elements: &DisplayList) -> bool {
6991 let mut found_paint = false;
6992 for elem in elements.elements() {
6993 match elem {
6994 DisplayElement::InitClip => continue,
6995 DisplayElement::Clip { .. } => continue,
6996 DisplayElement::Fill { params, .. } => {
6997 if params.color.native_cmyk.is_none() {
6998 return false;
6999 }
7000 found_paint = true;
7001 }
7002 DisplayElement::Stroke { params, .. } => {
7003 if params.color.native_cmyk.is_none() {
7009 return false;
7010 }
7011 found_paint = true;
7012 }
7013 _ => return false,
7014 }
7015 }
7016 found_paint
7017}
7018
7019fn group_content_is_native_cmyk(elements: &DisplayList) -> bool {
7040 let mut found_paint = false;
7041 for elem in elements.elements() {
7042 match elem {
7043 DisplayElement::InitClip => continue,
7044 DisplayElement::Clip { .. } => continue,
7045 DisplayElement::Text { .. } => continue,
7046 DisplayElement::ErasePage => continue,
7047 DisplayElement::Fill { params, .. } => {
7048 if params.color.native_cmyk.is_none() {
7049 return false;
7050 }
7051 found_paint = true;
7052 }
7053 DisplayElement::Stroke { params, .. } => {
7054 if params.color.native_cmyk.is_none() {
7055 return false;
7056 }
7057 found_paint = true;
7058 }
7059 DisplayElement::Image { params, .. } => {
7060 if !is_cmyk_color_space(¶ms.color_space) {
7061 return false;
7062 }
7063 found_paint = true;
7064 }
7065 DisplayElement::AxialShading { .. }
7066 | DisplayElement::RadialShading { .. }
7067 | DisplayElement::MeshShading { .. }
7068 | DisplayElement::PatchShading { .. } => {
7069 return false;
7072 }
7073 DisplayElement::PatternFill { .. } => {
7074 return false;
7078 }
7079 DisplayElement::Group { elements: sub, .. } => {
7080 if !group_content_is_native_cmyk(sub) {
7081 return false;
7082 }
7083 found_paint = true;
7084 }
7085 DisplayElement::SoftMasked { .. } => {
7086 return false;
7097 }
7098 DisplayElement::OcgGroup { elements: sub, .. } => {
7099 if !group_content_is_native_cmyk(sub) {
7100 return false;
7101 }
7102 found_paint = true;
7103 }
7104 _ => return false,
7105 }
7106 }
7107 found_paint
7108}
7109
7110fn content_list_is_simple_native_cmyk(list: &DisplayList) -> bool {
7117 let mut found_paint = false;
7118 for elem in list.elements() {
7119 match elem {
7120 DisplayElement::InitClip
7121 | DisplayElement::Clip { .. }
7122 | DisplayElement::Text { .. }
7123 | DisplayElement::ErasePage => continue,
7124 DisplayElement::Fill { params, .. } => {
7125 if params.color.native_cmyk.is_none() {
7126 return false;
7127 }
7128 if params.blend_mode != 0 || params.alpha != 1.0 {
7129 return false;
7130 }
7131 found_paint = true;
7132 }
7133 DisplayElement::Stroke { params, .. } => {
7134 if params.color.native_cmyk.is_none() {
7135 return false;
7136 }
7137 if params.blend_mode != 0 || params.alpha != 1.0 {
7138 return false;
7139 }
7140 found_paint = true;
7141 }
7142 DisplayElement::Group { params, elements } => {
7150 if params.blend_mode != 0 || params.alpha != 1.0 {
7151 return false;
7152 }
7153 if !content_list_is_simple_native_cmyk(elements) {
7154 return false;
7155 }
7156 if elements.elements().iter().any(|e| {
7160 matches!(
7161 e,
7162 DisplayElement::Fill { .. } | DisplayElement::Stroke { .. }
7163 )
7164 }) {
7165 found_paint = true;
7166 }
7167 }
7168 _ => return false,
7169 }
7170 }
7171 found_paint
7172}
7173
7174fn has_overprint_elements(list: &DisplayList) -> bool {
7176 for elem in list.elements() {
7177 match elem {
7178 DisplayElement::Fill { params, .. } => {
7179 if params.overprint {
7180 return true;
7181 }
7182 }
7183 DisplayElement::Stroke { params, .. } => {
7184 if params.overprint {
7185 return true;
7186 }
7187 }
7188 DisplayElement::Image { params, .. } => {
7189 if params.overprint {
7190 return true;
7191 }
7192 }
7193 DisplayElement::AxialShading { params } => {
7194 if params.overprint {
7195 return true;
7196 }
7197 }
7198 DisplayElement::RadialShading { params } => {
7199 if params.overprint {
7200 return true;
7201 }
7202 }
7203 DisplayElement::MeshShading { params } => {
7204 if params.overprint {
7205 return true;
7206 }
7207 }
7208 DisplayElement::PatchShading { params } => {
7209 if params.overprint {
7210 return true;
7211 }
7212 }
7213 DisplayElement::Group { elements, .. } => {
7214 if has_overprint_elements(elements) {
7215 return true;
7216 }
7217 }
7218 DisplayElement::SoftMasked { content, mask, .. } => {
7219 if has_overprint_elements(content) || has_overprint_elements(mask) {
7220 return true;
7221 }
7222 }
7223 DisplayElement::OcgGroup { elements, .. } => {
7224 if has_overprint_elements(elements) {
7225 return true;
7226 }
7227 }
7228 _ => {}
7229 }
7230 }
7231 false
7232}
7233
7234#[allow(clippy::too_many_arguments)]
7237fn render_overprint_fill(
7238 pixmap: &mut Pixmap,
7239 cmyk_buf: &mut [f32],
7240 op_bg: &mut [u8],
7241 op_touched: &mut [u8],
7242 spot_mask: &[u8],
7243 band_state: &mut BandState,
7244 path: &PsPath,
7245 params: &FillParams,
7246 vp_x: f32,
7247 vp_y: f32,
7248 scale_x: f32,
7249 scale_y: f32,
7250 out_w: u32,
7251 out_h: u32,
7252 icc: Option<&IccCache>,
7253 no_aa: bool,
7254) {
7255 let Some(skia_path) = build_skia_path(path) else {
7256 return;
7257 };
7258 let fill_rule = to_fill_rule(¶ms.fill_rule);
7259
7260 let mut coverage_mask = match Mask::new(out_w, out_h) {
7261 Some(m) => m,
7262 None => return,
7263 };
7264 let transform = viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
7265 coverage_mask.fill_path(&skia_path, fill_rule, !no_aa, transform);
7266
7267 let (bbox_x0, bbox_y0, bbox_x1, bbox_y1) =
7269 path_device_bbox(&skia_path, transform, out_w, out_h);
7270
7271 let clip_coverage: Option<&[u8]> = match &band_state.clip_region {
7273 None => None,
7274 Some(ClipRegion::Rect(r)) => {
7275 let data = coverage_mask.data_mut();
7277 let stride = out_w as usize;
7278 for y in bbox_y0..bbox_y1 {
7279 let row_start = y * stride;
7280 for x in bbox_x0..bbox_x1 {
7281 let yu = y as u32;
7282 let xu = x as u32;
7283 if yu < r.y0 || yu >= r.y1 || xu < r.x0 || xu >= r.x1 {
7284 data[row_start + x] = 0;
7285 }
7286 }
7287 }
7288 None
7289 }
7290 Some(ClipRegion::Mask(clip_mask)) => Some(clip_mask.data()),
7291 };
7292
7293 let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
7299
7300 let (src_c, src_m, src_y, src_k) = if !is_custom_spot && let Some(c) = params.color.process_cmyk
7316 {
7317 c
7318 } else if let Some(c) = params.color.native_cmyk {
7319 c
7320 } else {
7321 let r = params.color.r;
7322 let g = params.color.g;
7323 let b = params.color.b;
7324 (1.0 - r, 1.0 - g, 1.0 - b, 0.0)
7325 };
7326
7327 let mut channels = params.painted_channels;
7328 if channels == 0 {
7331 channels = stet_graphics::device::CMYK_ALL;
7332 }
7333 if params.overprint_mode == 1
7335 && channels == stet_graphics::device::CMYK_ALL
7336 && params.is_device_cmyk
7337 {
7338 channels = 0;
7339 if src_c != 0.0 {
7340 channels |= stet_graphics::device::CMYK_C;
7341 }
7342 if src_m != 0.0 {
7343 channels |= stet_graphics::device::CMYK_M;
7344 }
7345 if src_y != 0.0 {
7346 channels |= stet_graphics::device::CMYK_Y;
7347 }
7348 if src_k != 0.0 {
7349 channels |= stet_graphics::device::CMYK_K;
7350 }
7351 if channels == 0 && !params.opm_paired {
7362 channels = stet_graphics::device::CMYK_ALL;
7363 }
7364 }
7365
7366 let is_k_only_cmyk = params.is_device_cmyk
7374 && params.overprint_mode == 0
7375 && src_c == 0.0
7376 && src_m == 0.0
7377 && src_y == 0.0;
7378 if channels == stet_graphics::device::CMYK_ALL && !is_custom_spot && !is_k_only_cmyk {
7379 let cov_data = coverage_mask.data();
7380 let stride = out_w as usize;
7381 for y in bbox_y0..bbox_y1 {
7382 for x in bbox_x0..bbox_x1 {
7383 let mi = y * stride + x;
7384 let mut cov = cov_data[mi] as f32 / 255.0;
7385 if let Some(clip) = clip_coverage {
7386 cov *= clip[mi] as f32 / 255.0;
7387 }
7388 if cov > 0.0 {
7389 let ci = mi * 4;
7390 cmyk_buf[ci] = src_c as f32;
7391 cmyk_buf[ci + 1] = src_m as f32;
7392 cmyk_buf[ci + 2] = src_y as f32;
7393 cmyk_buf[ci + 3] = src_k as f32;
7394 }
7395 }
7396 }
7397 let mut temp_mask = None;
7398 let Some(mask_ref) =
7399 resolve_clip_mask(&band_state.clip_region, &mut temp_mask, out_w, out_h)
7400 else {
7401 return;
7402 };
7403 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, no_aa);
7404 pixmap.fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
7405 return;
7406 }
7407
7408 let cov_data = coverage_mask.data();
7409 let stride = out_w as usize;
7410 let px_data = pixmap.data_mut();
7411 let px_stride = out_w as usize * 4;
7412
7413 for y in bbox_y0..bbox_y1 {
7414 for x in bbox_x0..bbox_x1 {
7415 let mi = y * stride + x;
7416 let mut cov = cov_data[mi] as f32 / 255.0;
7417 if let Some(clip) = clip_coverage {
7418 cov *= clip[mi] as f32 / 255.0;
7419 }
7420 if cov <= 0.0 {
7421 continue;
7422 }
7423
7424 let ci = mi * 4;
7425 let pi = y * px_stride + x * 4;
7426 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
7433 op_bg[pi] = px_data[pi];
7434 op_bg[pi + 1] = px_data[pi + 1];
7435 op_bg[pi + 2] = px_data[pi + 2];
7436 op_bg[pi + 3] = px_data[pi + 3];
7437 op_touched[mi] = 1;
7438 }
7439 let cur_c = cmyk_buf[ci] as f64;
7440 let cur_m = cmyk_buf[ci + 1] as f64;
7441 let cur_y = cmyk_buf[ci + 2] as f64;
7442 let cur_k = cmyk_buf[ci + 3] as f64;
7443 let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
7455 let pixmap_has_colour = px_data[pi + 3] > 0
7456 && (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
7457 let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
7465
7466 let is_promoted_gray = params.painted_channels == stet_graphics::device::CMYK_K
7482 && channels == stet_graphics::device::CMYK_K
7483 && params.is_device_cmyk
7484 && src_c == 0.0
7485 && src_m == 0.0
7486 && src_y == 0.0;
7487 let effective_channels = if is_promoted_gray && spot_mask[mi] == 0 {
7488 stet_graphics::device::CMYK_ALL
7489 } else {
7490 channels
7491 };
7492
7493 let new_c = if effective_channels & stet_graphics::device::CMYK_C != 0 {
7494 src_c
7495 } else {
7496 cur_c
7497 };
7498 let new_m = if effective_channels & stet_graphics::device::CMYK_M != 0 {
7499 src_m
7500 } else {
7501 cur_m
7502 };
7503 let new_y = if effective_channels & stet_graphics::device::CMYK_Y != 0 {
7504 src_y
7505 } else {
7506 cur_y
7507 };
7508 let new_k = if effective_channels & stet_graphics::device::CMYK_K != 0 {
7509 src_k
7510 } else {
7511 cur_k
7512 };
7513
7514 if !is_custom_spot {
7518 cmyk_buf[ci] = new_c as f32;
7519 cmyk_buf[ci + 1] = new_m as f32;
7520 cmyk_buf[ci + 2] = new_y as f32;
7521 cmyk_buf[ci + 3] = new_k as f32;
7522 }
7523
7524 let delta = (new_c - cur_c)
7538 .abs()
7539 .max((new_m - cur_m).abs())
7540 .max((new_y - cur_y).abs())
7541 .max((new_k - cur_k).abs());
7542 if delta < 1e-4 && spot_mask[mi] != 0 && pixmap_has_colour && !is_custom_spot {
7543 continue;
7544 }
7545
7546 let (r, g, b) =
7547 if is_promoted_gray && effective_channels == stet_graphics::device::CMYK_ALL {
7548 (params.color.r, params.color.g, params.color.b)
7563 } else if use_multiplicative {
7564 let bg_r = px_data[pi] as f64 / 255.0;
7570 let bg_g = px_data[pi + 1] as f64 / 255.0;
7571 let bg_b = px_data[pi + 2] as f64 / 255.0;
7572 let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
7573 1.0 - src_c
7574 } else {
7575 1.0
7576 };
7577 let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
7578 1.0 - src_m
7579 } else {
7580 1.0
7581 };
7582 let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
7583 1.0 - src_y
7584 } else {
7585 1.0
7586 };
7587 let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
7588 1.0 - src_k
7589 } else {
7590 1.0
7591 };
7592 (
7593 (bg_r * over_r * k_fac).clamp(0.0, 1.0),
7594 (bg_g * over_g * k_fac).clamp(0.0, 1.0),
7595 (bg_b * over_b * k_fac).clamp(0.0, 1.0),
7596 )
7597 } else if let Some(icc_cache) = icc {
7598 icc_cache
7599 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
7600 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
7601 } else {
7602 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
7603 };
7604
7605 let a = (cov * params.alpha as f32).min(1.0);
7606 let (bk_r, bk_g, bk_b, bk_a) = if op_touched[mi] != 0 {
7619 let new_r = (r as f32 * 255.0).clamp(0.0, 255.0);
7620 let new_g = (g as f32 * 255.0).clamp(0.0, 255.0);
7621 let new_b = (b as f32 * 255.0).clamp(0.0, 255.0);
7622 let dr = (op_bg[pi] as f32 - new_r).abs();
7623 let dg = (op_bg[pi + 1] as f32 - new_g).abs();
7624 let db = (op_bg[pi + 2] as f32 - new_b).abs();
7625 if dr.max(dg).max(db) <= 4.0 {
7626 (op_bg[pi], op_bg[pi + 1], op_bg[pi + 2], op_bg[pi + 3])
7627 } else {
7628 (
7629 px_data[pi],
7630 px_data[pi + 1],
7631 px_data[pi + 2],
7632 px_data[pi + 3],
7633 )
7634 }
7635 } else {
7636 (
7637 px_data[pi],
7638 px_data[pi + 1],
7639 px_data[pi + 2],
7640 px_data[pi + 3],
7641 )
7642 };
7643 let dst_a = bk_a as f32 / 255.0;
7644 let one_minus_a = 1.0 - a;
7645 let out_a = a + dst_a * one_minus_a;
7646 if out_a > 0.0 {
7647 px_data[pi] = ((r as f32 * a + (bk_r as f32 / 255.0) * one_minus_a) * 255.0)
7652 .clamp(0.0, 255.0)
7653 .round() as u8;
7654 px_data[pi + 1] = ((g as f32 * a + (bk_g as f32 / 255.0) * one_minus_a) * 255.0)
7655 .clamp(0.0, 255.0)
7656 .round() as u8;
7657 px_data[pi + 2] = ((b as f32 * a + (bk_b as f32 / 255.0) * one_minus_a) * 255.0)
7658 .clamp(0.0, 255.0)
7659 .round() as u8;
7660 px_data[pi + 3] = (out_a * 255.0).round() as u8;
7661 }
7662 }
7663 }
7664}
7665fn cmyk_to_rgb_plrm(c: f64, m: f64, y: f64, k: f64) -> (f64, f64, f64) {
7667 (
7668 1.0 - (c + k).min(1.0),
7669 1.0 - (m + k).min(1.0),
7670 1.0 - (y + k).min(1.0),
7671 )
7672}
7673
7674#[allow(clippy::too_many_arguments)]
7676fn path_device_bbox(
7679 skia_path: &stet_tiny_skia::Path,
7680 transform: Transform,
7681 w: u32,
7682 h: u32,
7683) -> (usize, usize, usize, usize) {
7684 let b = skia_path.bounds();
7685 let mut corners = [
7686 stet_tiny_skia::Point {
7687 x: b.left(),
7688 y: b.top(),
7689 },
7690 stet_tiny_skia::Point {
7691 x: b.right(),
7692 y: b.top(),
7693 },
7694 stet_tiny_skia::Point {
7695 x: b.right(),
7696 y: b.bottom(),
7697 },
7698 stet_tiny_skia::Point {
7699 x: b.left(),
7700 y: b.bottom(),
7701 },
7702 ];
7703 transform.map_points(&mut corners);
7704 let min_x = corners.iter().map(|p| p.x).fold(f32::INFINITY, f32::min);
7705 let min_y = corners.iter().map(|p| p.y).fold(f32::INFINITY, f32::min);
7706 let max_x = corners
7707 .iter()
7708 .map(|p| p.x)
7709 .fold(f32::NEG_INFINITY, f32::max);
7710 let max_y = corners
7711 .iter()
7712 .map(|p| p.y)
7713 .fold(f32::NEG_INFINITY, f32::max);
7714 let x0 = (min_x.floor() as i32 - 1).max(0) as usize;
7716 let y0 = (min_y.floor() as i32 - 1).max(0) as usize;
7717 let x1 = (max_x.ceil() as i32 + 1).clamp(0, w as i32) as usize;
7718 let y1 = (max_y.ceil() as i32 + 1).clamp(0, h as i32) as usize;
7719 (x0, y0, x1, y1)
7720}
7721
7722fn update_cmyk_buffer_for_fill(
7723 cmyk_buf: &mut [f32],
7724 spot_mask: &mut [u8],
7725 path: &PsPath,
7726 params: &FillParams,
7727 vp_x: f32,
7728 vp_y: f32,
7729 scale_x: f32,
7730 scale_y: f32,
7731 out_w: u32,
7732 out_h: u32,
7733 clip_region: &Option<ClipRegion>,
7734 no_aa: bool,
7735 icc: Option<&IccCache>,
7736) {
7737 let is_custom_spot = params.painted_channels == 0
7757 && !params.is_device_cmyk
7758 && params.color.process_cmyk.is_some();
7759
7760 let has_spot_contrib = (is_custom_spot && params.color.native_cmyk.is_some())
7774 || matches!(
7775 (params.color.native_cmyk, params.color.process_cmyk),
7776 (Some(nat), Some(proc_))
7777 if (nat.0 - proc_.0).abs() > 1e-6
7778 || (nat.1 - proc_.1).abs() > 1e-6
7779 || (nat.2 - proc_.2).abs() > 1e-6
7780 || (nat.3 - proc_.3).abs() > 1e-6
7781 );
7782
7783 let (src_c, src_m, src_y, src_k) = if is_custom_spot {
7791 (0.0, 0.0, 0.0, 0.0)
7792 } else if let Some(c) = params.color.process_cmyk {
7793 c
7794 } else if let Some(c) = params.color.native_cmyk {
7795 c
7796 } else if let Some(cmyk) = icc.and_then(|i| {
7797 i.convert_rgb_to_cmyk_readonly(params.color.r, params.color.g, params.color.b)
7798 }) {
7799 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
7800 } else {
7801 (
7802 (1.0 - params.color.r).clamp(0.0, 1.0),
7803 (1.0 - params.color.g).clamp(0.0, 1.0),
7804 (1.0 - params.color.b).clamp(0.0, 1.0),
7805 0.0,
7806 )
7807 };
7808 let Some(skia_path) = build_skia_path(path) else {
7809 return;
7810 };
7811
7812 let mut coverage_mask = match Mask::new(out_w, out_h) {
7813 Some(m) => m,
7814 None => return,
7815 };
7816 let transform = viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
7817 let fill_rule = to_fill_rule(¶ms.fill_rule);
7818 coverage_mask.fill_path(&skia_path, fill_rule, !no_aa, transform);
7819
7820 let cov_data = coverage_mask.data();
7821 let clip_data: Option<&[u8]> = match clip_region {
7822 Some(ClipRegion::Mask(m)) => Some(m.data()),
7823 _ => None,
7824 };
7825
7826 let (mut bx0, mut by0, mut bx1, mut by1) =
7828 path_device_bbox(&skia_path, transform, out_w, out_h);
7829 if let Some(ClipRegion::Rect(r)) = clip_region {
7830 bx0 = bx0.max(r.x0 as usize);
7831 by0 = by0.max(r.y0 as usize);
7832 bx1 = bx1.min(r.x1 as usize);
7833 by1 = by1.min(r.y1 as usize);
7834 }
7835
7836 let stride = out_w as usize;
7837 for y in by0..by1 {
7838 for x in bx0..bx1 {
7839 let mi = y * stride + x;
7840 let mut cov = cov_data[mi] as f32 / 255.0;
7841 if let Some(clip) = clip_data {
7842 cov *= clip[mi] as f32 / 255.0;
7843 }
7844 if cov > 0.0 {
7845 let ci = mi * 4;
7846 cmyk_buf[ci] = src_c as f32;
7847 cmyk_buf[ci + 1] = src_m as f32;
7848 cmyk_buf[ci + 2] = src_y as f32;
7849 cmyk_buf[ci + 3] = src_k as f32;
7850 if has_spot_contrib {
7851 spot_mask[mi] = 1;
7852 }
7853 }
7854 }
7855 }
7856}
7857
7858#[allow(clippy::too_many_arguments)]
7863fn render_overprint_stroke(
7864 pixmap: &mut Pixmap,
7865 cmyk_buf: &mut [f32],
7866 op_bg: &mut [u8],
7867 op_touched: &mut [u8],
7868 spot_mask: &[u8],
7869 band_state: &mut BandState,
7870 skia_path: &stet_tiny_skia::Path,
7871 stroke: &Stroke,
7872 transform: Transform,
7873 params: &StrokeParams,
7874 out_w: u32,
7875 out_h: u32,
7876 icc: Option<&IccCache>,
7877 no_aa: bool,
7878) {
7879 let resolution_scale = (transform.sx * transform.sx + transform.sy * transform.sy)
7881 .sqrt()
7882 .max(1.0);
7883 let dashed_op;
7884 let stroke_src = if let Some(ref dash) = stroke.dash {
7885 dashed_op = skia_path.dash(dash, resolution_scale);
7886 match dashed_op.as_ref() {
7887 Some(p) => p,
7888 None => skia_path,
7889 }
7890 } else {
7891 skia_path
7892 };
7893 let Some(stroked_user) = stroke_src.stroke(stroke, resolution_scale) else {
7894 return;
7895 };
7896 let Some(stroked) = stroked_user.transform(transform) else {
7897 return;
7898 };
7899
7900 let mut coverage_mask = match Mask::new(out_w, out_h) {
7901 Some(m) => m,
7902 None => return,
7903 };
7904 coverage_mask.fill_path(
7905 &stroked,
7906 SkiaFillRule::Winding,
7907 !no_aa,
7908 Transform::identity(),
7909 );
7910
7911 let (bbox_x0, bbox_y0, bbox_x1, bbox_y1) =
7912 path_device_bbox(&stroked, Transform::identity(), out_w, out_h);
7913
7914 let clip_coverage: Option<&[u8]> = match &band_state.clip_region {
7916 None => None,
7917 Some(ClipRegion::Rect(r)) => {
7918 let data = coverage_mask.data_mut();
7919 let stride = out_w as usize;
7920 for y in bbox_y0..bbox_y1 {
7921 let row_start = y * stride;
7922 for x in bbox_x0..bbox_x1 {
7923 let yu = y as u32;
7924 let xu = x as u32;
7925 if yu < r.y0 || yu >= r.y1 || xu < r.x0 || xu >= r.x1 {
7926 data[row_start + x] = 0;
7927 }
7928 }
7929 }
7930 None
7931 }
7932 Some(ClipRegion::Mask(clip_mask)) => Some(clip_mask.data()),
7933 };
7934
7935 let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
7939
7940 let (src_c, src_m, src_y, src_k) = if !is_custom_spot && let Some(c) = params.color.process_cmyk
7947 {
7948 c
7949 } else if let Some(c) = params.color.native_cmyk {
7950 c
7951 } else {
7952 let r = params.color.r;
7953 let g = params.color.g;
7954 let b = params.color.b;
7955 (1.0 - r, 1.0 - g, 1.0 - b, 0.0)
7956 };
7957
7958 let mut channels = params.painted_channels;
7959 if channels == 0 {
7960 channels = stet_graphics::device::CMYK_ALL;
7961 }
7962 if params.overprint_mode == 1
7963 && channels == stet_graphics::device::CMYK_ALL
7964 && params.is_device_cmyk
7965 {
7966 channels = 0;
7967 if src_c != 0.0 {
7968 channels |= stet_graphics::device::CMYK_C;
7969 }
7970 if src_m != 0.0 {
7971 channels |= stet_graphics::device::CMYK_M;
7972 }
7973 if src_y != 0.0 {
7974 channels |= stet_graphics::device::CMYK_Y;
7975 }
7976 if src_k != 0.0 {
7977 channels |= stet_graphics::device::CMYK_K;
7978 }
7979 if channels == 0 && !params.opm_paired {
7984 channels = stet_graphics::device::CMYK_ALL;
7985 }
7986 }
7987
7988 let is_k_only_cmyk = params.is_device_cmyk
7989 && params.overprint_mode == 0
7990 && src_c == 0.0
7991 && src_m == 0.0
7992 && src_y == 0.0;
7993 if channels == stet_graphics::device::CMYK_ALL && !is_custom_spot && !is_k_only_cmyk {
7994 let cov_data = coverage_mask.data();
7999 let stride = out_w as usize;
8000 for y in bbox_y0..bbox_y1 {
8001 for x in bbox_x0..bbox_x1 {
8002 let mi = y * stride + x;
8003 let mut cov = cov_data[mi] as f32 / 255.0;
8004 if let Some(clip) = clip_coverage {
8005 cov *= clip[mi] as f32 / 255.0;
8006 }
8007 if cov > 0.0 {
8008 let ci = mi * 4;
8009 cmyk_buf[ci] = src_c as f32;
8010 cmyk_buf[ci + 1] = src_m as f32;
8011 cmyk_buf[ci + 2] = src_y as f32;
8012 cmyk_buf[ci + 3] = src_k as f32;
8013 }
8014 }
8015 }
8016 let mut temp_mask = None;
8017 let Some(mask_ref) =
8018 resolve_clip_mask(&band_state.clip_region, &mut temp_mask, out_w, out_h)
8019 else {
8020 return;
8021 };
8022 let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, no_aa);
8023 pixmap.stroke_path(skia_path, &paint, stroke, transform, mask_ref);
8024 return;
8025 }
8026
8027 let cov_data = coverage_mask.data();
8028 let stride = out_w as usize;
8029 let px_data = pixmap.data_mut();
8030 let px_stride = out_w as usize * 4;
8031
8032 for y in bbox_y0..bbox_y1 {
8033 for x in bbox_x0..bbox_x1 {
8034 let mi = y * stride + x;
8035 let mut cov = cov_data[mi] as f32 / 255.0;
8036 if let Some(clip) = clip_coverage {
8037 cov *= clip[mi] as f32 / 255.0;
8038 }
8039 if cov <= 0.0 {
8040 continue;
8041 }
8042
8043 let ci = mi * 4;
8044 let pi = y * px_stride + x * 4;
8045 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
8050 op_bg[pi] = px_data[pi];
8051 op_bg[pi + 1] = px_data[pi + 1];
8052 op_bg[pi + 2] = px_data[pi + 2];
8053 op_bg[pi + 3] = px_data[pi + 3];
8054 op_touched[mi] = 1;
8055 }
8056 let cur_c = cmyk_buf[ci] as f64;
8057 let cur_m = cmyk_buf[ci + 1] as f64;
8058 let cur_y = cmyk_buf[ci + 2] as f64;
8059 let cur_k = cmyk_buf[ci + 3] as f64;
8060 let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
8061 let pixmap_has_colour = px_data[pi + 3] > 0
8062 && (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
8063 let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
8071
8072 let is_promoted_gray = params.painted_channels == stet_graphics::device::CMYK_K
8075 && channels == stet_graphics::device::CMYK_K
8076 && params.is_device_cmyk
8077 && src_c == 0.0
8078 && src_m == 0.0
8079 && src_y == 0.0;
8080 let effective_channels = if is_promoted_gray && spot_mask[mi] == 0 {
8081 stet_graphics::device::CMYK_ALL
8082 } else {
8083 channels
8084 };
8085
8086 let new_c = if effective_channels & stet_graphics::device::CMYK_C != 0 {
8087 src_c
8088 } else {
8089 cur_c
8090 };
8091 let new_m = if effective_channels & stet_graphics::device::CMYK_M != 0 {
8092 src_m
8093 } else {
8094 cur_m
8095 };
8096 let new_y = if effective_channels & stet_graphics::device::CMYK_Y != 0 {
8097 src_y
8098 } else {
8099 cur_y
8100 };
8101 let new_k = if effective_channels & stet_graphics::device::CMYK_K != 0 {
8102 src_k
8103 } else {
8104 cur_k
8105 };
8106
8107 if !is_custom_spot {
8108 cmyk_buf[ci] = new_c as f32;
8109 cmyk_buf[ci + 1] = new_m as f32;
8110 cmyk_buf[ci + 2] = new_y as f32;
8111 cmyk_buf[ci + 3] = new_k as f32;
8112 }
8113
8114 let delta = (new_c - cur_c)
8116 .abs()
8117 .max((new_m - cur_m).abs())
8118 .max((new_y - cur_y).abs())
8119 .max((new_k - cur_k).abs());
8120 if delta < 1e-4 && spot_mask[mi] != 0 && pixmap_has_colour && !is_custom_spot {
8121 continue;
8122 }
8123
8124 let (r, g, b) =
8125 if is_promoted_gray && effective_channels == stet_graphics::device::CMYK_ALL {
8126 (params.color.r, params.color.g, params.color.b)
8132 } else if use_multiplicative {
8133 let bg_r = px_data[pi] as f64 / 255.0;
8134 let bg_g = px_data[pi + 1] as f64 / 255.0;
8135 let bg_b = px_data[pi + 2] as f64 / 255.0;
8136 let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
8137 1.0 - src_c
8138 } else {
8139 1.0
8140 };
8141 let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
8142 1.0 - src_m
8143 } else {
8144 1.0
8145 };
8146 let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
8147 1.0 - src_y
8148 } else {
8149 1.0
8150 };
8151 let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
8152 1.0 - src_k
8153 } else {
8154 1.0
8155 };
8156 (
8157 (bg_r * over_r * k_fac).clamp(0.0, 1.0),
8158 (bg_g * over_g * k_fac).clamp(0.0, 1.0),
8159 (bg_b * over_b * k_fac).clamp(0.0, 1.0),
8160 )
8161 } else if let Some(icc_cache) = icc {
8162 icc_cache
8163 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
8164 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
8165 } else {
8166 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
8167 };
8168
8169 let a = (cov * params.alpha as f32).min(1.0);
8170 let (bk_r, bk_g, bk_b, bk_a) = if op_touched[mi] != 0 {
8175 let new_r = (r as f32 * 255.0).clamp(0.0, 255.0);
8176 let new_g = (g as f32 * 255.0).clamp(0.0, 255.0);
8177 let new_b = (b as f32 * 255.0).clamp(0.0, 255.0);
8178 let dr = (op_bg[pi] as f32 - new_r).abs();
8179 let dg = (op_bg[pi + 1] as f32 - new_g).abs();
8180 let db = (op_bg[pi + 2] as f32 - new_b).abs();
8181 if dr.max(dg).max(db) <= 4.0 {
8182 (op_bg[pi], op_bg[pi + 1], op_bg[pi + 2], op_bg[pi + 3])
8183 } else {
8184 (
8185 px_data[pi],
8186 px_data[pi + 1],
8187 px_data[pi + 2],
8188 px_data[pi + 3],
8189 )
8190 }
8191 } else {
8192 (
8193 px_data[pi],
8194 px_data[pi + 1],
8195 px_data[pi + 2],
8196 px_data[pi + 3],
8197 )
8198 };
8199 let dst_a = bk_a as f32 / 255.0;
8200 let one_minus_a = 1.0 - a;
8201 let out_a = a + dst_a * one_minus_a;
8202 if out_a > 0.0 {
8203 px_data[pi] = ((r as f32 * a + (bk_r as f32 / 255.0) * one_minus_a) * 255.0)
8205 .clamp(0.0, 255.0)
8206 .round() as u8;
8207 px_data[pi + 1] = ((g as f32 * a + (bk_g as f32 / 255.0) * one_minus_a) * 255.0)
8208 .clamp(0.0, 255.0)
8209 .round() as u8;
8210 px_data[pi + 2] = ((b as f32 * a + (bk_b as f32 / 255.0) * one_minus_a) * 255.0)
8211 .clamp(0.0, 255.0)
8212 .round() as u8;
8213 px_data[pi + 3] = (out_a * 255.0).round() as u8;
8214 }
8215 }
8216 }
8217}
8218
8219#[allow(clippy::too_many_arguments)]
8224fn update_cmyk_buffer_for_stroke(
8225 cmyk_buf: &mut [f32],
8226 spot_mask: &mut [u8],
8227 path: &PsPath,
8228 params: &StrokeParams,
8229 stroke: &Stroke,
8230 transform: Transform,
8231 out_w: u32,
8232 out_h: u32,
8233 clip_region: &Option<ClipRegion>,
8234 no_aa: bool,
8235 icc: Option<&IccCache>,
8236) {
8237 let is_custom_spot = params.painted_channels == 0
8244 && !params.is_device_cmyk
8245 && params.color.process_cmyk.is_some();
8246 let has_spot_contrib = (is_custom_spot && params.color.native_cmyk.is_some())
8248 || matches!(
8249 (params.color.native_cmyk, params.color.process_cmyk),
8250 (Some(nat), Some(proc_))
8251 if (nat.0 - proc_.0).abs() > 1e-6
8252 || (nat.1 - proc_.1).abs() > 1e-6
8253 || (nat.2 - proc_.2).abs() > 1e-6
8254 || (nat.3 - proc_.3).abs() > 1e-6
8255 );
8256
8257 let (src_c, src_m, src_y, src_k) = if is_custom_spot {
8258 (0.0, 0.0, 0.0, 0.0)
8259 } else if let Some(c) = params.color.process_cmyk {
8260 c
8261 } else if let Some(c) = params.color.native_cmyk {
8262 c
8263 } else if let Some(cmyk) = icc.and_then(|i| {
8264 i.convert_rgb_to_cmyk_readonly(params.color.r, params.color.g, params.color.b)
8265 }) {
8266 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
8267 } else {
8268 (
8269 (1.0 - params.color.r).clamp(0.0, 1.0),
8270 (1.0 - params.color.g).clamp(0.0, 1.0),
8271 (1.0 - params.color.b).clamp(0.0, 1.0),
8272 0.0,
8273 )
8274 };
8275
8276 let Some(skia_path) = build_skia_path(path) else {
8277 return;
8278 };
8279
8280 let resolution_scale = (transform.sx * transform.sx + transform.sy * transform.sy)
8284 .sqrt()
8285 .max(1.0);
8286 let dashed_op;
8287 let stroke_src = if let Some(ref dash) = stroke.dash {
8288 dashed_op = skia_path.dash(dash, resolution_scale);
8289 match dashed_op.as_ref() {
8290 Some(p) => p,
8291 None => &skia_path,
8292 }
8293 } else {
8294 &skia_path
8295 };
8296 let Some(stroked_user) = stroke_src.stroke(stroke, resolution_scale) else {
8297 return;
8298 };
8299 let Some(stroked) = stroked_user.transform(transform) else {
8300 return;
8301 };
8302
8303 let mut coverage_mask = match Mask::new(out_w, out_h) {
8304 Some(m) => m,
8305 None => return,
8306 };
8307 coverage_mask.fill_path(
8308 &stroked,
8309 SkiaFillRule::Winding,
8310 !no_aa,
8311 Transform::identity(),
8312 );
8313
8314 let cov_data = coverage_mask.data();
8315 let clip_data: Option<&[u8]> = match clip_region {
8316 Some(ClipRegion::Mask(m)) => Some(m.data()),
8317 _ => None,
8318 };
8319
8320 let (mut bx0, mut by0, mut bx1, mut by1) =
8321 path_device_bbox(&stroked, Transform::identity(), out_w, out_h);
8322 if let Some(ClipRegion::Rect(r)) = clip_region {
8323 bx0 = bx0.max(r.x0 as usize);
8324 by0 = by0.max(r.y0 as usize);
8325 bx1 = bx1.min(r.x1 as usize);
8326 by1 = by1.min(r.y1 as usize);
8327 }
8328
8329 let stride = out_w as usize;
8330 for y in by0..by1 {
8331 for x in bx0..bx1 {
8332 let mi = y * stride + x;
8333 let mut cov = cov_data[mi] as f32 / 255.0;
8334 if let Some(clip) = clip_data {
8335 cov *= clip[mi] as f32 / 255.0;
8336 }
8337 if cov > 0.0 {
8338 let ci = mi * 4;
8339 cmyk_buf[ci] = src_c as f32;
8340 cmyk_buf[ci + 1] = src_m as f32;
8341 cmyk_buf[ci + 2] = src_y as f32;
8342 cmyk_buf[ci + 3] = src_k as f32;
8343 if has_spot_contrib {
8344 spot_mask[mi] = 1;
8345 }
8346 }
8347 }
8348 }
8349}
8350
8351#[allow(clippy::too_many_arguments)]
8353fn render_overprint_image(
8354 pixmap: &mut Pixmap,
8355 cmyk_buf: &mut [f32],
8356 op_bg: &mut [u8],
8357 op_touched: &mut [u8],
8358 band_state: &mut BandState,
8359 sample_data: &[u8],
8360 params: &ImageParams,
8361 vp_x: f32,
8362 vp_y: f32,
8363 scale_x: f32,
8364 scale_y: f32,
8365 out_w: u32,
8366 out_h: u32,
8367 icc: Option<&IccCache>,
8368) {
8369 let iw = params.width as usize;
8370 let ih = params.height as usize;
8371 let Some(image_inv) = params.image_matrix.invert() else {
8372 return;
8373 };
8374 let combined = params.ctm.concat(&image_inv);
8375 let Some(inv_combined) = combined.invert() else {
8376 return;
8377 };
8378
8379 let px_data = pixmap.data_mut();
8380 let stride = out_w as usize;
8381 let inv_sx = 1.0 / scale_x as f64;
8382 let inv_sy = 1.0 / scale_y as f64;
8383
8384 let clip_data: Option<&[u8]> = match &band_state.clip_region {
8385 Some(ClipRegion::Mask(m)) => Some(m.data()),
8386 _ => None,
8387 };
8388 let clip_rect = match &band_state.clip_region {
8389 Some(ClipRegion::Rect(r)) => Some(*r),
8390 _ => None,
8391 };
8392
8393 let mask_info = if let ImageColorSpace::Mask {
8394 color, polarity, ..
8395 } = ¶ms.color_space
8396 {
8397 let (src_c, src_m, src_y, src_k) = color.native_cmyk.unwrap_or_else(|| {
8398 let r = color.r;
8399 let g = color.g;
8400 let b = color.b;
8401 (1.0 - r, 1.0 - g, 1.0 - b, 0.0)
8402 });
8403 Some((src_c, src_m, src_y, src_k, *polarity, iw.div_ceil(8)))
8404 } else {
8405 None
8406 };
8407
8408 for by in 0..out_h as usize {
8409 for bx in 0..out_w as usize {
8410 if let Some(ref r) = clip_rect
8411 && ((by as u32) < r.y0
8412 || (by as u32) >= r.y1
8413 || (bx as u32) < r.x0
8414 || (bx as u32) >= r.x1)
8415 {
8416 continue;
8417 }
8418 if let Some(clip) = clip_data {
8419 let ci_clip = by * stride + bx;
8420 if clip[ci_clip] == 0 {
8421 let bh = out_h as usize;
8422 let has_neighbor = (bx > 0 && clip[ci_clip - 1] != 0)
8423 || (bx + 1 < stride && clip[ci_clip + 1] != 0)
8424 || (by > 0 && clip[ci_clip - stride] != 0)
8425 || (by + 1 < bh && clip[ci_clip + stride] != 0)
8426 || (bx > 0 && by > 0 && clip[ci_clip - stride - 1] != 0)
8427 || (bx + 1 < stride && by > 0 && clip[ci_clip - stride + 1] != 0)
8428 || (bx > 0 && by + 1 < bh && clip[ci_clip + stride - 1] != 0)
8429 || (bx + 1 < stride && by + 1 < bh && clip[ci_clip + stride + 1] != 0);
8430 if !has_neighbor {
8431 continue;
8432 }
8433 }
8434 }
8435
8436 let dx = (bx as f64 + 0.5) * inv_sx + vp_x as f64;
8438 let dy = (by as f64 + 0.5) * inv_sy + vp_y as f64;
8439 let ix = inv_combined.a * dx + inv_combined.c * dy + inv_combined.tx;
8440 let iy = inv_combined.b * dx + inv_combined.d * dy + inv_combined.ty;
8441
8442 let col = ix.floor() as i64;
8443 let row = iy.floor() as i64;
8444 if col < 0 || col >= iw as i64 || row < 0 || row >= ih as i64 {
8445 continue;
8446 }
8447 let col = col as usize;
8448 let row = row as usize;
8449
8450 let (src_c, src_m, src_y, src_k) =
8451 if let Some((mc, mm, my, mk, polarity, bytes_per_row)) = mask_info {
8452 let byte_idx = row * bytes_per_row + col / 8;
8453 let bit_offset = 7 - (col % 8);
8454 let bit = if byte_idx < sample_data.len() {
8455 (sample_data[byte_idx] >> bit_offset) & 1
8456 } else {
8457 0
8458 };
8459 let paint = if polarity { bit == 1 } else { bit == 0 };
8460 if !paint {
8461 continue;
8462 }
8463 (mc, mm, my, mk)
8464 } else if let Some(cmyk) =
8465 sample_pixel_cmyk(sample_data, ¶ms.color_space, iw, row, col)
8466 {
8467 cmyk
8468 } else {
8469 continue;
8470 };
8471
8472 let mi = by * stride + bx;
8473 let ci = mi * 4;
8474 let pi = mi * 4;
8475
8476 if image_cs_has_spot_tint_transform(¶ms.color_space) {
8495 let cur_c = cmyk_buf[ci] as f64;
8496 let cur_m = cmyk_buf[ci + 1] as f64;
8497 let cur_y = cmyk_buf[ci + 2] as f64;
8498 let cur_k = cmyk_buf[ci + 3] as f64;
8499 let cur_is_zero = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
8500 let named = params.painted_channels;
8501 let opm1 = params.overprint_mode == 1;
8508 let (new_c, new_m, new_y, new_k) = if cur_is_zero {
8509 (src_c, src_m, src_y, src_k)
8510 } else {
8511 let nc =
8512 if named & stet_graphics::device::CMYK_C != 0 && !(opm1 && src_c == 0.0) {
8513 src_c
8514 } else {
8515 cur_c
8516 };
8517 let nm =
8518 if named & stet_graphics::device::CMYK_M != 0 && !(opm1 && src_m == 0.0) {
8519 src_m
8520 } else {
8521 cur_m
8522 };
8523 let ny =
8524 if named & stet_graphics::device::CMYK_Y != 0 && !(opm1 && src_y == 0.0) {
8525 src_y
8526 } else {
8527 cur_y
8528 };
8529 let nk =
8530 if named & stet_graphics::device::CMYK_K != 0 && !(opm1 && src_k == 0.0) {
8531 src_k
8532 } else {
8533 cur_k
8534 };
8535 (nc, nm, ny, nk)
8536 };
8537 cmyk_buf[ci] = new_c as f32;
8538 cmyk_buf[ci + 1] = new_m as f32;
8539 cmyk_buf[ci + 2] = new_y as f32;
8540 cmyk_buf[ci + 3] = new_k as f32;
8541 let alt_is_non_cmyk = image_cs_alt_is_non_cmyk(¶ms.color_space);
8550 let (r, g, b) = if cur_is_zero
8551 && alt_is_non_cmyk
8552 && let Some(rgb) =
8553 sample_pixel_visual_rgb(sample_data, ¶ms.color_space, iw, row, col)
8554 {
8555 rgb
8556 } else if let Some(icc_cache) = icc {
8557 icc_cache
8558 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
8559 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
8560 } else {
8561 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
8562 };
8563 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
8564 op_bg[pi] = px_data[pi];
8565 op_bg[pi + 1] = px_data[pi + 1];
8566 op_bg[pi + 2] = px_data[pi + 2];
8567 op_bg[pi + 3] = px_data[pi + 3];
8568 op_touched[mi] = 1;
8569 }
8570 px_data[pi] = (r * 255.0).round() as u8;
8571 px_data[pi + 1] = (g * 255.0).round() as u8;
8572 px_data[pi + 2] = (b * 255.0).round() as u8;
8573 px_data[pi + 3] = 255;
8574 continue;
8575 }
8576
8577 let mut channels = params.painted_channels;
8578 if channels == 0 {
8581 channels = stet_graphics::device::CMYK_ALL;
8582 }
8583 let is_direct_cmyk = matches!(
8584 ¶ms.color_space,
8585 ImageColorSpace::DeviceCMYK
8586 | ImageColorSpace::ICCBased { n: 4, .. }
8587 | ImageColorSpace::Mask { .. }
8588 );
8589 let is_custom_spot = params.painted_channels == 0
8599 && !is_cmyk_color_space(¶ms.color_space)
8600 && match ¶ms.color_space {
8601 ImageColorSpace::Mask { color, .. } => color.native_cmyk.is_some(),
8602 _ => true,
8603 };
8604 if params.overprint_mode == 1
8605 && channels == stet_graphics::device::CMYK_ALL
8606 && is_direct_cmyk
8607 {
8608 channels = 0;
8609 if src_c != 0.0 {
8610 channels |= stet_graphics::device::CMYK_C;
8611 }
8612 if src_m != 0.0 {
8613 channels |= stet_graphics::device::CMYK_M;
8614 }
8615 if src_y != 0.0 {
8616 channels |= stet_graphics::device::CMYK_Y;
8617 }
8618 if src_k != 0.0 {
8619 channels |= stet_graphics::device::CMYK_K;
8620 }
8621 }
8622
8623 let cur_c = cmyk_buf[ci] as f64;
8624 let cur_m = cmyk_buf[ci + 1] as f64;
8625 let cur_y = cmyk_buf[ci + 2] as f64;
8626 let cur_k = cmyk_buf[ci + 3] as f64;
8627 let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
8628 let pixmap_has_colour = px_data[pi + 3] > 0
8629 && (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
8630 let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
8638
8639 let new_c = if channels & stet_graphics::device::CMYK_C != 0 {
8640 src_c
8641 } else {
8642 cur_c
8643 };
8644 let new_m = if channels & stet_graphics::device::CMYK_M != 0 {
8645 src_m
8646 } else {
8647 cur_m
8648 };
8649 let new_y = if channels & stet_graphics::device::CMYK_Y != 0 {
8650 src_y
8651 } else {
8652 cur_y
8653 };
8654 let new_k = if channels & stet_graphics::device::CMYK_K != 0 {
8655 src_k
8656 } else {
8657 cur_k
8658 };
8659
8660 if !is_custom_spot {
8661 cmyk_buf[ci] = new_c as f32;
8662 cmyk_buf[ci + 1] = new_m as f32;
8663 cmyk_buf[ci + 2] = new_y as f32;
8664 cmyk_buf[ci + 3] = new_k as f32;
8665 }
8666
8667 let (r, g, b) = if use_multiplicative {
8668 let bg_r = px_data[pi] as f64 / 255.0;
8669 let bg_g = px_data[pi + 1] as f64 / 255.0;
8670 let bg_b = px_data[pi + 2] as f64 / 255.0;
8671 let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
8672 1.0 - src_c
8673 } else {
8674 1.0
8675 };
8676 let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
8677 1.0 - src_m
8678 } else {
8679 1.0
8680 };
8681 let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
8682 1.0 - src_y
8683 } else {
8684 1.0
8685 };
8686 let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
8687 1.0 - src_k
8688 } else {
8689 1.0
8690 };
8691 (
8692 (bg_r * over_r * k_fac).clamp(0.0, 1.0),
8693 (bg_g * over_g * k_fac).clamp(0.0, 1.0),
8694 (bg_b * over_b * k_fac).clamp(0.0, 1.0),
8695 )
8696 } else if let Some(icc_cache) = icc {
8697 icc_cache
8698 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
8699 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
8700 } else {
8701 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
8702 };
8703
8704 if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
8707 op_bg[pi] = px_data[pi];
8708 op_bg[pi + 1] = px_data[pi + 1];
8709 op_bg[pi + 2] = px_data[pi + 2];
8710 op_bg[pi + 3] = px_data[pi + 3];
8711 op_touched[mi] = 1;
8712 }
8713
8714 px_data[pi] = (r * 255.0).round() as u8;
8715 px_data[pi + 1] = (g * 255.0).round() as u8;
8716 px_data[pi + 2] = (b * 255.0).round() as u8;
8717 px_data[pi + 3] = 255;
8718 }
8719 }
8720}
8721
8722#[allow(clippy::too_many_arguments)]
8732fn update_cmyk_buffer_for_image(
8733 cmyk_buf: &mut [f32],
8734 sample_data: &[u8],
8735 pixmap_rgba: &[u8],
8736 params: &ImageParams,
8737 vp_x: f32,
8738 vp_y: f32,
8739 scale_x: f32,
8740 scale_y: f32,
8741 out_w: u32,
8742 out_h: u32,
8743 clip_region: &Option<ClipRegion>,
8744 icc: Option<&IccCache>,
8745) {
8746 let iw = params.width as usize;
8747 let ih = params.height as usize;
8748 let Some(image_inv) = params.image_matrix.invert() else {
8749 return;
8750 };
8751 let combined = params.ctm.concat(&image_inv);
8752 let Some(inv_combined) = combined.invert() else {
8753 return;
8754 };
8755 let stride = out_w as usize;
8756 let inv_sx = 1.0 / scale_x as f64;
8757 let inv_sy = 1.0 / scale_y as f64;
8758
8759 let mask_info = if let ImageColorSpace::Mask {
8760 color, polarity, ..
8761 } = ¶ms.color_space
8762 {
8763 let Some((c, m, y, k)) = color.native_cmyk else {
8764 return;
8765 };
8766 Some((
8767 c as f32,
8768 m as f32,
8769 y as f32,
8770 k as f32,
8771 *polarity,
8772 iw.div_ceil(8),
8773 ))
8774 } else {
8775 None
8776 };
8777
8778 let clip_data: Option<&[u8]> = match clip_region {
8779 Some(ClipRegion::Mask(m)) => Some(m.data()),
8780 _ => None,
8781 };
8782 let clip_rect = match clip_region {
8783 Some(ClipRegion::Rect(r)) => Some(*r),
8784 _ => None,
8785 };
8786
8787 for by in 0..out_h as usize {
8788 for bx in 0..out_w as usize {
8789 if let Some(ref r) = clip_rect
8790 && ((by as u32) < r.y0
8791 || (by as u32) >= r.y1
8792 || (bx as u32) < r.x0
8793 || (bx as u32) >= r.x1)
8794 {
8795 continue;
8796 }
8797 if let Some(clip) = clip_data
8798 && clip[by * stride + bx] == 0
8799 {
8800 continue;
8801 }
8802
8803 let dx = (bx as f64 + 0.5) * inv_sx + vp_x as f64;
8804 let dy = (by as f64 + 0.5) * inv_sy + vp_y as f64;
8805 let ix = inv_combined.a * dx + inv_combined.c * dy + inv_combined.tx;
8806 let iy = inv_combined.b * dx + inv_combined.d * dy + inv_combined.ty;
8807
8808 let col = ix.floor() as i64;
8809 let row = iy.floor() as i64;
8810 if col < 0 || col >= iw as i64 || row < 0 || row >= ih as i64 {
8811 continue;
8812 }
8813 let col = col as usize;
8814 let row = row as usize;
8815
8816 let ci = (by * stride + bx) * 4;
8817 if let Some((sc, sm, sy, sk, polarity, bytes_per_row)) = mask_info {
8818 let byte_idx = row * bytes_per_row + col / 8;
8819 let bit_offset = 7 - (col % 8);
8820 let bit = if byte_idx < sample_data.len() {
8821 (sample_data[byte_idx] >> bit_offset) & 1
8822 } else {
8823 0
8824 };
8825 let paint = if polarity { bit == 1 } else { bit == 0 };
8826 if paint {
8827 cmyk_buf[ci] = sc;
8828 cmyk_buf[ci + 1] = sm;
8829 cmyk_buf[ci + 2] = sy;
8830 cmyk_buf[ci + 3] = sk;
8831 }
8832 } else if let Some((sc, sm, sy, sk)) =
8833 sample_pixel_cmyk(sample_data, ¶ms.color_space, iw, row, col)
8834 {
8835 cmyk_buf[ci] = sc as f32;
8836 cmyk_buf[ci + 1] = sm as f32;
8837 cmyk_buf[ci + 2] = sy as f32;
8838 cmyk_buf[ci + 3] = sk as f32;
8839 } else if ci + 3 < pixmap_rgba.len() && pixmap_rgba[ci + 3] > 0 {
8840 let r = pixmap_rgba[ci] as f64 / 255.0;
8844 let g = pixmap_rgba[ci + 1] as f64 / 255.0;
8845 let b = pixmap_rgba[ci + 2] as f64 / 255.0;
8846 let cmyk =
8847 if let Some(c) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(r, g, b)) {
8848 c
8849 } else {
8850 [
8851 (1.0 - r).clamp(0.0, 1.0),
8852 (1.0 - g).clamp(0.0, 1.0),
8853 (1.0 - b).clamp(0.0, 1.0),
8854 0.0,
8855 ]
8856 };
8857 cmyk_buf[ci] = cmyk[0] as f32;
8858 cmyk_buf[ci + 1] = cmyk[1] as f32;
8859 cmyk_buf[ci + 2] = cmyk[2] as f32;
8860 cmyk_buf[ci + 3] = cmyk[3] as f32;
8861 }
8862 }
8863 }
8864}
8865fn image_supports_overprint(cs: &ImageColorSpace) -> bool {
8869 use stet_graphics::device::cmyk_channel_for_name;
8870 match cs {
8871 ImageColorSpace::Mask { .. } => true,
8872 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. } => true,
8873 ImageColorSpace::Separation {
8874 alt_space, name, ..
8875 } => {
8876 matches!(
8877 alt_space.as_ref(),
8878 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8879 ) || cmyk_channel_for_name(name) != 0
8880 }
8881 ImageColorSpace::DeviceN {
8882 alt_space, names, ..
8883 } => {
8884 matches!(
8885 alt_space.as_ref(),
8886 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8887 ) || names.iter().any(|n| cmyk_channel_for_name(n) != 0)
8888 }
8889 ImageColorSpace::Indexed { base, .. } => image_supports_overprint(base),
8890 _ => false,
8891 }
8892}
8893
8894fn is_cmyk_color_space(cs: &ImageColorSpace) -> bool {
8896 match cs {
8897 ImageColorSpace::DeviceCMYK => true,
8898 ImageColorSpace::ICCBased { n: 4, .. } => true,
8899 ImageColorSpace::Indexed { base, .. } => is_cmyk_color_space(base),
8900 _ => false,
8901 }
8902}
8903
8904fn image_cs_has_spot_tint_transform(cs: &ImageColorSpace) -> bool {
8911 use stet_graphics::device::cmyk_channel_for_name;
8912 let is_cmyk_alt = |alt: &ImageColorSpace| {
8913 matches!(
8914 alt,
8915 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8916 )
8917 };
8918 match cs {
8919 ImageColorSpace::Separation {
8920 name, alt_space, ..
8921 } => cmyk_channel_for_name(name) == 0 && is_cmyk_alt(alt_space.as_ref()),
8922 ImageColorSpace::DeviceN {
8923 names, alt_space, ..
8924 } => {
8925 let has_spot = names.iter().any(|n| cmyk_channel_for_name(n) == 0);
8926 let has_process = names.iter().any(|n| cmyk_channel_for_name(n) != 0);
8927 has_spot && (is_cmyk_alt(alt_space.as_ref()) || has_process)
8928 }
8929 ImageColorSpace::Indexed { base, .. } => image_cs_has_spot_tint_transform(base),
8930 _ => false,
8931 }
8932}
8933
8934fn image_cs_alt_is_non_cmyk(cs: &ImageColorSpace) -> bool {
8940 let is_cmyk_alt = |alt: &ImageColorSpace| {
8941 matches!(
8942 alt,
8943 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
8944 )
8945 };
8946 match cs {
8947 ImageColorSpace::Separation { alt_space, .. }
8948 | ImageColorSpace::DeviceN { alt_space, .. } => !is_cmyk_alt(alt_space.as_ref()),
8949 ImageColorSpace::Indexed { base, .. } => image_cs_alt_is_non_cmyk(base),
8950 _ => false,
8951 }
8952}
8953
8954fn sample_pixel_visual_rgb(
8959 sample_data: &[u8],
8960 cs: &ImageColorSpace,
8961 iw: usize,
8962 row: usize,
8963 col: usize,
8964) -> Option<(f64, f64, f64)> {
8965 let to_f64 = |(r, g, b): (u8, u8, u8)| (r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0);
8966 match cs {
8967 ImageColorSpace::Separation {
8968 alt_space,
8969 tint_table,
8970 ..
8971 } => {
8972 let si = row * iw + col;
8973 if si >= sample_data.len() {
8974 return None;
8975 }
8976 let tint = sample_data[si] as f32 / 255.0;
8977 let no = tint_table.num_outputs as usize;
8978 let mut comps = vec![0.0f32; no];
8979 tint_table.lookup_1d(tint, &mut comps);
8980 Some(to_f64(alt_comps_to_rgb(&comps, alt_space)))
8981 }
8982 ImageColorSpace::DeviceN {
8983 alt_space,
8984 tint_table,
8985 ..
8986 } => {
8987 let ni = tint_table.num_inputs as usize;
8988 let si = (row * iw + col) * ni;
8989 if si + ni > sample_data.len() {
8990 return None;
8991 }
8992 let mut inputs = vec![0.0f32; ni];
8993 for (c, inp) in inputs.iter_mut().enumerate() {
8994 *inp = sample_data[si + c] as f32 / 255.0;
8995 }
8996 let no = tint_table.num_outputs as usize;
8997 let mut comps = vec![0.0f32; no];
8998 tint_table.lookup_nd(&inputs, &mut comps);
8999 Some(to_f64(alt_comps_to_rgb(&comps, alt_space)))
9000 }
9001 ImageColorSpace::Indexed {
9002 base,
9003 hival,
9004 lookup,
9005 } => {
9006 let pi = row * iw + col;
9007 if pi >= sample_data.len() {
9008 return None;
9009 }
9010 let idx = (sample_data[pi] as usize).min(*hival as usize);
9011 let base_ncomp = base.num_components() as usize;
9012 let li = idx * base_ncomp;
9013 if li + base_ncomp > lookup.len() {
9014 return None;
9015 }
9016 match base.as_ref() {
9017 ImageColorSpace::Separation {
9018 alt_space,
9019 tint_table,
9020 ..
9021 } => {
9022 let tint = lookup[li] as f32 / 255.0;
9023 let no = tint_table.num_outputs as usize;
9024 let mut comps = vec![0.0f32; no];
9025 tint_table.lookup_1d(tint, &mut comps);
9026 Some(to_f64(alt_comps_to_rgb(&comps, alt_space)))
9027 }
9028 ImageColorSpace::DeviceN {
9029 alt_space,
9030 tint_table,
9031 ..
9032 } => {
9033 let ni = tint_table.num_inputs as usize;
9034 let mut inputs = vec![0.0f32; ni];
9035 for (c, inp) in inputs.iter_mut().enumerate() {
9036 if c < base_ncomp {
9037 *inp = lookup[li + c] as f32 / 255.0;
9038 }
9039 }
9040 let no = tint_table.num_outputs as usize;
9041 let mut comps = vec![0.0f32; no];
9042 tint_table.lookup_nd(&inputs, &mut comps);
9043 Some(to_f64(alt_comps_to_rgb(&comps, alt_space)))
9044 }
9045 _ => None,
9046 }
9047 }
9048 _ => None,
9049 }
9050}
9051
9052fn devicen_named_cmyk(names: &[Vec<u8>], inputs: &[u8]) -> (f64, f64, f64, f64) {
9058 use stet_graphics::device::{CMYK_C, CMYK_K, CMYK_M, CMYK_Y, cmyk_channel_for_name};
9059 let mut c = 0.0;
9060 let mut m = 0.0;
9061 let mut y = 0.0;
9062 let mut k = 0.0;
9063 for (i, name) in names.iter().enumerate() {
9064 let bit = cmyk_channel_for_name(name);
9065 if bit == 0 {
9066 continue;
9067 }
9068 let v = inputs.get(i).copied().unwrap_or(0) as f64 / 255.0;
9069 if bit & CMYK_C != 0 {
9070 c = v;
9071 }
9072 if bit & CMYK_M != 0 {
9073 m = v;
9074 }
9075 if bit & CMYK_Y != 0 {
9076 y = v;
9077 }
9078 if bit & CMYK_K != 0 {
9079 k = v;
9080 }
9081 }
9082 (c, m, y, k)
9083}
9084
9085fn sample_pixel_cmyk(
9090 sample_data: &[u8],
9091 cs: &ImageColorSpace,
9092 iw: usize,
9093 row: usize,
9094 col: usize,
9095) -> Option<(f64, f64, f64, f64)> {
9096 use stet_graphics::device::cmyk_channel_for_name;
9097 let is_cmyk_alt = |alt: &ImageColorSpace| {
9098 matches!(
9099 alt,
9100 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
9101 )
9102 };
9103 match cs {
9104 ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. } => {
9105 let si = (row * iw + col) * 4;
9106 if si + 3 < sample_data.len() {
9107 Some((
9108 sample_data[si] as f64 / 255.0,
9109 sample_data[si + 1] as f64 / 255.0,
9110 sample_data[si + 2] as f64 / 255.0,
9111 sample_data[si + 3] as f64 / 255.0,
9112 ))
9113 } else {
9114 None
9115 }
9116 }
9117 ImageColorSpace::Separation {
9118 alt_space,
9119 tint_table,
9120 name,
9121 } => {
9122 let si = row * iw + col;
9123 if si >= sample_data.len() {
9124 return None;
9125 }
9126 let tint = sample_data[si] as f32 / 255.0;
9127 if is_cmyk_alt(alt_space.as_ref()) {
9128 let mut alt = [0.0f32; 4];
9129 tint_table.lookup_1d(tint, &mut alt);
9130 return Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64));
9131 }
9132 let bit = cmyk_channel_for_name(name);
9134 if bit == 0 {
9135 return None;
9136 }
9137 let names = vec![name.clone()];
9138 let inputs = [(tint * 255.0).round() as u8];
9139 Some(devicen_named_cmyk(&names, &inputs))
9140 }
9141 ImageColorSpace::DeviceN {
9142 alt_space,
9143 tint_table,
9144 names,
9145 } => {
9146 let ni = tint_table.num_inputs as usize;
9147 let si = (row * iw + col) * ni;
9148 if si + ni > sample_data.len() {
9149 return None;
9150 }
9151 if is_cmyk_alt(alt_space.as_ref()) {
9152 let mut inputs = vec![0.0f32; ni];
9153 for (c, inp) in inputs.iter_mut().enumerate() {
9154 *inp = sample_data[si + c] as f32 / 255.0;
9155 }
9156 let mut alt = [0.0f32; 4];
9157 tint_table.lookup_nd(&inputs, &mut alt);
9158 return Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64));
9159 }
9160 if !names.iter().any(|n| cmyk_channel_for_name(n) != 0) {
9162 return None;
9163 }
9164 Some(devicen_named_cmyk(names, &sample_data[si..si + ni]))
9165 }
9166 ImageColorSpace::Indexed {
9167 base,
9168 hival,
9169 lookup,
9170 } => {
9171 let pi = row * iw + col;
9172 if pi >= sample_data.len() {
9173 return None;
9174 }
9175 let idx = sample_data[pi] as usize;
9176 let idx = idx.min(*hival as usize);
9177 let base_ncomp = base.num_components() as usize;
9178 let li = idx * base_ncomp;
9179 if is_cmyk_color_space(base) && base_ncomp == 4 && li + 3 < lookup.len() {
9181 return Some((
9182 lookup[li] as f64 / 255.0,
9183 lookup[li + 1] as f64 / 255.0,
9184 lookup[li + 2] as f64 / 255.0,
9185 lookup[li + 3] as f64 / 255.0,
9186 ));
9187 }
9188 if li + base_ncomp <= lookup.len() {
9190 match base.as_ref() {
9191 ImageColorSpace::Separation {
9192 alt_space,
9193 tint_table,
9194 name,
9195 } => {
9196 let tint = lookup[li] as f32 / 255.0;
9197 if is_cmyk_alt(alt_space.as_ref()) {
9198 let mut alt = [0.0f32; 4];
9199 tint_table.lookup_1d(tint, &mut alt);
9200 return Some((
9201 alt[0] as f64,
9202 alt[1] as f64,
9203 alt[2] as f64,
9204 alt[3] as f64,
9205 ));
9206 }
9207 let bit = cmyk_channel_for_name(name);
9209 if bit == 0 {
9210 return None;
9211 }
9212 let names = vec![name.clone()];
9213 let inputs = [(tint * 255.0).round() as u8];
9214 return Some(devicen_named_cmyk(&names, &inputs));
9215 }
9216 ImageColorSpace::DeviceN {
9217 alt_space,
9218 tint_table,
9219 names,
9220 } => {
9221 let ni = tint_table.num_inputs as usize;
9222 if is_cmyk_alt(alt_space.as_ref()) {
9223 let mut inputs = vec![0.0f32; ni];
9224 for (c, inp) in inputs.iter_mut().enumerate() {
9225 if c < base_ncomp {
9226 *inp = lookup[li + c] as f32 / 255.0;
9227 }
9228 }
9229 let mut alt = [0.0f32; 4];
9230 tint_table.lookup_nd(&inputs, &mut alt);
9231 return Some((
9232 alt[0] as f64,
9233 alt[1] as f64,
9234 alt[2] as f64,
9235 alt[3] as f64,
9236 ));
9237 }
9238 if !names.iter().any(|n| cmyk_channel_for_name(n) != 0) {
9240 return None;
9241 }
9242 let take = ni.min(base_ncomp);
9243 return Some(devicen_named_cmyk(names, &lookup[li..li + take]));
9244 }
9245 _ => {}
9246 }
9247 }
9248 None
9249 }
9250 _ => None,
9251 }
9252}
9253#[allow(clippy::too_many_arguments)]
9259fn render_banded_to_sink(
9260 page_w: u32,
9261 page_h: u32,
9262 band_h: u32,
9263 dpi: f64,
9264 list: &DisplayList,
9265 sink: &mut dyn stet_graphics::device::PageSink,
9266 icc_cache: &IccCache,
9267 no_aa: bool,
9268 layer_set: &LayerSet,
9269) -> Result<(), String> {
9270 let bboxes = precompute_bboxes(list, dpi);
9272
9273 let epochs = build_clip_epochs(list, &bboxes);
9277
9278 let clip_seen = precompute_clip_seen(list);
9280
9281 use stet_graphics::display_list::GroupColorSpace;
9286 let needs_cmyk_buffer = has_overprint_elements(list)
9287 || list.page_group_color_space() == GroupColorSpace::DeviceCMYK
9288 || has_cmyk_group(list);
9289
9290 let preprocessed_images = preprocess_images_for_bands(list, Some(icc_cache));
9292
9293 const BAND_OVERLAP: u32 = 6;
9297
9298 let render_h = band_h + 2 * BAND_OVERLAP;
9299
9300 sink.begin_page(page_w, page_h)?;
9302
9303 let num_bands = page_h.div_ceil(band_h);
9304 let elements = list.elements();
9305 let row_bytes = page_w as usize * 4;
9306 let icc_ref = Some(icc_cache);
9307
9308 let render_band = |band_idx: u32| -> Vec<u8> {
9310 let y_start = band_idx * band_h;
9311 let actual_h = (page_h - y_start).min(band_h);
9312
9313 let render_y_start = y_start.saturating_sub(BAND_OVERLAP);
9314 let render_y_end_f = ((y_start + actual_h + BAND_OVERLAP).min(page_h)) as f64;
9315 let band_offset = y_start - render_y_start;
9316
9317 let mut band_pixmap = Pixmap::new(page_w, render_h).expect("Failed to create band pixmap");
9318 band_pixmap.as_mut().data_mut().fill(0x00);
9320
9321 let cmyk_buf = if needs_cmyk_buffer {
9322 Some(vec![0.0f32; page_w as usize * render_h as usize * 4])
9324 } else {
9325 None
9326 };
9327
9328 let mut band_state = BandState {
9329 clip_region: None,
9330 spare_mask: None,
9331 clip_mask_cache: HashMap::new(),
9332 clip_mask_seen: clip_seen.clone(),
9333 mask_pool: Vec::new(),
9334 cmyk_buffer: cmyk_buf,
9335 op_bg_snapshot: None,
9336 op_touched: None,
9337 spot_mask: None,
9338 };
9339
9340 for epoch in &epochs {
9342 if !epoch.has_erase_page {
9343 match epoch.paint_bbox {
9344 Some(ref pb)
9345 if pb.y_max <= render_y_start as f64 || pb.y_min >= render_y_end_f =>
9346 {
9347 continue;
9348 }
9349 None => continue,
9350 _ => {}
9351 }
9352 }
9353
9354 for i in epoch.start_idx..epoch.end_idx {
9355 let force_process = matches!(
9361 &elements[i],
9362 DisplayElement::OcgGroup { elements: inner, .. }
9363 if contains_clip_op(inner)
9364 );
9365 if !force_process
9366 && let Some(ref bbox) = bboxes[i]
9367 && (bbox.y_max <= render_y_start as f64 || bbox.y_min >= render_y_end_f)
9368 {
9369 continue;
9370 }
9371 let ctx = RenderContext {
9372 vp_x: 0.0,
9373 vp_y: render_y_start as f32,
9374 scale_x: 1.0,
9375 scale_y: 1.0,
9376 out_w: page_w,
9377 out_h: render_h,
9378 effective_dpi: dpi,
9379 icc: icc_ref,
9380 image_cache: None,
9381 preprocessed: Some(&preprocessed_images),
9382 elem_idx: i,
9383 no_aa,
9384 opm_zero_transparent: false,
9385 knockout_painter_pass: KnockoutPainterPass::None,
9386 parent_group_isolated: false,
9387 alpha_extraction_pass: false,
9388 layer_set,
9389 };
9390 render_element(&mut band_pixmap, &mut band_state, &elements[i], &ctx);
9391 }
9392 }
9393
9394 composite_onto_white(band_pixmap.data_mut());
9396
9397 let start_byte = band_offset as usize * row_bytes;
9399 let total_bytes = actual_h as usize * row_bytes;
9400 band_pixmap.data()[start_byte..start_byte + total_bytes].to_vec()
9401 };
9402
9403 #[cfg(feature = "parallel")]
9405 {
9406 let chunk_size = rayon::current_num_threads().max(1);
9411
9412 for chunk_start in (0..num_bands).step_by(chunk_size) {
9413 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
9414
9415 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
9416 .into_par_iter()
9417 .map(&render_band)
9418 .collect();
9419
9420 for (i, band_data) in rendered.iter().enumerate() {
9421 let band_idx = chunk_start + i as u32;
9422 let y_start = band_idx * band_h;
9423 let actual_h = (page_h - y_start).min(band_h);
9424 sink.write_rows(band_data, actual_h)?;
9425 }
9426 }
9427 }
9428 #[cfg(not(feature = "parallel"))]
9429 {
9430 for band_idx in 0..num_bands {
9432 let band_data = render_band(band_idx);
9433 let y_start = band_idx * band_h;
9434 let actual_h = (page_h - y_start).min(band_h);
9435 sink.write_rows(&band_data, actual_h)?;
9436 }
9437 }
9438
9439 sink.end_page()
9440}
9441
9442#[derive(Clone, Copy)]
9444struct BBox2D {
9445 x_min: f64,
9446 y_min: f64,
9447 x_max: f64,
9448 y_max: f64,
9449}
9450
9451fn precompute_full_bboxes(list: &DisplayList, dpi: f64) -> Vec<Option<BBox2D>> {
9453 list.elements()
9454 .iter()
9455 .map(|elem| match elem {
9456 DisplayElement::Fill { path, params } => fill_device_full_bbox(path, ¶ms.ctm),
9457 DisplayElement::Stroke { path, params } => {
9458 path_full_bbox(path).map(|mut bbox| {
9459 let effective_lw = params.line_width.max(hairline_min_width(¶ms.ctm, dpi));
9461 let expand = effective_lw * params.miter_limit * 0.5;
9462 let m = ¶ms.ctm;
9463 let is_identity = m.a == 1.0
9464 && m.b == 0.0
9465 && m.c == 0.0
9466 && m.d == 1.0
9467 && m.tx == 0.0
9468 && m.ty == 0.0;
9469 if is_identity {
9470 bbox.x_min -= expand;
9471 bbox.x_max += expand;
9472 bbox.y_min -= expand;
9473 bbox.y_max += expand;
9474 } else {
9475 let col_x_len = (m.a * m.a + m.b * m.b).sqrt().max(1.0);
9478 let col_y_len = (m.c * m.c + m.d * m.d).sqrt().max(1.0);
9479 let expand_x = effective_lw * col_x_len * params.miter_limit * 0.5;
9480 let expand_y = effective_lw * col_y_len * params.miter_limit * 0.5;
9481 bbox.x_min -= expand_x;
9482 bbox.x_max += expand_x;
9483 bbox.y_min -= expand_y;
9484 bbox.y_max += expand_y;
9485 let corners = [
9487 (
9488 m.a * bbox.x_min + m.c * bbox.y_min + m.tx,
9489 m.b * bbox.x_min + m.d * bbox.y_min + m.ty,
9490 ),
9491 (
9492 m.a * bbox.x_max + m.c * bbox.y_min + m.tx,
9493 m.b * bbox.x_max + m.d * bbox.y_min + m.ty,
9494 ),
9495 (
9496 m.a * bbox.x_min + m.c * bbox.y_max + m.tx,
9497 m.b * bbox.x_min + m.d * bbox.y_max + m.ty,
9498 ),
9499 (
9500 m.a * bbox.x_max + m.c * bbox.y_max + m.tx,
9501 m.b * bbox.x_max + m.d * bbox.y_max + m.ty,
9502 ),
9503 ];
9504 bbox.x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
9505 bbox.x_max = corners
9506 .iter()
9507 .map(|c| c.0)
9508 .fold(f64::NEG_INFINITY, f64::max);
9509 bbox.y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
9510 bbox.y_max = corners
9511 .iter()
9512 .map(|c| c.1)
9513 .fold(f64::NEG_INFINITY, f64::max);
9514 }
9515 bbox
9516 })
9517 }
9518 DisplayElement::Image { params, .. } => image_full_bbox(params),
9519 DisplayElement::AxialShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9520 DisplayElement::RadialShading { params } => {
9521 shading_full_bbox(¶ms.bbox, ¶ms.ctm)
9522 }
9523 DisplayElement::MeshShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9524 DisplayElement::PatchShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9525 DisplayElement::PatternFill { params } => pattern_fill_full_bbox(params),
9526 DisplayElement::Group { params, .. } => Some(BBox2D {
9527 x_min: params.bbox[0],
9528 y_min: params.bbox[1],
9529 x_max: params.bbox[2],
9530 y_max: params.bbox[3],
9531 }),
9532 DisplayElement::SoftMasked { params, .. } => Some(BBox2D {
9533 x_min: params.bbox[0],
9534 y_min: params.bbox[1],
9535 x_max: params.bbox[2],
9536 y_max: params.bbox[3],
9537 }),
9538 DisplayElement::OcgGroup {
9539 elements,
9540 visibility,
9541 } => {
9542 if !visibility.default_visible() && !contains_clip_op(elements) {
9547 return None;
9548 }
9549 let child_bboxes = precompute_full_bboxes(elements, dpi);
9550 let mut x_min = f64::INFINITY;
9551 let mut y_min = f64::INFINITY;
9552 let mut x_max = f64::NEG_INFINITY;
9553 let mut y_max = f64::NEG_INFINITY;
9554 for cb in child_bboxes.into_iter().flatten() {
9555 x_min = x_min.min(cb.x_min);
9556 y_min = y_min.min(cb.y_min);
9557 x_max = x_max.max(cb.x_max);
9558 y_max = y_max.max(cb.y_max);
9559 }
9560 if x_min <= x_max && y_min <= y_max {
9561 Some(BBox2D {
9562 x_min,
9563 y_min,
9564 x_max,
9565 y_max,
9566 })
9567 } else {
9568 None
9569 }
9570 }
9571 _ => None, })
9573 .collect()
9574}
9575
9576fn clip_path_bbox(path: &PsPath, params: &ClipParams) -> Option<BBox2D> {
9585 let mut bbox = path_full_bbox(path)?;
9586 let ctm = ¶ms.ctm;
9587 let is_identity = ctm.a == 1.0
9588 && ctm.b == 0.0
9589 && ctm.c == 0.0
9590 && ctm.d == 1.0
9591 && ctm.tx == 0.0
9592 && ctm.ty == 0.0;
9593 if !is_identity {
9594 let corners = [
9595 ctm.transform_point(bbox.x_min, bbox.y_min),
9596 ctm.transform_point(bbox.x_max, bbox.y_min),
9597 ctm.transform_point(bbox.x_min, bbox.y_max),
9598 ctm.transform_point(bbox.x_max, bbox.y_max),
9599 ];
9600 bbox.x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
9601 bbox.x_max = corners
9602 .iter()
9603 .map(|c| c.0)
9604 .fold(f64::NEG_INFINITY, f64::max);
9605 bbox.y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
9606 bbox.y_max = corners
9607 .iter()
9608 .map(|c| c.1)
9609 .fold(f64::NEG_INFINITY, f64::max);
9610 }
9611 if let Some(sp) = ¶ms.stroke_params {
9612 let scale = (ctm.a * ctm.a + ctm.b * ctm.b)
9613 .sqrt()
9614 .max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt())
9615 .max(1.0);
9616 let expand = sp.line_width * 0.5 * scale;
9617 bbox.x_min -= expand;
9618 bbox.x_max += expand;
9619 bbox.y_min -= expand;
9620 bbox.y_max += expand;
9621 }
9622 Some(bbox)
9623}
9624
9625fn intersect_bbox(a: &BBox2D, b: &BBox2D) -> Option<BBox2D> {
9627 let x_min = a.x_min.max(b.x_min);
9628 let y_min = a.y_min.max(b.y_min);
9629 let x_max = a.x_max.min(b.x_max);
9630 let y_max = a.y_max.min(b.y_max);
9631 if x_min < x_max && y_min < y_max {
9632 Some(BBox2D {
9633 x_min,
9634 y_min,
9635 x_max,
9636 y_max,
9637 })
9638 } else {
9639 None
9640 }
9641}
9642
9643fn compute_paint_bounds(list: &DisplayList, _dpi: f64) -> Option<BBox2D> {
9664 let mut clip_stack: Vec<BBox2D> = Vec::new();
9668 let mut union: Option<BBox2D> = None;
9669
9670 let push_paint = |union: &mut Option<BBox2D>, clip_stack: &[BBox2D], bbox: BBox2D| {
9671 let visible = match clip_stack.last() {
9675 Some(clip) => match intersect_bbox(clip, &bbox) {
9676 Some(b) => b,
9677 None => return,
9678 },
9679 None => bbox,
9680 };
9681 *union = Some(match union.take() {
9682 None => visible,
9683 Some(u) => BBox2D {
9684 x_min: u.x_min.min(visible.x_min),
9685 y_min: u.y_min.min(visible.y_min),
9686 x_max: u.x_max.max(visible.x_max),
9687 y_max: u.y_max.max(visible.y_max),
9688 },
9689 });
9690 };
9691
9692 for elem in list.elements() {
9693 match elem {
9694 DisplayElement::Clip { path, params } => {
9695 if let Some(cb) = clip_path_bbox(path, params) {
9696 let new_top = match clip_stack.last() {
9697 Some(prev) => match intersect_bbox(prev, &cb) {
9698 Some(b) => b,
9699 None => BBox2D {
9703 x_min: 0.0,
9704 y_min: 0.0,
9705 x_max: 0.0,
9706 y_max: 0.0,
9707 },
9708 },
9709 None => cb,
9710 };
9711 clip_stack.push(new_top);
9712 }
9713 }
9714 DisplayElement::InitClip | DisplayElement::ErasePage => {
9715 clip_stack.clear();
9716 }
9717 DisplayElement::Fill { path, .. } => {
9718 if let Some(b) = path_full_bbox(path) {
9719 push_paint(&mut union, &clip_stack, b);
9720 }
9721 }
9722 DisplayElement::Stroke { path, params } => {
9723 if let Some(mut b) = path_full_bbox(path) {
9724 let expand = params.line_width * params.miter_limit * 0.5;
9725 b.x_min -= expand;
9726 b.x_max += expand;
9727 b.y_min -= expand;
9728 b.y_max += expand;
9729 push_paint(&mut union, &clip_stack, b);
9730 }
9731 }
9732 DisplayElement::Image { params, .. } => {
9733 if let Some(b) = image_full_bbox(params) {
9734 push_paint(&mut union, &clip_stack, b);
9735 }
9736 }
9737 DisplayElement::AxialShading { params } => {
9738 let b = match ¶ms.bbox {
9739 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9740 None => clip_stack.last().copied(),
9741 };
9742 if let Some(b) = b {
9743 push_paint(&mut union, &clip_stack, b);
9744 }
9745 }
9746 DisplayElement::RadialShading { params } => {
9747 let b = match ¶ms.bbox {
9748 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9749 None => clip_stack.last().copied(),
9750 };
9751 if let Some(b) = b {
9752 push_paint(&mut union, &clip_stack, b);
9753 }
9754 }
9755 DisplayElement::MeshShading { params } => {
9756 let b = match ¶ms.bbox {
9757 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9758 None => clip_stack.last().copied(),
9759 };
9760 if let Some(b) = b {
9761 push_paint(&mut union, &clip_stack, b);
9762 }
9763 }
9764 DisplayElement::PatchShading { params } => {
9765 let b = match ¶ms.bbox {
9766 Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
9767 None => clip_stack.last().copied(),
9768 };
9769 if let Some(b) = b {
9770 push_paint(&mut union, &clip_stack, b);
9771 }
9772 }
9773 DisplayElement::PatternFill { params } => {
9774 if let Some(b) = pattern_fill_full_bbox(params) {
9775 push_paint(&mut union, &clip_stack, b);
9776 }
9777 }
9778 DisplayElement::Group { params, .. } => {
9779 push_paint(
9780 &mut union,
9781 &clip_stack,
9782 BBox2D {
9783 x_min: params.bbox[0],
9784 y_min: params.bbox[1],
9785 x_max: params.bbox[2],
9786 y_max: params.bbox[3],
9787 },
9788 );
9789 }
9790 DisplayElement::SoftMasked { params, .. } => {
9791 push_paint(
9792 &mut union,
9793 &clip_stack,
9794 BBox2D {
9795 x_min: params.bbox[0],
9796 y_min: params.bbox[1],
9797 x_max: params.bbox[2],
9798 y_max: params.bbox[3],
9799 },
9800 );
9801 }
9802 DisplayElement::Text { .. } => {} DisplayElement::OcgGroup { .. } => {
9804 }
9809 _ => {}
9810 }
9811 }
9812 union
9813}
9814
9815fn fill_device_full_bbox(path: &PsPath, ctm: &Matrix) -> Option<BBox2D> {
9820 let bbox = path_full_bbox(path)?;
9821 let is_identity = ctm.a == 1.0
9822 && ctm.b == 0.0
9823 && ctm.c == 0.0
9824 && ctm.d == 1.0
9825 && ctm.tx == 0.0
9826 && ctm.ty == 0.0;
9827 if is_identity {
9828 return Some(bbox);
9829 }
9830 let corners = [
9831 (bbox.x_min, bbox.y_min),
9832 (bbox.x_max, bbox.y_min),
9833 (bbox.x_min, bbox.y_max),
9834 (bbox.x_max, bbox.y_max),
9835 ];
9836 let mut x_min = f64::INFINITY;
9837 let mut x_max = f64::NEG_INFINITY;
9838 let mut y_min = f64::INFINITY;
9839 let mut y_max = f64::NEG_INFINITY;
9840 for (x, y) in &corners {
9841 let dx = ctm.a * x + ctm.c * y + ctm.tx;
9842 let dy = ctm.b * x + ctm.d * y + ctm.ty;
9843 x_min = x_min.min(dx);
9844 x_max = x_max.max(dx);
9845 y_min = y_min.min(dy);
9846 y_max = y_max.max(dy);
9847 }
9848 Some(BBox2D {
9849 x_min,
9850 y_min,
9851 x_max,
9852 y_max,
9853 })
9854}
9855
9856fn path_full_bbox(path: &PsPath) -> Option<BBox2D> {
9857 let mut x_min = f64::INFINITY;
9858 let mut x_max = f64::NEG_INFINITY;
9859 let mut y_min = f64::INFINITY;
9860 let mut y_max = f64::NEG_INFINITY;
9861 for seg in &path.segments {
9862 match seg {
9863 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => {
9864 x_min = x_min.min(*x);
9865 x_max = x_max.max(*x);
9866 y_min = y_min.min(*y);
9867 y_max = y_max.max(*y);
9868 }
9869 PathSegment::CurveTo {
9870 x1,
9871 y1,
9872 x2,
9873 y2,
9874 x3,
9875 y3,
9876 } => {
9877 x_min = x_min.min(*x1).min(*x2).min(*x3);
9878 x_max = x_max.max(*x1).max(*x2).max(*x3);
9879 y_min = y_min.min(*y1).min(*y2).min(*y3);
9880 y_max = y_max.max(*y1).max(*y2).max(*y3);
9881 }
9882 PathSegment::ClosePath => {}
9883 }
9884 }
9885 if x_min <= x_max {
9886 Some(BBox2D {
9887 x_min,
9888 y_min,
9889 x_max,
9890 y_max,
9891 })
9892 } else {
9893 None
9894 }
9895}
9896
9897fn pattern_fill_full_bbox(params: &stet_graphics::device::PatternFillParams) -> Option<BBox2D> {
9902 if let Some(ref sp) = params.stroke_params {
9903 let bbox = path_full_bbox(¶ms.path)?;
9904 let ctm = &sp.ctm;
9905 let corners = [
9906 ctm.transform_point(bbox.x_min, bbox.y_min),
9907 ctm.transform_point(bbox.x_max, bbox.y_min),
9908 ctm.transform_point(bbox.x_min, bbox.y_max),
9909 ctm.transform_point(bbox.x_max, bbox.y_max),
9910 ];
9911 let mut dev_bbox = BBox2D {
9912 x_min: f64::INFINITY,
9913 y_min: f64::INFINITY,
9914 x_max: f64::NEG_INFINITY,
9915 y_max: f64::NEG_INFINITY,
9916 };
9917 for (x, y) in &corners {
9918 dev_bbox.x_min = dev_bbox.x_min.min(*x);
9919 dev_bbox.y_min = dev_bbox.y_min.min(*y);
9920 dev_bbox.x_max = dev_bbox.x_max.max(*x);
9921 dev_bbox.y_max = dev_bbox.y_max.max(*y);
9922 }
9923 let half_w = sp.line_width
9924 * 0.5
9925 * (ctm.a * ctm.a + ctm.b * ctm.b)
9926 .sqrt()
9927 .max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt());
9928 dev_bbox.x_min -= half_w;
9929 dev_bbox.y_min -= half_w;
9930 dev_bbox.x_max += half_w;
9931 dev_bbox.y_max += half_w;
9932 Some(dev_bbox)
9933 } else {
9934 path_full_bbox(¶ms.path)
9935 }
9936}
9937
9938fn pattern_fill_y_bbox(params: &stet_graphics::device::PatternFillParams) -> Option<YBBox> {
9940 let bbox = pattern_fill_full_bbox(params)?;
9941 Some(YBBox {
9942 y_min: bbox.y_min,
9943 y_max: bbox.y_max,
9944 })
9945}
9946
9947fn image_full_bbox(params: &ImageParams) -> Option<BBox2D> {
9949 let m = ¶ms.ctm;
9950 let im = ¶ms.image_matrix;
9951 let im_inv = im.invert()?;
9952 let combined = m.concat(&im_inv);
9953 let w = params.width as f64;
9955 let h = params.height as f64;
9956 let corners = [
9957 combined.transform_point(0.0, 0.0),
9958 combined.transform_point(w, 0.0),
9959 combined.transform_point(0.0, h),
9960 combined.transform_point(w, h),
9961 ];
9962 let mut x_min = f64::INFINITY;
9963 let mut x_max = f64::NEG_INFINITY;
9964 let mut y_min = f64::INFINITY;
9965 let mut y_max = f64::NEG_INFINITY;
9966 for (x, y) in &corners {
9967 x_min = x_min.min(*x);
9968 x_max = x_max.max(*x);
9969 y_min = y_min.min(*y);
9970 y_max = y_max.max(*y);
9971 }
9972 Some(BBox2D {
9973 x_min,
9974 y_min,
9975 x_max,
9976 y_max,
9977 })
9978}
9979
9980fn shading_full_bbox(bbox: &Option<[f64; 4]>, ctm: &Matrix) -> Option<BBox2D> {
9982 if let Some(bbox) = bbox {
9983 let corners = [
9984 ctm.transform_point(bbox[0], bbox[1]),
9985 ctm.transform_point(bbox[2], bbox[1]),
9986 ctm.transform_point(bbox[0], bbox[3]),
9987 ctm.transform_point(bbox[2], bbox[3]),
9988 ];
9989 let mut x_min = f64::INFINITY;
9990 let mut x_max = f64::NEG_INFINITY;
9991 let mut y_min = f64::INFINITY;
9992 let mut y_max = f64::NEG_INFINITY;
9993 for (x, y) in &corners {
9994 x_min = x_min.min(*x);
9995 x_max = x_max.max(*x);
9996 y_min = y_min.min(*y);
9997 y_max = y_max.max(*y);
9998 }
9999 Some(BBox2D {
10000 x_min,
10001 y_min,
10002 x_max,
10003 y_max,
10004 })
10005 } else {
10006 Some(BBox2D {
10007 x_min: 0.0,
10008 y_min: 0.0,
10009 x_max: 1e9,
10010 y_max: 1e9,
10011 })
10012 }
10013}
10014
10015fn build_viewport_epochs(list: &DisplayList, bboxes: &[Option<BBox2D>]) -> Vec<ViewportEpoch> {
10017 let elements = list.elements();
10018 let mut epochs = Vec::new();
10019 let mut epoch_start = 0;
10020 let mut x_min = f64::INFINITY;
10021 let mut x_max = f64::NEG_INFINITY;
10022 let mut y_min = f64::INFINITY;
10023 let mut y_max = f64::NEG_INFINITY;
10024 let mut has_erase = false;
10025
10026 for (i, element) in elements.iter().enumerate() {
10027 if matches!(element, DisplayElement::InitClip) && i > epoch_start {
10028 epochs.push(ViewportEpoch {
10029 start_idx: epoch_start,
10030 end_idx: i,
10031 paint_bbox: if x_min <= x_max {
10032 Some(BBox2D {
10033 x_min,
10034 y_min,
10035 x_max,
10036 y_max,
10037 })
10038 } else {
10039 None
10040 },
10041 has_erase_page: has_erase,
10042 });
10043 epoch_start = i;
10044 x_min = f64::INFINITY;
10045 x_max = f64::NEG_INFINITY;
10046 y_min = f64::INFINITY;
10047 y_max = f64::NEG_INFINITY;
10048 has_erase = false;
10049 }
10050 if matches!(element, DisplayElement::ErasePage) {
10051 has_erase = true;
10052 }
10053 if let Some(ref bbox) = bboxes[i] {
10054 x_min = x_min.min(bbox.x_min);
10055 x_max = x_max.max(bbox.x_max);
10056 y_min = y_min.min(bbox.y_min);
10057 y_max = y_max.max(bbox.y_max);
10058 }
10059 }
10060 if epoch_start < elements.len() {
10061 epochs.push(ViewportEpoch {
10062 start_idx: epoch_start,
10063 end_idx: elements.len(),
10064 paint_bbox: if x_min <= x_max {
10065 Some(BBox2D {
10066 x_min,
10067 y_min,
10068 x_max,
10069 y_max,
10070 })
10071 } else {
10072 None
10073 },
10074 has_erase_page: has_erase,
10075 });
10076 }
10077 epochs
10078}
10079
10080struct ViewportEpoch {
10082 start_idx: usize,
10083 end_idx: usize,
10084 paint_bbox: Option<BBox2D>,
10085 has_erase_page: bool,
10086}
10087
10088pub struct PreparedDisplayList {
10094 bboxes: Vec<Option<BBox2D>>,
10095 epochs: Vec<ViewportEpoch>,
10096 clip_seen: HashSet<u64>,
10097}
10098
10099pub fn prepare_display_list(list: &DisplayList) -> PreparedDisplayList {
10104 let bboxes = precompute_full_bboxes(list, 72.0);
10105 let epochs = build_viewport_epochs(list, &bboxes);
10106 let clip_seen = precompute_clip_seen(list);
10107 PreparedDisplayList {
10108 bboxes,
10109 epochs,
10110 clip_seen,
10111 }
10112}
10113
10114struct PreprocessedImage {
10119 data: Vec<u8>,
10121 width: u32,
10123 height: u32,
10124 adj_sx: f32,
10128 adj_ky: f32,
10129 adj_kx: f32,
10130 adj_sy: f32,
10131 quality: stet_tiny_skia::FilterQuality,
10133}
10134
10135pub struct ImageCache {
10141 entries: Vec<Option<Vec<u8>>>,
10143}
10144
10145impl ImageCache {
10146 pub fn build(list: &DisplayList, icc: Option<&IccCache>) -> Self {
10148 let entries = list
10149 .elements()
10150 .iter()
10151 .map(|elem| {
10152 if let DisplayElement::Image {
10153 sample_data,
10154 params,
10155 } = elem
10156 {
10157 if params.width == 0 || params.height == 0 {
10158 return None;
10159 }
10160 let mut rgba = samples_to_rgba(sample_data, params, icc, false);
10161 if params.mask_color.is_some() {
10162 apply_mask_color_rgba(&mut rgba, sample_data, params);
10163 }
10164 Some(rgba)
10165 } else {
10166 None
10167 }
10168 })
10169 .collect();
10170 Self { entries }
10171 }
10172
10173 pub fn get(&self, index: usize) -> Option<&[u8]> {
10175 self.entries.get(index).and_then(|e| e.as_deref())
10176 }
10177}
10178
10179fn preprocess_images_for_bands(
10184 list: &DisplayList,
10185 icc: Option<&IccCache>,
10186) -> Vec<Option<PreprocessedImage>> {
10187 list.elements()
10188 .iter()
10189 .map(|elem| {
10190 let DisplayElement::Image {
10191 sample_data,
10192 params,
10193 } = elem
10194 else {
10195 return None;
10196 };
10197 let iw = params.width;
10198 let ih = params.height;
10199 if iw == 0 || ih == 0 {
10200 return None;
10201 }
10202 if params.overprint {
10204 return None;
10205 }
10206
10207 let mut rgba = samples_to_rgba(sample_data, params, icc, false);
10209 if params.mask_color.is_some() {
10210 apply_mask_color_rgba(&mut rgba, sample_data, params);
10211 }
10212
10213 let image_inv = params.image_matrix.invert()?;
10215 let combined = params.ctm.concat(&image_inv);
10216 let base_transform = enforce_min_image_size(to_transform(&combined), iw, ih);
10217
10218 let (data, width, height, adj_t) =
10220 match prescale_image(&rgba, iw, ih, base_transform, params.interpolate) {
10221 Some((d, w, h, t)) => {
10222 drop(rgba); (d, w, h, t)
10224 }
10225 None => (rgba, iw, ih, base_transform),
10226 };
10227
10228 let quality = image_filter_quality(adj_t, params.interpolate);
10229
10230 Some(PreprocessedImage {
10231 data,
10232 width,
10233 height,
10234 adj_sx: adj_t.sx,
10235 adj_ky: adj_t.ky,
10236 adj_kx: adj_t.kx,
10237 adj_sy: adj_t.sy,
10238 quality,
10239 })
10240 })
10241 .collect()
10242}
10243
10244#[allow(clippy::too_many_arguments)]
10250pub fn render_region_prepared(
10251 list: &DisplayList,
10252 prepared: &PreparedDisplayList,
10253 vp_x: f64,
10254 vp_y: f64,
10255 vp_w: f64,
10256 vp_h: f64,
10257 pixel_w: u32,
10258 pixel_h: u32,
10259 dpi: f64,
10260 icc: Option<&IccCache>,
10261 image_cache: Option<&ImageCache>,
10262 no_aa: bool,
10263) -> Vec<u8> {
10264 if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
10265 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10266 }
10267
10268 let layer_set = LayerSet::new();
10269 let scale_x = pixel_w as f64 / vp_w;
10270 let scale_y = pixel_h as f64 / vp_h;
10271 let effective_dpi = dpi * scale_x;
10272
10273 const OVERLAP: u32 = 6;
10284 let render_h = pixel_h + 2 * OVERLAP;
10285 let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create viewport pixmap");
10286 pixmap.fill(Color::TRANSPARENT);
10288
10289 let cmyk_buf = if has_overprint_elements(list)
10290 || list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
10291 || has_cmyk_group(list)
10292 {
10293 Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
10294 } else {
10295 None
10296 };
10297
10298 let mut state = BandState {
10299 clip_region: None,
10300 spare_mask: None,
10301 clip_mask_cache: HashMap::new(),
10302 clip_mask_seen: prepared.clip_seen.clone(),
10303 mask_pool: Vec::new(),
10304 cmyk_buffer: cmyk_buf,
10305 op_bg_snapshot: None,
10306 op_touched: None,
10307 spot_mask: None,
10308 };
10309
10310 let elements = list.elements();
10311 let vp_x_f = vp_x as f32;
10312 let vp_y_f = vp_y as f32;
10313 let sx = scale_x as f32;
10314 let sy = scale_y as f32;
10315 let vp_x_max = vp_x + vp_w;
10316 let vp_y_max = vp_y + vp_h;
10317
10318 for epoch in &prepared.epochs {
10319 if !epoch.has_erase_page {
10320 match epoch.paint_bbox {
10321 Some(ref pb)
10322 if pb.x_max <= vp_x
10323 || pb.x_min >= vp_x_max
10324 || pb.y_max <= vp_y
10325 || pb.y_min >= vp_y_max =>
10326 {
10327 continue;
10328 }
10329 None => continue,
10330 _ => {}
10331 }
10332 }
10333
10334 #[allow(clippy::needless_range_loop)]
10335 for i in epoch.start_idx..epoch.end_idx {
10336 let force_process = matches!(
10339 &elements[i],
10340 DisplayElement::OcgGroup { elements: inner, .. }
10341 if contains_clip_op(inner)
10342 );
10343 if !force_process
10344 && let Some(ref bbox) = prepared.bboxes[i]
10345 && (bbox.x_max <= vp_x
10346 || bbox.x_min >= vp_x_max
10347 || bbox.y_max <= vp_y
10348 || bbox.y_min >= vp_y_max)
10349 {
10350 continue;
10351 }
10352 let ctx = RenderContext {
10353 vp_x: vp_x_f,
10354 vp_y: vp_y_f,
10355 scale_x: sx,
10356 scale_y: sy,
10357 out_w: pixel_w,
10358 out_h: render_h,
10359 effective_dpi,
10360 icc,
10361 image_cache,
10362 preprocessed: None,
10363 elem_idx: i,
10364 no_aa,
10365 opm_zero_transparent: false,
10366 knockout_painter_pass: KnockoutPainterPass::None,
10367 parent_group_isolated: false,
10368 alpha_extraction_pass: false,
10369 layer_set: &layer_set,
10370 };
10371 render_element(&mut pixmap, &mut state, &elements[i], &ctx);
10372 }
10373 }
10374
10375 composite_onto_white(pixmap.data_mut());
10377 let row_bytes = pixel_w as usize * 4;
10379 let end = pixel_h as usize * row_bytes;
10380 pixmap.data()[..end].to_vec()
10381}
10382
10383pub fn viewport_band_count(pixel_w: u32, pixel_h: u32) -> (u32, u32) {
10388 let band_h = select_band_height(pixel_w, pixel_h);
10389 let num_bands = if band_h >= pixel_h {
10390 1
10391 } else {
10392 pixel_h.div_ceil(band_h)
10393 };
10394 (num_bands, band_h)
10395}
10396
10397#[allow(clippy::too_many_arguments)]
10406pub fn render_region_single_band(
10407 list: &DisplayList,
10408 prepared: &PreparedDisplayList,
10409 vp_x: f64,
10410 vp_y: f64,
10411 vp_w: f64,
10412 vp_h: f64,
10413 pixel_w: u32,
10414 pixel_h: u32,
10415 band_idx: u32,
10416 band_h: u32,
10417 num_bands: u32,
10418 dpi: f64,
10419 icc: Option<&IccCache>,
10420 image_cache: Option<&ImageCache>,
10421 no_aa: bool,
10422) -> Vec<u8> {
10423 if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
10424 let actual_h = if band_idx < num_bands - 1 {
10425 band_h
10426 } else {
10427 pixel_h - band_idx * band_h
10428 };
10429 return vec![0xFF; pixel_w as usize * actual_h as usize * 4];
10430 }
10431
10432 let layer_set = LayerSet::new();
10433 let scale_x = pixel_w as f64 / vp_w;
10434 let scale_y = pixel_h as f64 / vp_h;
10435 let effective_dpi = dpi * scale_x;
10436
10437 let out_y_start = band_idx * band_h;
10439 let actual_h = if band_idx < num_bands - 1 {
10440 band_h
10441 } else {
10442 pixel_h - out_y_start
10443 };
10444
10445 const OVERLAP: u32 = 6;
10456 let render_y_start = out_y_start.saturating_sub(OVERLAP);
10457 let render_y_end = (out_y_start + actual_h + OVERLAP).min(pixel_h);
10458 let render_h = band_h + 2 * OVERLAP;
10459 let overlap_top = out_y_start - render_y_start;
10460
10461 let src_y_min = vp_y + render_y_start as f64 / scale_y;
10463 let src_y_max = vp_y + render_y_end as f64 / scale_y;
10464
10465 let band_vp_y = vp_y + render_y_start as f64 / scale_y;
10467
10468 let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create band pixmap");
10469 pixmap.fill(Color::TRANSPARENT);
10470
10471 let cmyk_buf = if has_overprint_elements(list)
10472 || list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
10473 || has_cmyk_group(list)
10474 {
10475 Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
10476 } else {
10477 None
10478 };
10479
10480 let mut state = BandState {
10481 clip_region: None,
10482 spare_mask: None,
10483 clip_mask_cache: HashMap::new(),
10484 clip_mask_seen: prepared.clip_seen.clone(),
10485 mask_pool: Vec::new(),
10486 cmyk_buffer: cmyk_buf,
10487 op_bg_snapshot: None,
10488 op_touched: None,
10489 spot_mask: None,
10490 };
10491
10492 let elements = list.elements();
10493 let vp_x_f = vp_x as f32;
10494 let band_vp_y_f = band_vp_y as f32;
10495 let sx = scale_x as f32;
10496 let sy = scale_y as f32;
10497 let vp_x_max = vp_x + vp_w;
10498
10499 for epoch in &prepared.epochs {
10500 if !epoch.has_erase_page {
10501 match epoch.paint_bbox {
10502 Some(ref pb)
10503 if pb.x_max <= vp_x
10504 || pb.x_min >= vp_x_max
10505 || pb.y_max <= src_y_min
10506 || pb.y_min >= src_y_max =>
10507 {
10508 continue;
10509 }
10510 None => continue,
10511 _ => {}
10512 }
10513 }
10514
10515 #[allow(clippy::needless_range_loop)]
10516 for i in epoch.start_idx..epoch.end_idx {
10517 let force_process = matches!(
10521 &elements[i],
10522 DisplayElement::OcgGroup { elements: inner, .. }
10523 if contains_clip_op(inner)
10524 );
10525 if !force_process
10526 && let Some(ref bbox) = prepared.bboxes[i]
10527 && (bbox.x_max <= vp_x
10528 || bbox.x_min >= vp_x_max
10529 || bbox.y_max <= src_y_min
10530 || bbox.y_min >= src_y_max)
10531 {
10532 continue;
10533 }
10534 let ctx = RenderContext {
10535 vp_x: vp_x_f,
10536 vp_y: band_vp_y_f,
10537 scale_x: sx,
10538 scale_y: sy,
10539 out_w: pixel_w,
10540 out_h: render_h,
10541 effective_dpi,
10542 icc,
10543 image_cache,
10544 preprocessed: None,
10545 elem_idx: i,
10546 no_aa,
10547 opm_zero_transparent: false,
10548 knockout_painter_pass: KnockoutPainterPass::None,
10549 parent_group_isolated: false,
10550 alpha_extraction_pass: false,
10551 layer_set: &layer_set,
10552 };
10553 render_element(&mut pixmap, &mut state, &elements[i], &ctx);
10554 }
10555 }
10556
10557 composite_onto_white(pixmap.data_mut());
10559
10560 let row_bytes = pixel_w as usize * 4;
10562 let start = overlap_top as usize * row_bytes;
10563 let end = start + actual_h as usize * row_bytes;
10564 pixmap.data()[start..end].to_vec()
10565}
10566
10567#[allow(clippy::too_many_arguments)]
10576pub fn render_region_prepared_parallel(
10577 list: &DisplayList,
10578 prepared: &PreparedDisplayList,
10579 vp_x: f64,
10580 vp_y: f64,
10581 vp_w: f64,
10582 vp_h: f64,
10583 pixel_w: u32,
10584 pixel_h: u32,
10585 dpi: f64,
10586 icc: Option<&IccCache>,
10587 image_cache: Option<&ImageCache>,
10588 no_aa: bool,
10589) -> Vec<u8> {
10590 let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
10591
10592 if num_bands <= 1 {
10593 return render_region_prepared(
10595 list,
10596 prepared,
10597 vp_x,
10598 vp_y,
10599 vp_w,
10600 vp_h,
10601 pixel_w,
10602 pixel_h,
10603 dpi,
10604 icc,
10605 image_cache,
10606 no_aa,
10607 );
10608 }
10609
10610 let render_band = |band_idx: u32| -> Vec<u8> {
10611 render_region_single_band(
10612 list,
10613 prepared,
10614 vp_x,
10615 vp_y,
10616 vp_w,
10617 vp_h,
10618 pixel_w,
10619 pixel_h,
10620 band_idx,
10621 band_h,
10622 num_bands,
10623 dpi,
10624 icc,
10625 image_cache,
10626 no_aa,
10627 )
10628 };
10629
10630 let row_bytes = pixel_w as usize * 4;
10631 let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
10632
10633 #[cfg(feature = "parallel")]
10634 {
10635 let chunk_size = rayon::current_num_threads().max(1);
10636
10637 for chunk_start in (0..num_bands).step_by(chunk_size) {
10638 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
10639
10640 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
10641 .into_par_iter()
10642 .map(&render_band)
10643 .collect();
10644
10645 for (i, band_data) in rendered.iter().enumerate() {
10646 let band_idx = chunk_start + i as u32;
10647 let y_start = (band_idx * band_h) as usize;
10648 let dest_start = y_start * row_bytes;
10649 let len = band_data.len();
10650 result[dest_start..dest_start + len].copy_from_slice(band_data);
10651 }
10652 }
10653 }
10654 #[cfg(not(feature = "parallel"))]
10655 {
10656 for band_idx in 0..num_bands {
10657 let band_data = render_band(band_idx);
10658 let y_start = (band_idx * band_h) as usize;
10659 let dest_start = y_start * row_bytes;
10660 let len = band_data.len();
10661 result[dest_start..dest_start + len].copy_from_slice(&band_data);
10662 }
10663 }
10664
10665 result
10666}
10667
10668#[allow(clippy::too_many_arguments)]
10673pub fn render_region_prepared_parallel_with_progress(
10674 list: &DisplayList,
10675 prepared: &PreparedDisplayList,
10676 vp_x: f64,
10677 vp_y: f64,
10678 vp_w: f64,
10679 vp_h: f64,
10680 pixel_w: u32,
10681 pixel_h: u32,
10682 dpi: f64,
10683 icc: Option<&IccCache>,
10684 image_cache: Option<&ImageCache>,
10685 no_aa: bool,
10686 progress: &std::sync::atomic::AtomicU32,
10687) -> Vec<u8> {
10688 let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
10689
10690 if num_bands <= 1 {
10691 let result = render_region_prepared(
10692 list,
10693 prepared,
10694 vp_x,
10695 vp_y,
10696 vp_w,
10697 vp_h,
10698 pixel_w,
10699 pixel_h,
10700 dpi,
10701 icc,
10702 image_cache,
10703 no_aa,
10704 );
10705 progress.store(1, std::sync::atomic::Ordering::Relaxed);
10706 return result;
10707 }
10708
10709 let render_band = |band_idx: u32| -> Vec<u8> {
10710 render_region_single_band(
10711 list,
10712 prepared,
10713 vp_x,
10714 vp_y,
10715 vp_w,
10716 vp_h,
10717 pixel_w,
10718 pixel_h,
10719 band_idx,
10720 band_h,
10721 num_bands,
10722 dpi,
10723 icc,
10724 image_cache,
10725 no_aa,
10726 )
10727 };
10728
10729 let row_bytes = pixel_w as usize * 4;
10730 let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
10731
10732 #[cfg(feature = "parallel")]
10733 {
10734 let chunk_size = rayon::current_num_threads().max(1);
10735
10736 for chunk_start in (0..num_bands).step_by(chunk_size) {
10737 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
10738
10739 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
10740 .into_par_iter()
10741 .map(&render_band)
10742 .collect();
10743
10744 for (i, band_data) in rendered.iter().enumerate() {
10745 let band_idx = chunk_start + i as u32;
10746 let y_start = (band_idx * band_h) as usize;
10747 let dest_start = y_start * row_bytes;
10748 let len = band_data.len();
10749 result[dest_start..dest_start + len].copy_from_slice(band_data);
10750 }
10751 progress.store(chunk_end, std::sync::atomic::Ordering::Relaxed);
10752 }
10753 }
10754 #[cfg(not(feature = "parallel"))]
10755 {
10756 for band_idx in 0..num_bands {
10757 let band_data = render_band(band_idx);
10758 let y_start = (band_idx * band_h) as usize;
10759 let dest_start = y_start * row_bytes;
10760 let len = band_data.len();
10761 result[dest_start..dest_start + len].copy_from_slice(&band_data);
10762 progress.store(band_idx + 1, std::sync::atomic::Ordering::Relaxed);
10763 }
10764 }
10765
10766 result
10767}
10768
10769#[allow(clippy::too_many_arguments)]
10772pub fn render_region_prepared_parallel_cancellable(
10773 list: &DisplayList,
10774 prepared: &PreparedDisplayList,
10775 vp_x: f64,
10776 vp_y: f64,
10777 vp_w: f64,
10778 vp_h: f64,
10779 pixel_w: u32,
10780 pixel_h: u32,
10781 dpi: f64,
10782 icc: Option<&IccCache>,
10783 image_cache: Option<&ImageCache>,
10784 no_aa: bool,
10785 cancelled: &std::sync::atomic::AtomicBool,
10786) -> Option<Vec<u8>> {
10787 if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
10788 return None;
10789 }
10790
10791 let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
10792
10793 if num_bands <= 1 {
10794 return Some(render_region_prepared(
10795 list,
10796 prepared,
10797 vp_x,
10798 vp_y,
10799 vp_w,
10800 vp_h,
10801 pixel_w,
10802 pixel_h,
10803 dpi,
10804 icc,
10805 image_cache,
10806 no_aa,
10807 ));
10808 }
10809
10810 let render_band = |band_idx: u32| -> Vec<u8> {
10811 render_region_single_band(
10812 list,
10813 prepared,
10814 vp_x,
10815 vp_y,
10816 vp_w,
10817 vp_h,
10818 pixel_w,
10819 pixel_h,
10820 band_idx,
10821 band_h,
10822 num_bands,
10823 dpi,
10824 icc,
10825 image_cache,
10826 no_aa,
10827 )
10828 };
10829
10830 let row_bytes = pixel_w as usize * 4;
10831 let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
10832
10833 #[cfg(feature = "parallel")]
10834 {
10835 let chunk_size = rayon::current_num_threads().max(1);
10836
10837 for chunk_start in (0..num_bands).step_by(chunk_size) {
10838 if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
10839 return None;
10840 }
10841 let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
10842
10843 let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
10844 .into_par_iter()
10845 .map(&render_band)
10846 .collect();
10847
10848 for (i, band_data) in rendered.iter().enumerate() {
10849 let band_idx = chunk_start + i as u32;
10850 let y_start = (band_idx * band_h) as usize;
10851 let dest_start = y_start * row_bytes;
10852 let len = band_data.len();
10853 result[dest_start..dest_start + len].copy_from_slice(band_data);
10854 }
10855 }
10856 }
10857 #[cfg(not(feature = "parallel"))]
10858 {
10859 for band_idx in 0..num_bands {
10860 if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
10861 return None;
10862 }
10863 let band_data = render_band(band_idx);
10864 let y_start = (band_idx * band_h) as usize;
10865 let dest_start = y_start * row_bytes;
10866 let len = band_data.len();
10867 result[dest_start..dest_start + len].copy_from_slice(&band_data);
10868 }
10869 }
10870
10871 Some(result)
10872}
10873
10874pub fn render_to_rgba(
10882 list: &DisplayList,
10883 pixel_w: u32,
10884 pixel_h: u32,
10885 dpi: f64,
10886 icc: Option<&IccCache>,
10887 no_aa: bool,
10888) -> Vec<u8> {
10889 render_to_rgba_with_layers(list, pixel_w, pixel_h, dpi, icc, no_aa, &LayerSet::new())
10890}
10891
10892#[allow(clippy::too_many_arguments)]
10899pub fn render_to_rgba_with_layers(
10900 list: &DisplayList,
10901 pixel_w: u32,
10902 pixel_h: u32,
10903 dpi: f64,
10904 icc: Option<&IccCache>,
10905 no_aa: bool,
10906 layer_set: &LayerSet,
10907) -> Vec<u8> {
10908 if pixel_w == 0 || pixel_h == 0 {
10909 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10910 }
10911
10912 let mut icc_cache = match icc {
10913 Some(c) => c.clone(),
10914 None => IccCache::new(),
10915 };
10916 register_shading_icc_profiles(list, &mut icc_cache);
10919
10920 let mut sink = MemorySink {
10921 data: Vec::new(),
10922 width: 0,
10923 };
10924
10925 let band_h = select_band_height(pixel_w, pixel_h);
10926 if let Err(e) = render_banded_to_sink(
10927 pixel_w, pixel_h, band_h, dpi, list, &mut sink, &icc_cache, no_aa, layer_set,
10928 ) {
10929 eprintln!("render_to_rgba: banded render failed: {e}");
10930 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10931 }
10932
10933 sink.data
10934}
10935
10936pub fn render_to_rgba_viewport(
10949 list: &DisplayList,
10950 pixel_w: u32,
10951 pixel_h: u32,
10952 dpi: f64,
10953 icc: Option<&IccCache>,
10954 no_aa: bool,
10955) -> Vec<u8> {
10956 if pixel_w == 0 || pixel_h == 0 {
10957 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
10958 }
10959
10960 let mut icc_cache = match icc {
10961 Some(c) => c.clone(),
10962 None => IccCache::new(),
10963 };
10964 register_shading_icc_profiles(list, &mut icc_cache);
10965
10966 let prepared = prepare_display_list(list);
10967 render_region_prepared_parallel(
10968 list,
10969 &prepared,
10970 0.0,
10971 0.0,
10972 pixel_w as f64,
10973 pixel_h as f64,
10974 pixel_w,
10975 pixel_h,
10976 dpi,
10977 Some(&icc_cache),
10978 None,
10979 no_aa,
10980 )
10981}
10982
10983fn debug_bbox_lines(list: &DisplayList, dpi: f64, depth: usize, out: &mut Vec<String>) {
10990 let y_bboxes = precompute_bboxes(list, dpi);
10991 let full_bboxes = precompute_full_bboxes(list, dpi);
10992 let elements = list.elements();
10993 let indent = " ".repeat(depth);
10994 for (i, elem) in elements.iter().enumerate() {
10995 let kind = match elem {
10996 DisplayElement::Fill { .. } => "Fill",
10997 DisplayElement::Stroke { .. } => "Stroke",
10998 DisplayElement::Image { .. } => "Image",
10999 DisplayElement::AxialShading { .. } => "AxialShading",
11000 DisplayElement::RadialShading { .. } => "RadialShading",
11001 DisplayElement::MeshShading { .. } => "MeshShading",
11002 DisplayElement::PatchShading { .. } => "PatchShading",
11003 DisplayElement::PatternFill { .. } => "PatternFill",
11004 DisplayElement::Group { .. } => "Group",
11005 DisplayElement::SoftMasked { .. } => "SoftMasked",
11006 DisplayElement::OcgGroup { .. } => "OcgGroup",
11007 DisplayElement::Clip { .. } => "Clip",
11008 DisplayElement::InitClip => "InitClip",
11009 DisplayElement::ErasePage => "ErasePage",
11010 DisplayElement::Text { .. } => "Text",
11011 _ => "Unknown",
11012 };
11013 let yb = &y_bboxes[i];
11014 let fb = &full_bboxes[i];
11015 let mut diff = false;
11016 if yb.is_some() != fb.is_some() {
11017 diff = true;
11018 }
11019 if let (Some(yb), Some(fb)) = (yb, fb)
11020 && ((yb.y_min - fb.y_min).abs() > 1e-9 || (yb.y_max - fb.y_max).abs() > 1e-9)
11021 {
11022 diff = true;
11023 }
11024 let yb_s = match yb {
11025 Some(b) => format!("Y[{:8.3}..{:8.3}]", b.y_min, b.y_max),
11026 None => "Y[None]".to_string(),
11027 };
11028 let fb_s = match fb {
11029 Some(b) => format!(
11030 "2D[x {:8.3}..{:8.3} y {:8.3}..{:8.3}]",
11031 b.x_min, b.x_max, b.y_min, b.y_max
11032 ),
11033 None => "2D[None]".to_string(),
11034 };
11035 out.push(format!(
11036 "{}{:4} {:15} {:30} {:55} {}",
11037 indent,
11038 i,
11039 kind,
11040 yb_s,
11041 fb_s,
11042 if diff { "DIFF" } else { "" }
11043 ));
11044 if let DisplayElement::Stroke { path, params } = elem {
11045 let rp = path_full_bbox(path);
11046 let m = ¶ms.ctm;
11047 out.push(format!(
11048 "{} ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] lw={:.4} miter={:.4} raw={}",
11049 indent,
11050 m.a,
11051 m.b,
11052 m.c,
11053 m.d,
11054 m.tx,
11055 m.ty,
11056 params.line_width,
11057 params.miter_limit,
11058 match rp {
11059 Some(b) => format!(
11060 "x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
11061 b.x_min, b.x_max, b.y_min, b.y_max
11062 ),
11063 None => "None".to_string(),
11064 }
11065 ));
11066 }
11067 if let DisplayElement::Clip { path, params } = elem {
11068 let rp = path_full_bbox(path);
11069 let m = ¶ms.ctm;
11070 out.push(format!(
11071 "{} clip ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] rule={:?} raw={}",
11072 indent,
11073 m.a,
11074 m.b,
11075 m.c,
11076 m.d,
11077 m.tx,
11078 m.ty,
11079 params.fill_rule,
11080 match rp {
11081 Some(b) => format!(
11082 "x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
11083 b.x_min, b.x_max, b.y_min, b.y_max
11084 ),
11085 None => "None".to_string(),
11086 }
11087 ));
11088 }
11089 if let DisplayElement::PatchShading { params } = elem {
11090 out.push(format!(
11091 "{} patch ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] bbox={:?} patches={}",
11092 indent,
11093 params.ctm.a,
11094 params.ctm.b,
11095 params.ctm.c,
11096 params.ctm.d,
11097 params.ctm.tx,
11098 params.ctm.ty,
11099 params.bbox,
11100 params.patches.len()
11101 ));
11102 if !params.patches.is_empty() {
11103 let patch = ¶ms.patches[0];
11104 let mut x_min = f64::INFINITY;
11106 let mut y_min = f64::INFINITY;
11107 let mut x_max = f64::NEG_INFINITY;
11108 let mut y_max = f64::NEG_INFINITY;
11109 for &(px, py) in &patch.points {
11110 let (dx, dy) = params.ctm.transform_point(px, py);
11111 x_min = x_min.min(dx);
11112 y_min = y_min.min(dy);
11113 x_max = x_max.max(dx);
11114 y_max = y_max.max(dy);
11115 }
11116 out.push(format!(
11117 "{} patch[0] pts={} dev x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
11118 indent,
11119 patch.points.len(),
11120 x_min,
11121 x_max,
11122 y_min,
11123 y_max
11124 ));
11125 }
11126 }
11127 if let DisplayElement::Group {
11128 elements: inner,
11129 params,
11130 } = elem
11131 {
11132 out.push(format!(
11133 "{} group bbox={:?} iso={} ko={} alpha={} bm={} cs={:?}",
11134 indent,
11135 params.bbox,
11136 params.isolated,
11137 params.knockout,
11138 params.alpha,
11139 params.blend_mode,
11140 params.color_space
11141 ));
11142 debug_bbox_lines(inner, dpi, depth + 1, out);
11143 }
11144 if let DisplayElement::SoftMasked {
11145 content, params, ..
11146 } = elem
11147 {
11148 out.push(format!(
11149 "{} softmasked bbox={:?}",
11150 indent, params.bbox
11151 ));
11152 debug_bbox_lines(content, dpi, depth + 1, out);
11153 }
11154 if let DisplayElement::OcgGroup {
11155 elements: inner,
11156 visibility,
11157 } = elem
11158 {
11159 out.push(format!(
11160 "{} ocg default_visible={}",
11161 indent,
11162 visibility.default_visible()
11163 ));
11164 debug_bbox_lines(inner, dpi, depth + 1, out);
11165 }
11166 }
11167}
11168
11169pub fn debug_bbox_comparison(list: &DisplayList, dpi: f64) -> Vec<String> {
11170 let mut out = Vec::new();
11171 debug_bbox_lines(list, dpi, 0, &mut out);
11172 out
11173}
11174
11175struct MemorySink {
11177 data: Vec<u8>,
11178 width: u32,
11179}
11180
11181impl stet_graphics::device::PageSink for MemorySink {
11182 fn begin_page(&mut self, width: u32, height: u32) -> Result<(), String> {
11183 self.width = width;
11184 self.data.reserve(width as usize * height as usize * 4);
11185 Ok(())
11186 }
11187
11188 fn write_rows(&mut self, rgba_rows: &[u8], _num_rows: u32) -> Result<(), String> {
11189 self.data.extend_from_slice(rgba_rows);
11190 Ok(())
11191 }
11192
11193 fn end_page(&mut self) -> Result<(), String> {
11194 Ok(())
11195 }
11196}
11197
11198#[allow(clippy::too_many_arguments)]
11207pub fn render_region(
11208 list: &DisplayList,
11209 vp_x: f64,
11210 vp_y: f64,
11211 vp_w: f64,
11212 vp_h: f64,
11213 pixel_w: u32,
11214 pixel_h: u32,
11215 dpi: f64,
11216 icc: Option<&IccCache>,
11217 image_cache: Option<&ImageCache>,
11218 no_aa: bool,
11219) -> Vec<u8> {
11220 if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
11221 return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
11222 }
11223
11224 let layer_set = LayerSet::new();
11225 let scale_x = pixel_w as f64 / vp_w;
11226 let scale_y = pixel_h as f64 / vp_h;
11227 let effective_dpi = dpi * scale_x;
11229
11230 let bboxes = precompute_full_bboxes(list, effective_dpi);
11231 let epochs = build_viewport_epochs(list, &bboxes);
11232 let clip_seen = precompute_clip_seen(list);
11233
11234 const OVERLAP: u32 = 6;
11238 let render_h = pixel_h + 2 * OVERLAP;
11239
11240 let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create viewport pixmap");
11241 pixmap.fill(Color::TRANSPARENT);
11242
11243 let cmyk_buf = if has_overprint_elements(list)
11244 || list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
11245 || has_cmyk_group(list)
11246 {
11247 Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
11248 } else {
11249 None
11250 };
11251
11252 let mut state = BandState {
11253 clip_region: None,
11254 spare_mask: None,
11255 clip_mask_cache: HashMap::new(),
11256 clip_mask_seen: clip_seen,
11257 mask_pool: Vec::new(),
11258 cmyk_buffer: cmyk_buf,
11259 op_bg_snapshot: None,
11260 op_touched: None,
11261 spot_mask: None,
11262 };
11263
11264 let elements = list.elements();
11265 let vp_x_f = vp_x as f32;
11266 let vp_y_f = vp_y as f32;
11267 let sx = scale_x as f32;
11268 let sy = scale_y as f32;
11269 let vp_x_max = vp_x + vp_w;
11270 let vp_y_max = vp_y + vp_h;
11271
11272 for epoch in &epochs {
11273 if !epoch.has_erase_page {
11275 match epoch.paint_bbox {
11276 Some(ref pb)
11277 if pb.x_max <= vp_x
11278 || pb.x_min >= vp_x_max
11279 || pb.y_max <= vp_y
11280 || pb.y_min >= vp_y_max =>
11281 {
11282 continue;
11283 }
11284 None => continue,
11285 _ => {}
11286 }
11287 }
11288
11289 for i in epoch.start_idx..epoch.end_idx {
11290 let force_process = matches!(
11293 &elements[i],
11294 DisplayElement::OcgGroup { elements: inner, .. }
11295 if contains_clip_op(inner)
11296 );
11297 if !force_process
11299 && let Some(ref bbox) = bboxes[i]
11300 && (bbox.x_max <= vp_x
11301 || bbox.x_min >= vp_x_max
11302 || bbox.y_max <= vp_y
11303 || bbox.y_min >= vp_y_max)
11304 {
11305 continue;
11306 }
11307 let ctx = RenderContext {
11308 vp_x: vp_x_f,
11309 vp_y: vp_y_f,
11310 scale_x: sx,
11311 scale_y: sy,
11312 out_w: pixel_w,
11313 out_h: render_h,
11314 effective_dpi,
11315 icc,
11316 image_cache,
11317 preprocessed: None,
11318 elem_idx: i,
11319 no_aa,
11320 opm_zero_transparent: false,
11321 knockout_painter_pass: KnockoutPainterPass::None,
11322 parent_group_isolated: false,
11323 alpha_extraction_pass: false,
11324 layer_set: &layer_set,
11325 };
11326 render_element(&mut pixmap, &mut state, &elements[i], &ctx);
11327 }
11328 }
11329
11330 composite_onto_white(pixmap.data_mut());
11331 let row_bytes = pixel_w as usize * 4;
11333 let end = pixel_h as usize * row_bytes;
11334 pixmap.data()[..end].to_vec()
11335}
11336fn copy_backdrop_crop(
11338 parent: &Pixmap,
11339 crop_x: i32,
11340 crop_y: i32,
11341 crop_w: u32,
11342 crop_h: u32,
11343) -> Vec<u8> {
11344 let pw = parent.width() as usize;
11345 let src = parent.data();
11346 let cw = crop_w as usize;
11347 let ch = crop_h as usize;
11348 let cx = crop_x as usize;
11349 let cy = crop_y as usize;
11350 let mut backdrop = vec![0u8; cw * ch * 4];
11351 for row in 0..ch {
11352 let src_off = ((cy + row) * pw + cx) * 4;
11353 let dst_off = row * cw * 4;
11354 backdrop[dst_off..dst_off + cw * 4].copy_from_slice(&src[src_off..src_off + cw * 4]);
11355 }
11356 backdrop
11357}
11358fn clip_polygon_halfplane(
11363 poly: &[(f32, f32)],
11364 nx: f32,
11365 ny: f32,
11366 px: f32,
11367 py: f32,
11368) -> Vec<(f32, f32)> {
11369 if poly.is_empty() {
11370 return vec![];
11371 }
11372 let dot = |x: f32, y: f32| nx * (x - px) + ny * (y - py);
11373 let mut out = Vec::with_capacity(poly.len() + 1);
11374 let n = poly.len();
11375 for i in 0..n {
11376 let (ax, ay) = poly[i];
11377 let (bx, by) = poly[(i + 1) % n];
11378 let da = dot(ax, ay);
11379 let db = dot(bx, by);
11380 if da >= 0.0 {
11381 out.push((ax, ay));
11382 }
11383 if (da >= 0.0) != (db >= 0.0) {
11384 let t = da / (da - db);
11386 out.push((ax + t * (bx - ax), ay + t * (by - ay)));
11387 }
11388 }
11389 out
11390}
11391
11392#[allow(clippy::too_many_arguments)]
11394fn render_axial_shading(
11395 pixmap: &mut Pixmap,
11396 params: &AxialShadingParams,
11397 vp_x: f32,
11398 vp_y: f32,
11399 scale_x: f32,
11400 scale_y: f32,
11401 clip_mask: Option<&Mask>,
11402 no_aa: bool,
11403 cmyk_buf: Option<&mut [f32]>,
11404 icc: Option<&IccCache>,
11405) {
11406 let pw = pixmap.width();
11407 let ph = pixmap.height();
11408 if params.color_stops.is_empty() || pw == 0 || ph == 0 {
11409 return;
11410 }
11411
11412 let (mut rx_min, mut ry_min, mut rx_max, mut ry_max) = if let Some(bbox) = ¶ms.bbox {
11413 let corners = [
11414 params.ctm.transform_point(bbox[0], bbox[1]),
11415 params.ctm.transform_point(bbox[2], bbox[1]),
11416 params.ctm.transform_point(bbox[0], bbox[3]),
11417 params.ctm.transform_point(bbox[2], bbox[3]),
11418 ];
11419 let x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
11420 let y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
11421 let x_max = corners
11422 .iter()
11423 .map(|c| c.0)
11424 .fold(f64::NEG_INFINITY, f64::max);
11425 let y_max = corners
11426 .iter()
11427 .map(|c| c.1)
11428 .fold(f64::NEG_INFINITY, f64::max);
11429 (
11430 ((x_min as f32 - vp_x) * scale_x).max(0.0),
11431 ((y_min as f32 - vp_y) * scale_y).max(0.0),
11432 ((x_max as f32 - vp_x) * scale_x).min(pw as f32),
11433 ((y_max as f32 - vp_y) * scale_y).min(ph as f32),
11434 )
11435 } else {
11436 (0.0, 0.0, pw as f32, ph as f32)
11437 };
11438
11439 if rx_max <= rx_min || ry_max <= ry_min {
11440 return;
11441 }
11442
11443 let (dx0, dy0) = params.ctm.transform_point(params.x0, params.y0);
11445 let (dx1, dy1) = params.ctm.transform_point(params.x1, params.y1);
11446
11447 let needs_perpendicular_clip = (!params.extend_start || !params.extend_end) && {
11451 let axis_x = dx1 - dx0;
11452 let axis_y = dy1 - dy0;
11453 axis_x.abs() > 1e-6 && axis_y.abs() > 1e-6
11454 };
11455
11456 let bbox_is_rotated =
11459 params.bbox.is_some() && (params.ctm.b.abs() > 1e-10 || params.ctm.c.abs() > 1e-10);
11460
11461 if needs_perpendicular_clip {
11462 let stops = build_gradient_stops(¶ms.color_stops);
11465 if stops.is_empty() {
11466 return;
11467 }
11468 let start = stet_tiny_skia::Point::from_xy(params.x0 as f32, params.y0 as f32);
11469 let end = stet_tiny_skia::Point::from_xy(params.x1 as f32, params.y1 as f32);
11470 let gradient_transform =
11471 viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
11472 let Some(gradient) = stet_tiny_skia::LinearGradient::new(
11473 start,
11474 end,
11475 stops,
11476 stet_tiny_skia::SpreadMode::Pad,
11477 gradient_transform,
11478 ) else {
11479 return;
11480 };
11481 let paint = Paint {
11482 shader: gradient,
11483 anti_alias: !no_aa,
11484 ..Paint::default()
11485 };
11486
11487 let mut poly: Vec<(f32, f32)> = if bbox_is_rotated {
11489 let bbox = params.bbox.as_ref().unwrap();
11490 let corners = [
11491 params.ctm.transform_point(bbox[0], bbox[1]),
11492 params.ctm.transform_point(bbox[2], bbox[1]),
11493 params.ctm.transform_point(bbox[2], bbox[3]),
11494 params.ctm.transform_point(bbox[0], bbox[3]),
11495 ];
11496 corners
11497 .iter()
11498 .map(|(x, y)| ((*x as f32 - vp_x) * scale_x, (*y as f32 - vp_y) * scale_y))
11499 .collect()
11500 } else {
11501 vec![
11502 (rx_min, ry_min),
11503 (rx_max, ry_min),
11504 (rx_max, ry_max),
11505 (rx_min, ry_max),
11506 ]
11507 };
11508 let ax = (dx1 - dx0) as f32 * scale_x;
11509 let ay = (dy1 - dy0) as f32 * scale_y;
11510 if !params.extend_start {
11511 let px = (dx0 as f32 - vp_x) * scale_x;
11512 let py = (dy0 as f32 - vp_y) * scale_y;
11513 poly = clip_polygon_halfplane(&poly, ax, ay, px, py);
11514 }
11515 if !params.extend_end {
11516 let px = (dx1 as f32 - vp_x) * scale_x;
11517 let py = (dy1 as f32 - vp_y) * scale_y;
11518 poly = clip_polygon_halfplane(&poly, -ax, -ay, px, py);
11519 }
11520 if poly.len() >= 3 {
11521 let mut pb = PathBuilder::new();
11522 pb.move_to(poly[0].0, poly[0].1);
11523 for &(x, y) in &poly[1..] {
11524 pb.line_to(x, y);
11525 }
11526 pb.close();
11527 if let Some(path) = pb.finish() {
11528 pixmap.fill_path(
11529 &path,
11530 &paint,
11531 SkiaFillRule::Winding,
11532 Transform::identity(),
11533 clip_mask,
11534 );
11535 }
11536 }
11537 } else {
11538 if !params.extend_start || !params.extend_end {
11541 let axis_x = dx1 - dx0;
11542 let axis_y = dy1 - dy0;
11543 let gx0 = (dx0 as f32 - vp_x) * scale_x;
11544 let gy0 = (dy0 as f32 - vp_y) * scale_y;
11545 let gx1 = (dx1 as f32 - vp_x) * scale_x;
11546 let gy1 = (dy1 as f32 - vp_y) * scale_y;
11547
11548 if axis_x.abs() >= axis_y.abs() {
11549 if !params.extend_start {
11550 if axis_x >= 0.0 {
11551 rx_min = rx_min.max(gx0);
11552 } else {
11553 rx_max = rx_max.min(gx0);
11554 }
11555 }
11556 if !params.extend_end {
11557 if axis_x >= 0.0 {
11558 rx_max = rx_max.min(gx1);
11559 } else {
11560 rx_min = rx_min.max(gx1);
11561 }
11562 }
11563 } else {
11564 if !params.extend_start {
11565 if axis_y >= 0.0 {
11566 ry_min = ry_min.max(gy0);
11567 } else {
11568 ry_max = ry_max.min(gy0);
11569 }
11570 }
11571 if !params.extend_end {
11572 if axis_y >= 0.0 {
11573 ry_max = ry_max.min(gy1);
11574 } else {
11575 ry_min = ry_min.max(gy1);
11576 }
11577 }
11578 }
11579 if rx_max <= rx_min || ry_max <= ry_min {
11580 return;
11581 }
11582 }
11583
11584 let ax = params.x1 - params.x0;
11586 let ay = params.y1 - params.y0;
11587 let axis_sq = ax * ax + ay * ay;
11588 if axis_sq < 1e-20 {
11589 return;
11590 }
11591
11592 let pixel_dx = (dx1 - dx0) * scale_x as f64;
11596 let pixel_dy = (dy1 - dy0) * scale_y as f64;
11597 let pixel_axis_len = (pixel_dx * pixel_dx + pixel_dy * pixel_dy).sqrt();
11598 let lut_size = (pixel_axis_len as usize)
11599 .max(params.color_stops.len())
11600 .max(256)
11601 .min(16384);
11602 let lut = build_gradient_lut(¶ms.color_stops, lut_size);
11603
11604 let Some(inv) = params.ctm.invert() else {
11605 return;
11606 };
11607 let inv_sx = 1.0 / scale_x as f64;
11608 let inv_sy = 1.0 / scale_y as f64;
11609 let dev_origin_x = vp_x as f64;
11610 let dev_origin_y = vp_y as f64;
11611
11612 let sx_base = inv.a * dev_origin_x + inv.c * dev_origin_y + inv.tx;
11616 let sy_base = inv.b * dev_origin_x + inv.d * dev_origin_y + inv.ty;
11617 let dsx_dx = inv.a * inv_sx;
11618 let dsx_dy = inv.c * inv_sy;
11619 let dsy_dx = inv.b * inv_sx;
11620 let dsy_dy = inv.d * inv_sy;
11621
11622 let inv_axis_sq = 1.0 / axis_sq;
11624 let t_origin = ((sx_base - params.x0) * ax + (sy_base - params.y0) * ay) * inv_axis_sq;
11625 let dt_dx = (dsx_dx * ax + dsy_dx * ay) * inv_axis_sq;
11626 let dt_dy = (dsx_dy * ax + dsy_dy * ay) * inv_axis_sq;
11627
11628 let bbox_pixel_clip = if bbox_is_rotated {
11631 let bbox = params.bbox.as_ref().unwrap();
11632 let (bx0, bx1) = (bbox[0].min(bbox[2]), bbox[0].max(bbox[2]));
11633 let (by0, by1) = (bbox[1].min(bbox[3]), bbox[1].max(bbox[3]));
11634 Some((
11635 dsx_dx, dsx_dy, sx_base, dsy_dx, dsy_dy, sy_base, bx0, by0, bx1, by1,
11636 ))
11637 } else {
11638 None
11639 };
11640
11641 let ix_min = rx_min.floor() as u32;
11642 let ix_max = rx_max.ceil().min(pw as f32) as u32;
11643 let iy_min = ry_min.floor() as u32;
11644 let iy_max = ry_max.ceil().min(ph as f32) as u32;
11645
11646 let stride = pw as usize * 4;
11647 let data = pixmap.data_mut();
11648 let mask_data = clip_mask.map(|m| m.data());
11649 let alpha = (params.alpha.clamp(0.0, 1.0) * 255.0 + 0.5) as u16;
11650
11651 for py in iy_min..iy_max {
11652 let t_row = t_origin + dt_dy * py as f64;
11653 let row_offset = py as usize * stride;
11654
11655 let (ux_row, uy_row) =
11657 if let Some((_, dux_dy, ux_base, _, duy_dy, uy_base, ..)) = &bbox_pixel_clip {
11658 (ux_base + dux_dy * py as f64, uy_base + duy_dy * py as f64)
11659 } else {
11660 (0.0, 0.0)
11661 };
11662
11663 for px in ix_min..ix_max {
11664 if let Some(md) = mask_data {
11666 if md[py as usize * pw as usize + px as usize] == 0 {
11667 continue;
11668 }
11669 }
11670
11671 if let Some((dux_dx, _, _, duy_dx, _, _, bx0, by0, bx1, by1)) = &bbox_pixel_clip {
11673 let ux = ux_row + dux_dx * px as f64;
11674 let uy = uy_row + duy_dx * px as f64;
11675 if ux < *bx0 || ux > *bx1 || uy < *by0 || uy > *by1 {
11676 continue;
11677 }
11678 }
11679
11680 let t = t_row + dt_dx * px as f64;
11681 let t_clamped = t.clamp(0.0, 1.0);
11682 let idx = (t_clamped * (lut_size - 1) as f64 + 0.5) as usize;
11683 let [r, g, b, _] = lut[idx.min(lut_size - 1)];
11684
11685 let offset = row_offset + px as usize * 4;
11686 if alpha >= 255 {
11687 data[offset] = r;
11688 data[offset + 1] = g;
11689 data[offset + 2] = b;
11690 data[offset + 3] = 255;
11691 } else {
11692 let a = alpha as u16;
11694 let inv_a = 255 - a;
11695 data[offset] = ((r as u16 * a + data[offset] as u16 * inv_a + 127) / 255) as u8;
11696 data[offset + 1] =
11697 ((g as u16 * a + data[offset + 1] as u16 * inv_a + 127) / 255) as u8;
11698 data[offset + 2] =
11699 ((b as u16 * a + data[offset + 2] as u16 * inv_a + 127) / 255) as u8;
11700 data[offset + 3] = ((a + data[offset + 3] as u16 * inv_a / 255).min(255)) as u8;
11701 }
11702 }
11703 }
11704 }
11705
11706 if let Some(buf) = cmyk_buf {
11708 let pw = pixmap.width();
11709 let inv_sx = 1.0 / scale_x as f64;
11710 let inv_sy = 1.0 / scale_y as f64;
11711 let axis_x = params.x1 - params.x0;
11712 let axis_y = params.y1 - params.y0;
11713 let axis_len_sq = axis_x * axis_x + axis_y * axis_y;
11714 let Some(inv_ctm) = params.ctm.invert() else {
11715 return;
11716 };
11717
11718 let iy_min = ry_min.floor() as u32;
11719 let iy_max = ry_max.ceil().min(pixmap.height() as f32) as u32;
11720 let ix_min = rx_min.floor() as u32;
11721 let ix_max = rx_max.ceil().min(pw as f32) as u32;
11722
11723 for py in iy_min..iy_max {
11724 let dev_y = py as f64 * inv_sy + vp_y as f64;
11725 for px in ix_min..ix_max {
11726 let dev_x = px as f64 * inv_sx + vp_x as f64;
11727 let (ux, uy) = inv_ctm.transform_point(dev_x, dev_y);
11728 let t = if axis_len_sq > 1e-10 {
11729 ((ux - params.x0) * axis_x + (uy - params.y0) * axis_y) / axis_len_sq
11730 } else {
11731 0.0
11732 };
11733 if t < 0.0 && !params.extend_start {
11734 continue;
11735 }
11736 if t > 1.0 && !params.extend_end {
11737 continue;
11738 }
11739 let clamped = t.clamp(0.0, 1.0);
11740
11741 if let Some(mask) = clip_mask {
11742 let mi = py as usize * pw as usize + px as usize;
11743 if mask.data()[mi] == 0 {
11744 continue;
11745 }
11746 }
11747
11748 let color = interpolate_color_stops(¶ms.color_stops, clamped);
11749 let cmyk = interpolate_cmyk_from_stops(
11750 ¶ms.color_stops,
11751 ¶ms.color_space,
11752 clamped,
11753 &color,
11754 icc,
11755 );
11756 let ci = (py as usize * pw as usize + px as usize) * 4;
11757 if ci + 3 < buf.len() {
11758 if params.spot_tint_blend && params.overprint {
11759 let cur_c = buf[ci] as f64;
11792 let cur_m = buf[ci + 1] as f64;
11793 let cur_y = buf[ci + 2] as f64;
11794 let cur_k = buf[ci + 3] as f64;
11795 let cur_is_zero =
11796 cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
11797 let named = params.painted_channels;
11798 if cur_is_zero {
11799 if named & stet_graphics::device::CMYK_C != 0 {
11800 buf[ci] = cmyk.0 as f32;
11801 }
11802 if named & stet_graphics::device::CMYK_M != 0 {
11803 buf[ci + 1] = cmyk.1 as f32;
11804 }
11805 if named & stet_graphics::device::CMYK_Y != 0 {
11806 buf[ci + 2] = cmyk.2 as f32;
11807 }
11808 if named & stet_graphics::device::CMYK_K != 0 {
11809 buf[ci + 3] = cmyk.3 as f32;
11810 }
11811 } else {
11812 let new_c = if named & stet_graphics::device::CMYK_C != 0 {
11813 cmyk.0
11814 } else {
11815 cur_c
11816 };
11817 let new_m = if named & stet_graphics::device::CMYK_M != 0 {
11818 cmyk.1
11819 } else {
11820 cur_m
11821 };
11822 let new_y = if named & stet_graphics::device::CMYK_Y != 0 {
11823 cmyk.2
11824 } else {
11825 cur_y
11826 };
11827 let new_k = if named & stet_graphics::device::CMYK_K != 0 {
11828 cmyk.3
11829 } else {
11830 cur_k
11831 };
11832 buf[ci] = new_c as f32;
11833 buf[ci + 1] = new_m as f32;
11834 buf[ci + 2] = new_y as f32;
11835 buf[ci + 3] = new_k as f32;
11836 let (rv, gv, bv) = if let Some(icc_cache) = icc {
11837 icc_cache
11838 .convert_cmyk_readonly(new_c, new_m, new_y, new_k)
11839 .unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
11840 } else {
11841 cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
11842 };
11843 let stride = pixmap.data().len() / pixmap.height() as usize;
11844 let offset = py as usize * stride + px as usize * 4;
11845 let data = pixmap.data_mut();
11846 data[offset] = (rv * 255.0).round().clamp(0.0, 255.0) as u8;
11847 data[offset + 1] = (gv * 255.0).round().clamp(0.0, 255.0) as u8;
11848 data[offset + 2] = (bv * 255.0).round().clamp(0.0, 255.0) as u8;
11849 }
11850 } else if params.overprint
11851 && params.painted_channels != stet_graphics::device::CMYK_ALL
11852 {
11853 if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
11854 buf[ci] = cmyk.0 as f32;
11855 }
11856 if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
11857 buf[ci + 1] = cmyk.1 as f32;
11858 }
11859 if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
11860 buf[ci + 2] = cmyk.2 as f32;
11861 }
11862 if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
11863 buf[ci + 3] = cmyk.3 as f32;
11864 }
11865 let c = buf[ci] as f64;
11867 let m = buf[ci + 1] as f64;
11868 let y = buf[ci + 2] as f64;
11869 let k = buf[ci + 3] as f64;
11870 let (rv, gv, bv) = if let Some(icc_cache) = icc {
11871 icc_cache
11872 .convert_cmyk_readonly(c, m, y, k)
11873 .unwrap_or_else(|| cmyk_to_rgb_plrm(c, m, y, k))
11874 } else {
11875 cmyk_to_rgb_plrm(c, m, y, k)
11876 };
11877 let stride = pixmap.data().len() / pixmap.height() as usize;
11878 let offset = py as usize * stride + px as usize * 4;
11879 let data = pixmap.data_mut();
11880 data[offset] = (rv * 255.0).round().clamp(0.0, 255.0) as u8;
11881 data[offset + 1] = (gv * 255.0).round().clamp(0.0, 255.0) as u8;
11882 data[offset + 2] = (bv * 255.0).round().clamp(0.0, 255.0) as u8;
11883 } else {
11884 buf[ci] = cmyk.0 as f32;
11901 buf[ci + 1] = cmyk.1 as f32;
11902 buf[ci + 2] = cmyk.2 as f32;
11903 buf[ci + 3] = cmyk.3 as f32;
11904 }
11905 }
11906 }
11907 }
11908 }
11909}
11910
11911#[allow(clippy::too_many_arguments)]
11913fn render_radial_shading(
11914 pixmap: &mut Pixmap,
11915 params: &RadialShadingParams,
11916 vp_x: f32,
11917 vp_y: f32,
11918 scale_x: f32,
11919 scale_y: f32,
11920 clip_mask: Option<&Mask>,
11921 _no_aa: bool,
11922 mut cmyk_buf: Option<&mut [f32]>,
11923 icc: Option<&IccCache>,
11924) {
11925 let pw = pixmap.width();
11926 let ph = pixmap.height();
11927 if params.color_stops.is_empty() || pw == 0 || ph == 0 {
11928 return;
11929 }
11930
11931 let Some(inv_ctm) = params.ctm.invert() else {
11932 return;
11933 };
11934
11935 let (px_min, py_min, px_max, py_max) = if let Some(bbox) = ¶ms.bbox {
11936 let corners = [
11937 params.ctm.transform_point(bbox[0], bbox[1]),
11938 params.ctm.transform_point(bbox[2], bbox[1]),
11939 params.ctm.transform_point(bbox[0], bbox[3]),
11940 params.ctm.transform_point(bbox[2], bbox[3]),
11941 ];
11942 let x_min = corners
11943 .iter()
11944 .map(|c| c.0 as f32)
11945 .fold(f32::INFINITY, f32::min);
11946 let y_min = corners
11947 .iter()
11948 .map(|c| c.1 as f32)
11949 .fold(f32::INFINITY, f32::min);
11950 let x_max = corners
11951 .iter()
11952 .map(|c| c.0 as f32)
11953 .fold(f32::NEG_INFINITY, f32::max);
11954 let y_max = corners
11955 .iter()
11956 .map(|c| c.1 as f32)
11957 .fold(f32::NEG_INFINITY, f32::max);
11958 (
11959 ((x_min - vp_x) * scale_x).max(0.0) as u32,
11960 ((y_min - vp_y) * scale_y).max(0.0) as u32,
11961 (((x_max - vp_x) * scale_x).ceil() as u32).min(pw),
11962 (((y_max - vp_y) * scale_y).ceil() as u32).min(ph),
11963 )
11964 } else {
11965 (0, 0, pw, ph)
11966 };
11967
11968 let inv_sx = 1.0 / scale_x as f64;
11969 let inv_sy = 1.0 / scale_y as f64;
11970
11971 let rotated_bbox = if let Some(bbox) = ¶ms.bbox {
11973 if params.ctm.b.abs() > 1e-10 || params.ctm.c.abs() > 1e-10 {
11974 let (bx0, bx1) = (bbox[0].min(bbox[2]), bbox[0].max(bbox[2]));
11975 let (by0, by1) = (bbox[1].min(bbox[3]), bbox[1].max(bbox[3]));
11976 Some((bx0, by0, bx1, by1))
11977 } else {
11978 None
11979 }
11980 } else {
11981 None
11982 };
11983
11984 let data = pixmap.data_mut();
11985 let stride = pw as usize * 4;
11986
11987 for py in py_min..py_max {
11988 let dev_y = py as f64 * inv_sy + vp_y as f64;
11989 for px in px_min..px_max {
11990 let dev_x = px as f64 * inv_sx + vp_x as f64;
11991 let (ux, uy) = inv_ctm.transform_point(dev_x, dev_y);
11992
11993 if let Some((bx0, by0, bx1, by1)) = rotated_bbox {
11995 if ux < bx0 || ux > bx1 || uy < by0 || uy > by1 {
11996 continue;
11997 }
11998 }
11999
12000 let t = solve_radial_t(
12001 ux,
12002 uy,
12003 params.x0,
12004 params.y0,
12005 params.r0,
12006 params.x1,
12007 params.y1,
12008 params.r1,
12009 params.extend_start,
12010 params.extend_end,
12011 );
12012 if let Some(t) = t {
12013 let clamped = t.clamp(0.0, 1.0);
12014 let color = interpolate_color_stops(¶ms.color_stops, clamped);
12015
12016 let clipped = clip_mask
12017 .is_some_and(|mask| mask.data()[py as usize * pw as usize + px as usize] == 0);
12018
12019 if clipped {
12020 continue;
12021 }
12022
12023 let cmyk = interpolate_cmyk_from_stops(
12032 ¶ms.color_stops,
12033 ¶ms.color_space,
12034 clamped,
12035 &color,
12036 icc,
12037 );
12038 let ci = (py as usize * pw as usize + px as usize) * 4;
12039 let buffer_clean = if let Some(ref buf) = cmyk_buf {
12040 if ci + 3 < buf.len() {
12041 buf[ci] == 0.0
12042 && buf[ci + 1] == 0.0
12043 && buf[ci + 2] == 0.0
12044 && buf[ci + 3] == 0.0
12045 } else {
12046 false
12047 }
12048 } else {
12049 false
12050 };
12051 let offset_for_check = py as usize * stride + px as usize * 4;
12052 let pixmap_has_colour = data[offset_for_check + 3] > 0
12053 && (data[offset_for_check] < 250
12054 || data[offset_for_check + 1] < 250
12055 || data[offset_for_check + 2] < 250);
12056 let use_multiplicative = params.overprint
12057 && params.painted_channels != stet_graphics::device::CMYK_ALL
12058 && buffer_clean
12059 && pixmap_has_colour;
12060
12061 if let Some(ref mut buf) = cmyk_buf
12063 && ci + 3 < buf.len()
12064 {
12065 if params.overprint
12066 && params.painted_channels != stet_graphics::device::CMYK_ALL
12067 {
12068 if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
12069 buf[ci] = cmyk.0 as f32;
12070 }
12071 if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
12072 buf[ci + 1] = cmyk.1 as f32;
12073 }
12074 if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
12075 buf[ci + 2] = cmyk.2 as f32;
12076 }
12077 if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
12078 buf[ci + 3] = cmyk.3 as f32;
12079 }
12080 } else {
12081 buf[ci] = cmyk.0 as f32;
12082 buf[ci + 1] = cmyk.1 as f32;
12083 buf[ci + 2] = cmyk.2 as f32;
12084 buf[ci + 3] = cmyk.3 as f32;
12085 }
12086 }
12087
12088 let offset = py as usize * stride + px as usize * 4;
12089 if use_multiplicative {
12090 let bg_r = data[offset] as f64 / 255.0;
12095 let bg_g = data[offset + 1] as f64 / 255.0;
12096 let bg_b = data[offset + 2] as f64 / 255.0;
12097 let over_r = if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
12098 1.0 - cmyk.0
12099 } else {
12100 1.0
12101 };
12102 let over_g = if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
12103 1.0 - cmyk.1
12104 } else {
12105 1.0
12106 };
12107 let over_b = if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
12108 1.0 - cmyk.2
12109 } else {
12110 1.0
12111 };
12112 let k_fac = if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
12113 1.0 - cmyk.3
12114 } else {
12115 1.0
12116 };
12117 data[offset] = ((bg_r * over_r * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
12118 data[offset + 1] =
12119 ((bg_g * over_g * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
12120 data[offset + 2] =
12121 ((bg_b * over_b * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
12122 data[offset + 3] = 255;
12123 } else {
12124 data[offset] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
12125 data[offset + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
12126 data[offset + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
12127 data[offset + 3] = 255;
12128
12129 if params.overprint
12144 && params.painted_channels != stet_graphics::device::CMYK_ALL
12145 && matches!(
12146 params.color_space,
12147 ShadingColorSpace::DeviceCMYK
12148 | ShadingColorSpace::Separation { .. }
12149 | ShadingColorSpace::DeviceN { .. }
12150 )
12151 && let Some(ref mut buf) = cmyk_buf
12152 && ci + 3 < buf.len()
12153 && let Some(icc_cache) = icc
12154 {
12155 let c = buf[ci] as f64;
12156 let m = buf[ci + 1] as f64;
12157 let y = buf[ci + 2] as f64;
12158 let k = buf[ci + 3] as f64;
12159 if let Some((r, g, b)) = icc_cache.convert_cmyk_readonly(c, m, y, k) {
12160 data[offset] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
12161 data[offset + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
12162 data[offset + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
12163 }
12164 }
12165 }
12166 }
12167 }
12168 }
12169}
12170#[allow(clippy::too_many_arguments)]
12175fn solve_radial_t(
12176 px: f64,
12177 py: f64,
12178 x0: f64,
12179 y0: f64,
12180 r0: f64,
12181 x1: f64,
12182 y1: f64,
12183 r1: f64,
12184 extend_start: bool,
12185 extend_end: bool,
12186) -> Option<f64> {
12187 let cdx = x1 - x0;
12190 let cdy = y1 - y0;
12191 let dr = r1 - r0;
12192
12193 let a = cdx * cdx + cdy * cdy - dr * dr;
12194 let dpx = px - x0;
12195 let dpy = py - y0;
12196 let b = -2.0 * (dpx * cdx + dpy * cdy + r0 * dr);
12197 let c = dpx * dpx + dpy * dpy - r0 * r0;
12198
12199 let in_domain = |t: f64| -> bool {
12201 (0.0..=1.0).contains(&t) || (t < 0.0 && extend_start) || (t > 1.0 && extend_end)
12202 };
12203
12204 if a.abs() < 1e-10 {
12205 if b.abs() < 1e-10 {
12207 return None;
12208 }
12209 let t = -c / b;
12210 let radius = r0 + t * dr;
12211 if radius >= 0.0 && in_domain(t) {
12212 return Some(t);
12213 }
12214 return None;
12215 }
12216
12217 let discriminant = b * b - 4.0 * a * c;
12218 if discriminant < 0.0 {
12219 return None;
12220 }
12221 let sqrt_d = discriminant.sqrt();
12222 let t1 = (-b + sqrt_d) / (2.0 * a);
12223 let t2 = (-b - sqrt_d) / (2.0 * a);
12224
12225 let mut best: Option<f64> = None;
12227 for t in [t1, t2] {
12228 let radius = r0 + t * dr;
12229 if radius >= 0.0 && in_domain(t) {
12230 best = Some(match best {
12231 Some(prev) => prev.max(t),
12232 None => t,
12233 });
12234 }
12235 }
12236 best
12237}
12238
12239#[allow(clippy::too_many_arguments)]
12241fn render_mesh_shading(
12242 pixmap: &mut Pixmap,
12243 params: &MeshShadingParams,
12244 vp_x: f32,
12245 vp_y: f32,
12246 scale_x: f32,
12247 scale_y: f32,
12248 clip_mask: Option<&Mask>,
12249 mut cmyk_buf: Option<&mut [f32]>,
12250 icc: Option<&IccCache>,
12251) {
12252 let pw = pixmap.width() as usize;
12253 let ph = pixmap.height() as usize;
12254 if pw == 0 || ph == 0 {
12255 return;
12256 }
12257 let data = pixmap.data_mut();
12258 let stride = pw * 4;
12259
12260 let lut = params.color_lut.as_deref();
12261
12262 for tri in ¶ms.triangles {
12263 let (dx0, dy0) = params.ctm.transform_point(tri.v0.x, tri.v0.y);
12264 let (dx1, dy1) = params.ctm.transform_point(tri.v1.x, tri.v1.y);
12265 let (dx2, dy2) = params.ctm.transform_point(tri.v2.x, tri.v2.y);
12266
12267 let x0 = (dx0 as f32 - vp_x) * scale_x;
12268 let y0 = (dy0 as f32 - vp_y) * scale_y;
12269 let x1 = (dx1 as f32 - vp_x) * scale_x;
12270 let y1 = (dy1 as f32 - vp_y) * scale_y;
12271 let x2 = (dx2 as f32 - vp_x) * scale_x;
12272 let y2 = (dy2 as f32 - vp_y) * scale_y;
12273
12274 let min_x = (x0.min(x1).min(x2).floor().max(0.0)) as usize;
12275 let max_x = (x0.max(x1).max(x2).ceil() as usize).min(pw);
12276 let min_y = (y0.min(y1).min(y2).floor().max(0.0)) as usize;
12277 let max_y = (y0.max(y1).max(y2).ceil() as usize).min(ph);
12278
12279 if min_x >= max_x || min_y >= max_y {
12280 continue;
12281 }
12282
12283 let x0 = x0 as f64;
12284 let y0 = y0 as f64;
12285 let x1 = x1 as f64;
12286 let y1 = y1 as f64;
12287 let x2 = x2 as f64;
12288 let y2 = y2 as f64;
12289 let denom = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2);
12294 if denom.abs() < 1e-10 {
12295 continue;
12296 }
12297 let (x1, y1, x2, y2) = if denom < 0.0 {
12298 (x2, y2, x1, y1)
12299 } else {
12300 (x1, y1, x2, y2)
12301 };
12302 let (v1_ref, v2_ref) = if denom < 0.0 {
12303 (&tri.v2, &tri.v1)
12304 } else {
12305 (&tri.v1, &tri.v2)
12306 };
12307 let denom = denom.abs();
12308 let inv_denom = 1.0 / denom;
12309
12310 for py in min_y..max_y {
12311 for px in min_x..max_x {
12312 let pxf = px as f64 + 0.5;
12313 let pyf = py as f64 + 0.5;
12314
12315 let w0 = ((y1 - y2) * (pxf - x2) + (x2 - x1) * (pyf - y2)) * inv_denom;
12316 let w1 = ((y2 - y0) * (pxf - x2) + (x0 - x2) * (pyf - y2)) * inv_denom;
12317 let w2 = 1.0 - w0 - w1;
12318
12319 if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 {
12320 continue;
12321 }
12322
12323 let clipped = clip_mask.is_some_and(|mask| mask.data()[py * pw + px] == 0);
12324
12325 let w0c = w0.max(0.0);
12326 let w1c = w1.max(0.0);
12327 let w2c = w2.max(0.0);
12328 let wsum = w0c + w1c + w2c;
12329 let w0n = w0c / wsum;
12330 let w1n = w1c / wsum;
12331 let w2n = w2c / wsum;
12332
12333 let (r, g, b) = if let Some(lut) = lut {
12336 let raw = w0n * tri.v0.raw_components[0]
12338 + w1n * v1_ref.raw_components[0]
12339 + w2n * v2_ref.raw_components[0];
12340 let raw = raw.clamp(0.0, 1.0);
12341 let fi = raw * (lut.len() - 1) as f64;
12343 let i0 = (fi as usize).min(lut.len().saturating_sub(2));
12344 let frac = fi - i0 as f64;
12345 let c0 = &lut[i0];
12346 let c1 = &lut[i0 + 1];
12347 (
12348 c0.r + frac * (c1.r - c0.r),
12349 c0.g + frac * (c1.g - c0.g),
12350 c0.b + frac * (c1.b - c0.b),
12351 )
12352 } else {
12353 (
12354 w0n * tri.v0.color.r + w1n * v1_ref.color.r + w2n * v2_ref.color.r,
12355 w0n * tri.v0.color.g + w1n * v1_ref.color.g + w2n * v2_ref.color.g,
12356 w0n * tri.v0.color.b + w1n * v1_ref.color.b + w2n * v2_ref.color.b,
12357 )
12358 };
12359
12360 if let Some(ref mut buf) = cmyk_buf {
12362 let ci = (py * pw + px) * 4;
12363 if ci + 3 < buf.len() {
12364 let cmyk = interpolate_cmyk_from_vertices(
12365 &tri.v0,
12366 v1_ref,
12367 v2_ref,
12368 w0n,
12369 w1n,
12370 w2n,
12371 ¶ms.color_space,
12372 r,
12373 g,
12374 b,
12375 icc,
12376 );
12377 if params.overprint
12378 && params.painted_channels != stet_graphics::device::CMYK_ALL
12379 {
12380 if !clipped {
12381 if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
12382 buf[ci] = cmyk.0 as f32;
12383 }
12384 if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
12385 buf[ci + 1] = cmyk.1 as f32;
12386 }
12387 if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
12388 buf[ci + 2] = cmyk.2 as f32;
12389 }
12390 if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
12391 buf[ci + 3] = cmyk.3 as f32;
12392 }
12393 }
12394 } else {
12395 buf[ci] = cmyk.0 as f32;
12396 buf[ci + 1] = cmyk.1 as f32;
12397 buf[ci + 2] = cmyk.2 as f32;
12398 buf[ci + 3] = cmyk.3 as f32;
12399 }
12400 }
12401 }
12402
12403 if clipped {
12404 continue;
12405 }
12406
12407 let offset = py * stride + px * 4;
12408 data[offset] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
12409 data[offset + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
12410 data[offset + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
12411 data[offset + 3] = 255;
12412 }
12413 }
12414 }
12415}
12416
12417#[derive(Clone, Copy)]
12427struct ShadingCull {
12428 ctm: Matrix,
12429 vp_x: f32,
12430 vp_y: f32,
12431 scale_x: f32,
12432 scale_y: f32,
12433 w: f32,
12434 h: f32,
12435}
12436
12437impl ShadingCull {
12438 fn new(
12441 ctm: Matrix,
12442 vp_x: f32,
12443 vp_y: f32,
12444 scale_x: f32,
12445 scale_y: f32,
12446 w: u32,
12447 h: u32,
12448 ) -> Option<Self> {
12449 (scale_x > 0.0 && scale_y > 0.0 && w > 0 && h > 0).then_some(Self {
12450 ctm,
12451 vp_x,
12452 vp_y,
12453 scale_x,
12454 scale_y,
12455 w: w as f32,
12456 h: h as f32,
12457 })
12458 }
12459
12460 fn rejects(&self, x_min: f64, y_min: f64, x_max: f64, y_max: f64) -> bool {
12464 let px_min = (x_min as f32 - self.vp_x) * self.scale_x;
12465 let px_max = (x_max as f32 - self.vp_x) * self.scale_x;
12466 let py_min = (y_min as f32 - self.vp_y) * self.scale_y;
12467 let py_max = (y_max as f32 - self.vp_y) * self.scale_y;
12468 px_max <= 0.0 || px_min >= self.w || py_max <= 0.0 || py_min >= self.h
12469 }
12470
12471 fn rejects_triangle(&self, p0: (f64, f64), p1: (f64, f64), p2: (f64, f64)) -> bool {
12474 self.rejects(
12475 p0.0.min(p1.0).min(p2.0),
12476 p0.1.min(p1.1).min(p2.1),
12477 p0.0.max(p1.0).max(p2.0),
12478 p0.1.max(p1.1).max(p2.1),
12479 )
12480 }
12481}
12482
12483fn coons_tensor_axis(
12490 c0: [f64; 4],
12491 c2: [f64; 4],
12492 d0: [f64; 4],
12493 d1: [f64; 4],
12494 corners: [f64; 4],
12495) -> [f64; 16] {
12496 const A: [f64; 4] = [1.0, 2.0 / 3.0, 1.0 / 3.0, 0.0];
12499 const B: [f64; 4] = [0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0];
12500 let [p00, p10, p01, p11] = corners;
12501 let mut out = [0.0; 16];
12502 for j in 0..4 {
12503 for i in 0..4 {
12504 let bilinear =
12505 A[i] * A[j] * p00 + B[i] * A[j] * p10 + A[i] * B[j] * p01 + B[i] * B[j] * p11;
12506 out[j * 4 + i] = A[j] * c0[i] + B[j] * c2[i] + A[i] * d0[j] + B[i] * d1[j] - bilinear;
12507 }
12508 }
12509 out
12510}
12511
12512fn coons_tensor_net(pts: &[(f64, f64)]) -> [(f64, f64); 16] {
12525 let c0 = [pts[0], pts[1], pts[2], pts[3]];
12526 let c2 = [pts[9], pts[8], pts[7], pts[6]];
12528 let d0 = [pts[0], pts[11], pts[10], pts[9]];
12529 let d1 = [pts[3], pts[4], pts[5], pts[6]];
12530 let corners = [pts[0], pts[3], pts[9], pts[6]];
12531
12532 let xs = coons_tensor_axis(
12533 c0.map(|p| p.0),
12534 c2.map(|p| p.0),
12535 d0.map(|p| p.0),
12536 d1.map(|p| p.0),
12537 corners.map(|p| p.0),
12538 );
12539 let ys = coons_tensor_axis(
12540 c0.map(|p| p.1),
12541 c2.map(|p| p.1),
12542 d0.map(|p| p.1),
12543 d1.map(|p| p.1),
12544 corners.map(|p| p.1),
12545 );
12546 std::array::from_fn(|k| (xs[k], ys[k]))
12547}
12548
12549fn patch_hull_bbox(
12558 patch: &stet_graphics::device::ShadingPatch,
12559 ctm: &Matrix,
12560) -> Option<(f64, f64, f64, f64)> {
12561 if patch.points.len() < 12 {
12562 return None;
12563 }
12564 let tensor_net;
12565 let net: &[(f64, f64)] = if patch.points.len() >= 16 {
12566 &patch.points[..16]
12567 } else {
12568 tensor_net = coons_tensor_net(&patch.points);
12569 &tensor_net
12570 };
12571 let mut x_min = f64::INFINITY;
12572 let mut y_min = f64::INFINITY;
12573 let mut x_max = f64::NEG_INFINITY;
12574 let mut y_max = f64::NEG_INFINITY;
12575 for &(px, py) in net {
12576 let (dx, dy) = ctm.transform_point(px, py);
12577 x_min = x_min.min(dx);
12578 y_min = y_min.min(dy);
12579 x_max = x_max.max(dx);
12580 y_max = y_max.max(dy);
12581 }
12582 x_min.is_finite().then_some((x_min, y_min, x_max, y_max))
12583}
12584
12585#[allow(clippy::too_many_arguments)]
12587fn render_patch_shading(
12588 pixmap: &mut Pixmap,
12589 params: &PatchShadingParams,
12590 vp_x: f32,
12591 vp_y: f32,
12592 scale_x: f32,
12593 scale_y: f32,
12594 clip_mask: Option<&Mask>,
12595 cmyk_buf: Option<&mut [f32]>,
12596 icc: Option<&IccCache>,
12597) {
12598 let mut triangles = Vec::new();
12599 let scale = scale_x.max(scale_y) as f64;
12600 let cull = ShadingCull::new(
12604 params.ctm,
12605 vp_x,
12606 vp_y,
12607 scale_x,
12608 scale_y,
12609 pixmap.width(),
12610 pixmap.height(),
12611 );
12612 for patch in ¶ms.patches {
12613 if patch.points.len() >= 12 {
12614 let mut x_min = f64::INFINITY;
12616 let mut y_min = f64::INFINITY;
12617 let mut x_max = f64::NEG_INFINITY;
12618 let mut y_max = f64::NEG_INFINITY;
12619 for &(px, py) in &patch.points {
12620 let (dx, dy) = params.ctm.transform_point(px, py);
12621 x_min = x_min.min(dx);
12622 y_min = y_min.min(dy);
12623 x_max = x_max.max(dx);
12624 y_max = y_max.max(dy);
12625 }
12626 let extent = (x_max - x_min).max(y_max - y_min).abs() * scale;
12627 let n = (extent / 2.0).ceil().clamp(8.0, 64.0) as usize;
12629 if let Some(cull) = cull.as_ref()
12634 && let Some((hx_min, hy_min, hx_max, hy_max)) = patch_hull_bbox(patch, ¶ms.ctm)
12635 && cull.rejects(hx_min, hy_min, hx_max, hy_max)
12636 {
12637 continue;
12638 }
12639 let icc_profile_hash = match ¶ms.color_space {
12641 stet_graphics::device::ShadingColorSpace::ICCBased { profile_hash, .. } => {
12642 Some(profile_hash)
12643 }
12644 _ => None,
12645 };
12646 subdivide_patch_to_triangles(
12647 patch,
12648 &mut triangles,
12649 n,
12650 icc_profile_hash,
12651 icc,
12652 cull.as_ref(),
12653 );
12654 }
12655 }
12656 if !triangles.is_empty() {
12657 let mesh_params = MeshShadingParams {
12658 triangles,
12659 ctm: params.ctm,
12660 bbox: params.bbox,
12661 color_space: params.color_space.clone(),
12662 overprint: params.overprint,
12663 overprint_mode: params.overprint_mode,
12664 painted_channels: params.painted_channels,
12665 color_lut: params.color_lut.clone(),
12666 alpha: params.alpha,
12667 blend_mode: params.blend_mode,
12668 alpha_is_shape: params.alpha_is_shape,
12669 };
12670 render_mesh_shading(
12671 pixmap,
12672 &mesh_params,
12673 vp_x,
12674 vp_y,
12675 scale_x,
12676 scale_y,
12677 clip_mask,
12678 cmyk_buf,
12679 icc,
12680 );
12681 }
12682}
12683fn subdivide_patch_to_triangles(
12688 patch: &stet_graphics::device::ShadingPatch,
12689 triangles: &mut Vec<stet_graphics::device::ShadingTriangle>,
12690 n: usize,
12691 icc_profile_hash: Option<&stet_graphics::icc::ProfileHash>,
12692 icc_cache: Option<&IccCache>,
12693 cull: Option<&ShadingCull>,
12694) {
12695 let mut grid: Vec<(f64, f64, DeviceColor, Vec<f64>)> = Vec::with_capacity((n + 1) * (n + 1));
12699 let mut device: Vec<(f64, f64)> = Vec::new();
12702 if cull.is_some() {
12703 device.reserve((n + 1) * (n + 1));
12704 }
12705 let use_tensor = patch.points.len() >= 16;
12706 let has_raw = !patch.raw_colors[0].is_empty();
12707 let use_icc_interp = has_raw && icc_profile_hash.is_some() && icc_cache.is_some();
12709
12710 for row in 0..=n {
12711 let v = row as f64 / n as f64;
12712 for col in 0..=n {
12713 let u = col as f64 / n as f64;
12714 let (x, y) = if use_tensor {
12715 eval_tensor_patch(patch, u, v)
12716 } else {
12717 eval_coons_patch(patch, u, v)
12718 };
12719 if let Some(cull) = cull {
12720 device.push(cull.ctm.transform_point(x, y));
12721 }
12722 let raw = if has_raw {
12723 bilinear_raw(&patch.raw_colors, u, v)
12724 } else {
12725 vec![]
12726 };
12727 let color = if use_icc_interp {
12733 if let Some((r, g, b)) = icc_cache
12734 .unwrap()
12735 .convert_color_readonly(icc_profile_hash.unwrap(), &raw)
12736 {
12737 DeviceColor::from_rgb(r, g, b)
12738 } else {
12739 bilinear_color(&patch.colors, u, v)
12740 }
12741 } else {
12742 bilinear_color(&patch.colors, u, v)
12743 };
12744 grid.push((x, y, color, raw));
12745 }
12746 }
12747
12748 let cols = n + 1;
12750 for row in 0..n {
12751 for col in 0..n {
12752 let i00 = row * cols + col;
12753 let i10 = i00 + 1;
12754 let i01 = i00 + cols;
12755 let i11 = i01 + 1;
12756
12757 let (keep_lower, keep_upper) = match cull {
12762 Some(cull) => (
12763 !cull.rejects_triangle(device[i00], device[i10], device[i01]),
12764 !cull.rejects_triangle(device[i10], device[i11], device[i01]),
12765 ),
12766 None => (true, true),
12767 };
12768 if !keep_lower && !keep_upper {
12769 continue;
12770 }
12771
12772 let (x00, y00, c00, r00) = &grid[i00];
12773 let (x10, y10, c10, r10) = &grid[i10];
12774 let (x01, y01, c01, r01) = &grid[i01];
12775 let (x11, y11, c11, r11) = &grid[i11];
12776
12777 use stet_graphics::device::ShadingVertex;
12778 if keep_lower {
12779 triangles.push(stet_graphics::device::ShadingTriangle {
12780 v0: ShadingVertex {
12781 x: *x00,
12782 y: *y00,
12783 color: c00.clone(),
12784 raw_components: r00.clone(),
12785 },
12786 v1: ShadingVertex {
12787 x: *x10,
12788 y: *y10,
12789 color: c10.clone(),
12790 raw_components: r10.clone(),
12791 },
12792 v2: ShadingVertex {
12793 x: *x01,
12794 y: *y01,
12795 color: c01.clone(),
12796 raw_components: r01.clone(),
12797 },
12798 });
12799 }
12800 if keep_upper {
12801 triangles.push(stet_graphics::device::ShadingTriangle {
12802 v0: ShadingVertex {
12803 x: *x10,
12804 y: *y10,
12805 color: c10.clone(),
12806 raw_components: r10.clone(),
12807 },
12808 v1: ShadingVertex {
12809 x: *x11,
12810 y: *y11,
12811 color: c11.clone(),
12812 raw_components: r11.clone(),
12813 },
12814 v2: ShadingVertex {
12815 x: *x01,
12816 y: *y01,
12817 color: c01.clone(),
12818 raw_components: r01.clone(),
12819 },
12820 });
12821 }
12822 }
12823 }
12824}
12825
12826fn eval_coons_patch(patch: &stet_graphics::device::ShadingPatch, u: f64, v: f64) -> (f64, f64) {
12829 let pts = &patch.points;
12830 if pts.len() < 12 {
12831 return (0.0, 0.0);
12832 }
12833
12834 let c0 = eval_cubic_bezier(pts[0], pts[1], pts[2], pts[3], u);
12839 let c2 = eval_cubic_bezier(pts[6], pts[7], pts[8], pts[9], 1.0 - u);
12840 let d0 = eval_cubic_bezier(pts[0], pts[11], pts[10], pts[9], v);
12841 let d1 = eval_cubic_bezier(pts[3], pts[4], pts[5], pts[6], v);
12842
12843 let p00 = pts[0];
12845 let p10 = pts[3];
12846 let p01 = pts[9];
12847 let p11 = pts[6];
12848 let bx = (1.0 - u) * (1.0 - v) * p00.0
12849 + u * (1.0 - v) * p10.0
12850 + (1.0 - u) * v * p01.0
12851 + u * v * p11.0;
12852 let by = (1.0 - u) * (1.0 - v) * p00.1
12853 + u * (1.0 - v) * p10.1
12854 + (1.0 - u) * v * p01.1
12855 + u * v * p11.1;
12856
12857 let x = (1.0 - v) * c0.0 + v * c2.0 + (1.0 - u) * d0.0 + u * d1.0 - bx;
12859 let y = (1.0 - v) * c0.1 + v * c2.1 + (1.0 - u) * d0.1 + u * d1.1 - by;
12860
12861 (x, y)
12862}
12863
12864fn eval_tensor_patch(patch: &stet_graphics::device::ShadingPatch, u: f64, v: f64) -> (f64, f64) {
12878 let pts = &patch.points;
12879
12880 let grid: [[usize; 4]; 4] = [[0, 1, 2, 3], [11, 12, 13, 4], [10, 15, 14, 5], [9, 8, 7, 6]];
12884
12885 let su = 1.0 - u;
12887 let bu = [su * su * su, 3.0 * su * su * u, 3.0 * su * u * u, u * u * u];
12888 let sv = 1.0 - v;
12889 let bv = [sv * sv * sv, 3.0 * sv * sv * v, 3.0 * sv * v * v, v * v * v];
12890
12891 let mut x = 0.0;
12892 let mut y = 0.0;
12893 for j in 0..4 {
12894 for i in 0..4 {
12895 let w = bu[i] * bv[j];
12896 let p = pts[grid[j][i]];
12897 x += w * p.0;
12898 y += w * p.1;
12899 }
12900 }
12901 (x, y)
12902}
12903
12904fn eval_cubic_bezier(
12906 p0: (f64, f64),
12907 p1: (f64, f64),
12908 p2: (f64, f64),
12909 p3: (f64, f64),
12910 t: f64,
12911) -> (f64, f64) {
12912 let s = 1.0 - t;
12913 let s2 = s * s;
12914 let t2 = t * t;
12915 let b0 = s2 * s;
12916 let b1 = 3.0 * s2 * t;
12917 let b2 = 3.0 * s * t2;
12918 let b3 = t2 * t;
12919 (
12920 b0 * p0.0 + b1 * p1.0 + b2 * p2.0 + b3 * p3.0,
12921 b0 * p0.1 + b1 * p1.1 + b2 * p2.1 + b3 * p3.1,
12922 )
12923}
12924
12925fn bilinear_color(colors: &[DeviceColor; 4], u: f64, v: f64) -> DeviceColor {
12927 let r = (1.0 - u) * (1.0 - v) * colors[0].r
12928 + u * (1.0 - v) * colors[1].r
12929 + (1.0 - u) * v * colors[3].r
12930 + u * v * colors[2].r;
12931 let g = (1.0 - u) * (1.0 - v) * colors[0].g
12932 + u * (1.0 - v) * colors[1].g
12933 + (1.0 - u) * v * colors[3].g
12934 + u * v * colors[2].g;
12935 let b = (1.0 - u) * (1.0 - v) * colors[0].b
12936 + u * (1.0 - v) * colors[1].b
12937 + (1.0 - u) * v * colors[3].b
12938 + u * v * colors[2].b;
12939 DeviceColor::from_rgb(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0))
12940}
12941
12942fn bilinear_raw(raw_colors: &[Vec<f64>; 4], u: f64, v: f64) -> Vec<f64> {
12944 let n = raw_colors[0].len();
12945 let mut result = vec![0.0; n];
12946 for i in 0..n {
12947 result[i] = (1.0 - u) * (1.0 - v) * raw_colors[0][i]
12948 + u * (1.0 - v) * raw_colors[1][i]
12949 + (1.0 - u) * v * raw_colors[3][i]
12950 + u * v * raw_colors[2][i];
12951 }
12952 result
12953}
12954
12955fn build_gradient_lut(stops: &[stet_graphics::device::ColorStop], size: usize) -> Vec<[u8; 4]> {
12961 let size = size.max(2);
12962 let mut lut = vec![[0u8; 4]; size];
12963 if stops.is_empty() {
12964 return lut;
12965 }
12966 let mut si = 0usize; let last = (size - 1) as f64;
12968 for i in 0..size {
12969 let t = i as f64 / last;
12970 while si + 1 < stops.len() && stops[si + 1].position < t {
12972 si += 1;
12973 }
12974 let (r, g, b) = if si + 1 >= stops.len() {
12975 let c = &stops[stops.len() - 1].color;
12976 (c.r, c.g, c.b)
12977 } else if t <= stops[si].position {
12978 let c = &stops[si].color;
12979 (c.r, c.g, c.b)
12980 } else {
12981 let t0 = stops[si].position;
12982 let t1 = stops[si + 1].position;
12983 let frac = if (t1 - t0).abs() < 1e-10 {
12984 0.0
12985 } else {
12986 (t - t0) / (t1 - t0)
12987 };
12988 let c0 = &stops[si].color;
12989 let c1 = &stops[si + 1].color;
12990 (
12991 c0.r + frac * (c1.r - c0.r),
12992 c0.g + frac * (c1.g - c0.g),
12993 c0.b + frac * (c1.b - c0.b),
12994 )
12995 };
12996 lut[i] = [
12997 (r * 255.0).round().clamp(0.0, 255.0) as u8,
12998 (g * 255.0).round().clamp(0.0, 255.0) as u8,
12999 (b * 255.0).round().clamp(0.0, 255.0) as u8,
13000 255,
13001 ];
13002 }
13003 lut
13004}
13005
13006fn build_gradient_stops(
13008 stops: &[stet_graphics::device::ColorStop],
13009) -> Vec<stet_tiny_skia::GradientStop> {
13010 let mut result = Vec::with_capacity(stops.len());
13011 for stop in stops {
13012 let r = (stop.color.r * 255.0).round().clamp(0.0, 255.0) as u8;
13013 let g = (stop.color.g * 255.0).round().clamp(0.0, 255.0) as u8;
13014 let b = (stop.color.b * 255.0).round().clamp(0.0, 255.0) as u8;
13015 result.push(stet_tiny_skia::GradientStop::new(
13016 stop.position as f32,
13017 Color::from_rgba8(r, g, b, 255),
13018 ));
13019 }
13020 result
13021}
13022
13023fn interpolate_color_stops(
13025 stops: &[stet_graphics::device::ColorStop],
13026 position: f64,
13027) -> DeviceColor {
13028 if stops.is_empty() {
13029 return DeviceColor::from_gray(0.0);
13030 }
13031 if stops.len() == 1 || position <= stops[0].position {
13032 return stops[0].color.clone();
13033 }
13034 if position >= stops.last().unwrap().position {
13035 return stops.last().unwrap().color.clone();
13036 }
13037
13038 for i in 1..stops.len() {
13040 if position <= stops[i].position {
13041 let t0 = stops[i - 1].position;
13042 let t1 = stops[i].position;
13043 let frac = if (t1 - t0).abs() < 1e-10 {
13044 0.0
13045 } else {
13046 (position - t0) / (t1 - t0)
13047 };
13048 let c0 = &stops[i - 1].color;
13049 let c1 = &stops[i].color;
13050 return DeviceColor::from_rgb(
13051 (c0.r + frac * (c1.r - c0.r)).clamp(0.0, 1.0),
13052 (c0.g + frac * (c1.g - c0.g)).clamp(0.0, 1.0),
13053 (c0.b + frac * (c1.b - c0.b)).clamp(0.0, 1.0),
13054 );
13055 }
13056 }
13057
13058 stops.last().unwrap().color.clone()
13059}
13060
13061fn interpolate_cmyk_from_stops(
13073 stops: &[stet_graphics::device::ColorStop],
13074 cs: &ShadingColorSpace,
13075 t: f64,
13076 color: &DeviceColor,
13077 icc: Option<&IccCache>,
13078) -> (f64, f64, f64, f64) {
13079 let rgb_to_cmyk = |c: &DeviceColor| -> (f64, f64, f64, f64) {
13080 if let Some(cmyk) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(c.r, c.g, c.b)) {
13081 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
13082 } else {
13083 (
13084 (1.0 - c.r).clamp(0.0, 1.0),
13085 (1.0 - c.g).clamp(0.0, 1.0),
13086 (1.0 - c.b).clamp(0.0, 1.0),
13087 0.0,
13088 )
13089 }
13090 };
13091
13092 match cs {
13093 ShadingColorSpace::DeviceCMYK
13097 | ShadingColorSpace::Separation { .. }
13098 | ShadingColorSpace::DeviceN { .. } => {
13099 if stops.len() == 1 {
13101 let rc = &stops[0].raw_components;
13102 if rc.len() >= 4 {
13103 return (rc[0], rc[1], rc[2], rc[3]);
13104 }
13105 }
13106 let mut lo = &stops[0];
13108 let mut hi = stops.last().unwrap();
13109 for i in 0..stops.len() - 1 {
13110 if stops[i + 1].position >= t {
13111 lo = &stops[i];
13112 hi = &stops[i + 1];
13113 break;
13114 }
13115 }
13116 let span = hi.position - lo.position;
13117 let frac = if span > 1e-10 {
13118 (t - lo.position) / span
13119 } else {
13120 0.0
13121 };
13122 let frac = frac.clamp(0.0, 1.0);
13123 if lo.raw_components.len() >= 4 && hi.raw_components.len() >= 4 {
13124 (
13125 lo.raw_components[0] + frac * (hi.raw_components[0] - lo.raw_components[0]),
13126 lo.raw_components[1] + frac * (hi.raw_components[1] - lo.raw_components[1]),
13127 lo.raw_components[2] + frac * (hi.raw_components[2] - lo.raw_components[2]),
13128 lo.raw_components[3] + frac * (hi.raw_components[3] - lo.raw_components[3]),
13129 )
13130 } else {
13131 rgb_to_cmyk(color)
13132 }
13133 }
13134 _ => rgb_to_cmyk(color),
13135 }
13136}
13137
13138#[allow(clippy::too_many_arguments)]
13144fn interpolate_cmyk_from_vertices(
13145 v0: &ShadingVertex,
13146 v1: &ShadingVertex,
13147 v2: &ShadingVertex,
13148 w0: f64,
13149 w1: f64,
13150 w2: f64,
13151 cs: &ShadingColorSpace,
13152 r: f64,
13153 g: f64,
13154 b: f64,
13155 icc: Option<&IccCache>,
13156) -> (f64, f64, f64, f64) {
13157 let rgb_to_cmyk = |r: f64, g: f64, b: f64| -> (f64, f64, f64, f64) {
13158 if let Some(cmyk) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(r, g, b)) {
13159 (cmyk[0], cmyk[1], cmyk[2], cmyk[3])
13160 } else {
13161 (
13162 (1.0 - r).clamp(0.0, 1.0),
13163 (1.0 - g).clamp(0.0, 1.0),
13164 (1.0 - b).clamp(0.0, 1.0),
13165 0.0,
13166 )
13167 }
13168 };
13169
13170 match cs {
13171 ShadingColorSpace::DeviceCMYK
13172 | ShadingColorSpace::Separation { .. }
13173 | ShadingColorSpace::DeviceN { .. } => {
13174 if v0.raw_components.len() >= 4
13175 && v1.raw_components.len() >= 4
13176 && v2.raw_components.len() >= 4
13177 {
13178 (
13179 w0 * v0.raw_components[0]
13180 + w1 * v1.raw_components[0]
13181 + w2 * v2.raw_components[0],
13182 w0 * v0.raw_components[1]
13183 + w1 * v1.raw_components[1]
13184 + w2 * v2.raw_components[1],
13185 w0 * v0.raw_components[2]
13186 + w1 * v1.raw_components[2]
13187 + w2 * v2.raw_components[2],
13188 w0 * v0.raw_components[3]
13189 + w1 * v1.raw_components[3]
13190 + w2 * v2.raw_components[3],
13191 )
13192 } else {
13193 rgb_to_cmyk(r, g, b)
13194 }
13195 }
13196 _ => rgb_to_cmyk(r, g, b),
13197 }
13198}
13199
13200#[cfg(test)]
13201mod tests {
13202 use super::*;
13203 #[cfg(feature = "ps-device")]
13204 use stet_graphics::color::DashPattern;
13205 use stet_graphics::device::{BgUcrState, HalftoneState, TransferState};
13206
13207 fn sample_coons_patch(seed: u64) -> stet_graphics::device::ShadingPatch {
13215 let mut state = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
13216 let mut next = || {
13217 state = state
13218 .wrapping_mul(6364136223846793005)
13219 .wrapping_add(1442695040888963407);
13220 ((state >> 33) as f64 / (1u64 << 31) as f64) * 2.0 - 1.0
13221 };
13222 stet_graphics::device::ShadingPatch {
13223 points: (0..12).map(|_| (next(), next())).collect(),
13224 colors: std::array::from_fn(|_| DeviceColor::from_rgb(0.5, 0.5, 0.5)),
13225 raw_colors: std::array::from_fn(|_| Vec::new()),
13226 }
13227 }
13228
13229 fn eval_tensor_net(net: &[(f64, f64); 16], u: f64, v: f64) -> (f64, f64) {
13231 let su = 1.0 - u;
13232 let bu = [su * su * su, 3.0 * su * su * u, 3.0 * su * u * u, u * u * u];
13233 let sv = 1.0 - v;
13234 let bv = [sv * sv * sv, 3.0 * sv * sv * v, 3.0 * sv * v * v, v * v * v];
13235 let mut x = 0.0;
13236 let mut y = 0.0;
13237 for j in 0..4 {
13238 for i in 0..4 {
13239 let w = bu[i] * bv[j];
13240 x += w * net[j * 4 + i].0;
13241 y += w * net[j * 4 + i].1;
13242 }
13243 }
13244 (x, y)
13245 }
13246
13247 #[test]
13249 fn coons_tensor_net_matches_coons_evaluation() {
13250 for seed in 0..32 {
13251 let patch = sample_coons_patch(seed);
13252 let net = coons_tensor_net(&patch.points);
13253 for iu in 0..=8 {
13254 for iv in 0..=8 {
13255 let (u, v) = (iu as f64 / 8.0, iv as f64 / 8.0);
13256 let (ex, ey) = eval_coons_patch(&patch, u, v);
13257 let (tx, ty) = eval_tensor_net(&net, u, v);
13258 assert!(
13259 (ex - tx).abs() < 1e-9 && (ey - ty).abs() < 1e-9,
13260 "seed {seed} at ({u}, {v}): coons ({ex}, {ey}) != tensor ({tx}, {ty})"
13261 );
13262 }
13263 }
13264 }
13265 }
13266
13267 #[test]
13275 fn coons_surface_can_escape_its_boundary_control_points() {
13276 let patch = stet_graphics::device::ShadingPatch {
13277 points: vec![
13278 (-0.5, 0.7),
13279 (0.88, 0.05),
13280 (0.86, 0.28),
13281 (-0.9, -0.49),
13282 (0.9, 0.78),
13283 (0.01, 0.22),
13284 (-0.67, -0.6),
13285 (0.83, 0.68),
13286 (0.87, -0.7),
13287 (-0.53, -0.88),
13288 (0.8, 0.76),
13289 (0.74, 0.84),
13290 ],
13291 colors: std::array::from_fn(|_| DeviceColor::from_rgb(0.5, 0.5, 0.5)),
13292 raw_colors: std::array::from_fn(|_| Vec::new()),
13293 };
13294 let control_x_max = patch
13295 .points
13296 .iter()
13297 .fold(f64::NEG_INFINITY, |m, p| m.max(p.0));
13298 let (surface_x, _) = eval_coons_patch(&patch, 0.475, 0.45);
13299 assert!(
13300 surface_x > control_x_max + 0.5,
13301 "surface x {surface_x} should escape control-point max {control_x_max}"
13302 );
13303
13304 let (_, _, hull_x_max, _) = patch_hull_bbox(&patch, &Matrix::identity()).unwrap();
13306 assert!(hull_x_max >= surface_x);
13307 }
13308
13309 #[test]
13312 fn patch_hull_bbox_contains_the_coons_surface() {
13313 let ctm = Matrix {
13314 a: 90.0,
13315 b: 12.0,
13316 c: -7.0,
13317 d: -80.0,
13318 tx: 15.0,
13319 ty: 400.0,
13320 };
13321 for seed in 0..64 {
13322 let patch = sample_coons_patch(seed);
13323 let (x_min, y_min, x_max, y_max) = patch_hull_bbox(&patch, &ctm).unwrap();
13324 for iu in 0..=16 {
13325 for iv in 0..=16 {
13326 let (u, v) = (iu as f64 / 16.0, iv as f64 / 16.0);
13327 let (x, y) = eval_coons_patch(&patch, u, v);
13328 let (dx, dy) = ctm.transform_point(x, y);
13329 assert!(
13330 dx >= x_min - 1e-9
13331 && dx <= x_max + 1e-9
13332 && dy >= y_min - 1e-9
13333 && dy <= y_max + 1e-9,
13334 "seed {seed} at ({u}, {v}): ({dx}, {dy}) outside \
13335 ({x_min}, {y_min})-({x_max}, {y_max})"
13336 );
13337 }
13338 }
13339 }
13340 }
13341
13342 #[test]
13346 fn triangle_culling_keeps_exactly_the_paintable_triangles() {
13347 let patch = sample_coons_patch(7);
13348 let ctm = Matrix {
13349 a: 100.0,
13350 b: 0.0,
13351 c: 0.0,
13352 d: 100.0,
13353 tx: 20.0,
13354 ty: 30.0,
13355 };
13356 let n = 16;
13357
13358 let mut all = Vec::new();
13359 subdivide_patch_to_triangles(&patch, &mut all, n, None, None, None);
13360 assert_eq!(all.len(), 2 * n * n);
13361
13362 let wide = ShadingCull::new(ctm, -500.0, -500.0, 1.0, 1.0, 4000, 4000).unwrap();
13366 let mut kept = Vec::new();
13367 subdivide_patch_to_triangles(&patch, &mut kept, n, None, None, Some(&wide));
13368 assert_eq!(kept.len(), all.len());
13369
13370 let elsewhere = ShadingCull::new(ctm, 0.0, 3000.0, 1.0, 1.0, 200, 140).unwrap();
13372 let mut none = Vec::new();
13373 subdivide_patch_to_triangles(&patch, &mut none, n, None, None, Some(&elsewhere));
13374 assert!(none.is_empty());
13375
13376 let band = ShadingCull::new(ctm, -200.0, 0.0, 1.0, 1.0, 600, 40).unwrap();
13378 let mut banded = Vec::new();
13379 subdivide_patch_to_triangles(&patch, &mut banded, n, None, None, Some(&band));
13380 let expected = all
13381 .iter()
13382 .filter(|tri| {
13383 !band.rejects_triangle(
13384 ctm.transform_point(tri.v0.x, tri.v0.y),
13385 ctm.transform_point(tri.v1.x, tri.v1.y),
13386 ctm.transform_point(tri.v2.x, tri.v2.y),
13387 )
13388 })
13389 .count();
13390 assert_eq!(banded.len(), expected);
13391 assert!(
13392 !banded.is_empty() && banded.len() < all.len(),
13393 "band should keep some but not all of {} triangles, kept {}",
13394 all.len(),
13395 banded.len()
13396 );
13397 }
13398
13399 #[cfg(feature = "ps-device")]
13400 #[test]
13401 fn test_create_device() {
13402 let dev = SkiaDevice::new(100, 100);
13403 assert_eq!(dev.page_size(), (100, 100));
13404 }
13405
13406 #[cfg(feature = "ps-device")]
13407 #[test]
13408 fn test_fill_rect() {
13409 let mut dev = SkiaDevice::new(100, 100);
13410 let mut path = PsPath::new();
13411 path.segments.push(PathSegment::MoveTo(10.0, 10.0));
13412 path.segments.push(PathSegment::LineTo(90.0, 10.0));
13413 path.segments.push(PathSegment::LineTo(90.0, 90.0));
13414 path.segments.push(PathSegment::LineTo(10.0, 90.0));
13415 path.segments.push(PathSegment::ClosePath);
13416
13417 let params = FillParams {
13418 color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
13419 fill_rule: FillRule::NonZeroWinding,
13420 ctm: Matrix::identity(),
13421 is_text_glyph: false,
13422 overprint: false,
13423 overprint_mode: 0,
13424 opm_paired: false,
13425 painted_channels: 0,
13426 is_device_cmyk: false,
13427 spot_color: None,
13428 icc_color: None,
13429 rendering_intent: 0,
13430 transfer: TransferState::default(),
13431 halftone: HalftoneState::default(),
13432 bg_ucr: BgUcrState::default(),
13433 alpha: 1.0,
13434 blend_mode: 0,
13435 alpha_is_shape: false,
13436 };
13437 dev.fill_path(&path, ¶ms);
13438
13439 let pixel = dev.pixmap().pixel(50, 50).unwrap();
13441 assert_eq!(pixel.red(), 255);
13442 assert_eq!(pixel.green(), 0);
13443 assert_eq!(pixel.blue(), 0);
13444 }
13445
13446 #[cfg(feature = "ps-device")]
13447 #[test]
13448 fn test_stroke_line() {
13449 let mut dev = SkiaDevice::new(100, 100);
13450 let mut path = PsPath::new();
13451 path.segments.push(PathSegment::MoveTo(10.0, 50.0));
13452 path.segments.push(PathSegment::LineTo(90.0, 50.0));
13453
13454 let params = StrokeParams {
13455 color: DeviceColor::from_rgb(0.0, 0.0, 1.0),
13456 line_width: 4.0,
13457 line_cap: LineCap::Butt,
13458 line_join: LineJoin::Miter,
13459 miter_limit: 10.0,
13460 dash_pattern: DashPattern::solid(),
13461 ctm: Matrix::identity(),
13462 stroke_adjust: false,
13463 is_text_glyph: false,
13464 overprint: false,
13465 overprint_mode: 0,
13466 opm_paired: false,
13467 painted_channels: 0,
13468 is_device_cmyk: false,
13469 spot_color: None,
13470 icc_color: None,
13471 rendering_intent: 0,
13472 transfer: TransferState::default(),
13473 halftone: HalftoneState::default(),
13474 bg_ucr: BgUcrState::default(),
13475 alpha: 1.0,
13476 blend_mode: 0,
13477 alpha_is_shape: false,
13478 };
13479 dev.stroke_path(&path, ¶ms);
13480
13481 let pixel = dev.pixmap().pixel(50, 50).unwrap();
13483 assert_eq!(pixel.blue(), 255);
13484 }
13485
13486 #[cfg(feature = "ps-device")]
13487 #[test]
13488 fn test_clip() {
13489 let mut dev = SkiaDevice::new(100, 100);
13490
13491 let mut clip_path = PsPath::new();
13493 clip_path.segments.push(PathSegment::MoveTo(0.0, 0.0));
13494 clip_path.segments.push(PathSegment::LineTo(50.0, 0.0));
13495 clip_path.segments.push(PathSegment::LineTo(50.0, 100.0));
13496 clip_path.segments.push(PathSegment::LineTo(0.0, 100.0));
13497 clip_path.segments.push(PathSegment::ClosePath);
13498
13499 let clip_params = ClipParams {
13500 fill_rule: FillRule::NonZeroWinding,
13501 ctm: Matrix::identity(),
13502 stroke_params: None,
13503 };
13504 dev.clip_path(&clip_path, &clip_params);
13505
13506 let mut fill_path = PsPath::new();
13508 fill_path.segments.push(PathSegment::MoveTo(0.0, 0.0));
13509 fill_path.segments.push(PathSegment::LineTo(100.0, 0.0));
13510 fill_path.segments.push(PathSegment::LineTo(100.0, 100.0));
13511 fill_path.segments.push(PathSegment::LineTo(0.0, 100.0));
13512 fill_path.segments.push(PathSegment::ClosePath);
13513
13514 let fill_params = FillParams {
13515 color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
13516 fill_rule: FillRule::NonZeroWinding,
13517 ctm: Matrix::identity(),
13518 is_text_glyph: false,
13519 overprint: false,
13520 overprint_mode: 0,
13521 opm_paired: false,
13522 painted_channels: 0,
13523 is_device_cmyk: false,
13524 spot_color: None,
13525 icc_color: None,
13526 rendering_intent: 0,
13527 transfer: TransferState::default(),
13528 halftone: HalftoneState::default(),
13529 bg_ucr: BgUcrState::default(),
13530 alpha: 1.0,
13531 blend_mode: 0,
13532 alpha_is_shape: false,
13533 };
13534 dev.fill_path(&fill_path, &fill_params);
13535
13536 let left_pixel = dev.pixmap().pixel(25, 50).unwrap();
13538 assert_eq!(left_pixel.red(), 255);
13539
13540 let right_pixel = dev.pixmap().pixel(75, 50).unwrap();
13542 assert_eq!(right_pixel.red(), 255);
13543 assert_eq!(right_pixel.green(), 255); }
13545
13546 #[cfg(feature = "ps-device")]
13547 #[test]
13548 fn test_erase_page() {
13549 let mut dev = SkiaDevice::new(100, 100);
13550 let mut path = PsPath::new();
13552 path.segments.push(PathSegment::MoveTo(0.0, 0.0));
13553 path.segments.push(PathSegment::LineTo(100.0, 0.0));
13554 path.segments.push(PathSegment::LineTo(100.0, 100.0));
13555 path.segments.push(PathSegment::LineTo(0.0, 100.0));
13556 path.segments.push(PathSegment::ClosePath);
13557 let params = FillParams {
13558 color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
13559 fill_rule: FillRule::NonZeroWinding,
13560 ctm: Matrix::identity(),
13561 is_text_glyph: false,
13562 overprint: false,
13563 overprint_mode: 0,
13564 opm_paired: false,
13565 painted_channels: 0,
13566 is_device_cmyk: false,
13567 spot_color: None,
13568 icc_color: None,
13569 rendering_intent: 0,
13570 transfer: TransferState::default(),
13571 halftone: HalftoneState::default(),
13572 bg_ucr: BgUcrState::default(),
13573 alpha: 1.0,
13574 blend_mode: 0,
13575 alpha_is_shape: false,
13576 };
13577 dev.fill_path(&path, ¶ms);
13578
13579 dev.erase_page();
13580
13581 let pixel = dev.pixmap().pixel(50, 50).unwrap();
13583 assert_eq!(pixel.red(), 255);
13584 assert_eq!(pixel.green(), 255);
13585 assert_eq!(pixel.blue(), 255);
13586 }
13587
13588 #[cfg(feature = "ps-device")]
13589 #[test]
13590 fn test_show_page() {
13591 let mut dev = SkiaDevice::new(10, 10);
13592 let path = std::env::temp_dir().join("stet_test_output.png");
13593 let path_str = path.to_string_lossy();
13594 let result = dev.show_page(&path_str);
13595 assert!(result.is_ok());
13596 assert!(path.exists());
13597 std::fs::remove_file(&path).ok();
13598 }
13599
13600 #[cfg(feature = "ps-device")]
13601 #[test]
13602 fn test_transform() {
13603 let mut dev = SkiaDevice::new(200, 200);
13604 let mut path = PsPath::new();
13606 path.segments.push(PathSegment::MoveTo(0.0, 0.0));
13607 path.segments.push(PathSegment::LineTo(10.0, 0.0));
13608 path.segments.push(PathSegment::LineTo(10.0, 10.0));
13609 path.segments.push(PathSegment::LineTo(0.0, 10.0));
13610 path.segments.push(PathSegment::ClosePath);
13611
13612 let params = FillParams {
13613 color: DeviceColor::from_rgb(0.0, 1.0, 0.0),
13614 fill_rule: FillRule::NonZeroWinding,
13615 ctm: Matrix::translate(100.0, 100.0),
13616 is_text_glyph: false,
13617 overprint: false,
13618 overprint_mode: 0,
13619 opm_paired: false,
13620 painted_channels: 0,
13621 is_device_cmyk: false,
13622 spot_color: None,
13623 icc_color: None,
13624 rendering_intent: 0,
13625 transfer: TransferState::default(),
13626 halftone: HalftoneState::default(),
13627 bg_ucr: BgUcrState::default(),
13628 alpha: 1.0,
13629 blend_mode: 0,
13630 alpha_is_shape: false,
13631 };
13632 dev.fill_path(&path, ¶ms);
13633
13634 let pixel = dev.pixmap().pixel(105, 105).unwrap();
13636 assert_eq!(pixel.green(), 255);
13637 assert_eq!(pixel.red(), 0);
13638 }
13639
13640 fn make_test_fill_at(x: f64, y: f64, w: f64, h: f64) -> DisplayElement {
13641 let mut path = PsPath::new();
13642 path.segments.push(PathSegment::MoveTo(x, y));
13643 path.segments.push(PathSegment::LineTo(x + w, y));
13644 path.segments.push(PathSegment::LineTo(x + w, y + h));
13645 path.segments.push(PathSegment::LineTo(x, y + h));
13646 path.segments.push(PathSegment::ClosePath);
13647 DisplayElement::Fill {
13648 path,
13649 params: FillParams {
13650 color: DeviceColor::from_rgb(0.0, 0.0, 0.0),
13651 fill_rule: FillRule::NonZeroWinding,
13652 ctm: Matrix::identity(),
13653 is_text_glyph: false,
13654 overprint: false,
13655 overprint_mode: 0,
13656 opm_paired: false,
13657 painted_channels: 0,
13658 is_device_cmyk: false,
13659 spot_color: None,
13660 icc_color: None,
13661 rendering_intent: 0,
13662 transfer: TransferState::default(),
13663 halftone: HalftoneState::default(),
13664 bg_ucr: BgUcrState::default(),
13665 alpha: 1.0,
13666 blend_mode: 0,
13667 alpha_is_shape: false,
13668 },
13669 }
13670 }
13671
13672 #[test]
13673 fn test_compute_paint_bounds_two_fills() {
13674 let mut list = DisplayList::new();
13675 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");
13679 assert!(
13680 (bounds.x_min - 10.0).abs() < 1e-9,
13681 "x_min was {}",
13682 bounds.x_min
13683 );
13684 assert!(
13685 (bounds.y_min - 20.0).abs() < 1e-9,
13686 "y_min was {}",
13687 bounds.y_min
13688 );
13689 assert!(
13690 (bounds.x_max - 150.0).abs() < 1e-9,
13691 "x_max was {}",
13692 bounds.x_max
13693 );
13694 assert!(
13695 (bounds.y_max - 75.0).abs() < 1e-9,
13696 "y_max was {}",
13697 bounds.y_max
13698 );
13699 }
13700
13701 #[test]
13702 fn test_compute_paint_bounds_empty_list() {
13703 let list = DisplayList::new();
13704 assert!(compute_paint_bounds(&list, 72.0).is_none());
13705 }
13706
13707 #[test]
13708 fn test_compute_paint_bounds_only_clip_returns_none() {
13709 let mut list = DisplayList::new();
13710 list.push(DisplayElement::InitClip);
13711 assert!(compute_paint_bounds(&list, 72.0).is_none());
13714 }
13715
13716 #[test]
13717 fn test_rasterize_mask_anchors_to_paint_bounds() {
13718 use stet_graphics::display_list::{SoftMaskParams, SoftMaskSubtype};
13719
13720 let mut mask = DisplayList::new();
13723 let mut path = PsPath::new();
13724 path.segments.push(PathSegment::MoveTo(200.0, 300.0));
13725 path.segments.push(PathSegment::LineTo(250.0, 300.0));
13726 path.segments.push(PathSegment::LineTo(250.0, 340.0));
13727 path.segments.push(PathSegment::LineTo(200.0, 340.0));
13728 path.segments.push(PathSegment::ClosePath);
13729 mask.push(DisplayElement::Fill {
13730 path,
13731 params: FillParams {
13732 color: DeviceColor::from_rgb(1.0, 1.0, 1.0),
13733 fill_rule: FillRule::NonZeroWinding,
13734 ctm: Matrix::identity(),
13735 is_text_glyph: false,
13736 overprint: false,
13737 overprint_mode: 0,
13738 opm_paired: false,
13739 painted_channels: 0,
13740 is_device_cmyk: false,
13741 spot_color: None,
13742 icc_color: None,
13743 rendering_intent: 0,
13744 transfer: TransferState::default(),
13745 halftone: HalftoneState::default(),
13746 bg_ucr: BgUcrState::default(),
13747 alpha: 1.0,
13748 blend_mode: 0,
13749 alpha_is_shape: false,
13750 },
13751 });
13752
13753 let params = SoftMaskParams {
13754 subtype: SoftMaskSubtype::Luminosity,
13755 bbox: [0.0, 0.0, 100.0, 100.0],
13758 backdrop_color: None, transfer_invert: false,
13760 has_nested_mask_scope: false,
13761 parent_clip_bbox: None,
13762 };
13763
13764 let raster = rasterize_mask(
13765 &mask,
13766 ¶ms,
13767 None,
13768 false,
13769 72.0,
13770 1.0,
13771 1.0,
13772 &LayerSet::new(),
13773 )
13774 .expect("expected raster");
13775
13776 assert_eq!(raster.origin_x, 199);
13779 assert_eq!(raster.origin_y, 299);
13780 assert_eq!(raster.width, 52);
13782 assert_eq!(raster.height, 42);
13783 assert_eq!(raster.scale_x, 1.0);
13784 assert_eq!(raster.scale_y, 1.0);
13785
13786 let mx = 225 - raster.origin_x;
13790 let my = 320 - raster.origin_y;
13791 assert!(mx >= 0 && (mx as u32) < raster.width);
13792 assert!(my >= 0 && (my as u32) < raster.height);
13793 let center_value = raster.data[(my as usize) * raster.width as usize + mx as usize];
13794 assert_eq!(
13795 center_value, 255,
13796 "center of painted mask should be opaque white (lum=255)"
13797 );
13798
13799 let mx_out = 300 - raster.origin_x;
13803 let in_bounds = mx_out >= 0 && (mx_out as u32) < raster.width;
13804 assert!(!in_bounds, "page x=300 should be outside the mask raster");
13805 assert_eq!(
13806 out_of_bounds_mask_value(¶ms),
13807 0,
13808 "black backdrop → out-of-bounds = 0"
13809 );
13810 }
13811
13812 #[test]
13813 fn test_band_local_to_mask_formula() {
13814 let raster_origin_x = 200i32;
13825 let raster_origin_y = 300i32;
13826
13827 let sample = |vp_x_dev: f32,
13829 vp_y_dev: f32,
13830 scale: f32,
13831 crop_x: i32,
13832 crop_y: i32,
13833 x: i32,
13834 y: i32|
13835 -> (i32, i32) {
13836 let vp_x_pixels = (vp_x_dev * scale).round() as i32;
13837 let vp_y_pixels = (vp_y_dev * scale).round() as i32;
13838 let page_x = vp_x_pixels + crop_x + x;
13839 let page_y = vp_y_pixels + crop_y + y;
13840 let mx = page_x - raster_origin_x;
13841 let my = page_y - raster_origin_y;
13842 (mx, my)
13843 };
13844
13845 let (mx, my) = sample(0.0, 0.0, 1.0, 220, 310, 0, 0);
13850 assert_eq!((mx, my), (20, 10), "top band: smask top-left");
13851
13852 let (mx, my) = sample(0.0, 0.0, 1.0, 220, 310, 5, 5);
13854 assert_eq!((mx, my), (25, 15), "top band: 5px into smask");
13855
13856 let (mx, my) = sample(0.0, 305.0, 1.0, 220, 5, 0, 0);
13864 assert_eq!((mx, my), (20, 10), "mid band: smask top-left");
13865
13866 let vp_x_pixels = (100.0_f32 * 2.0).round() as i32;
13878 let crop_x = ((220.0_f32 - 100.0) * 2.0).floor() as i32;
13879 let page_x_for_x_zero = vp_x_pixels + crop_x;
13880 assert_eq!(page_x_for_x_zero, 440, "viewport scale-2: page-x at x=0");
13881 }
13882
13883 fn x_path() -> PsPath {
13886 let mut p = PsPath::new();
13887 p.segments.push(PathSegment::MoveTo(10.0, 10.0));
13888 p.segments.push(PathSegment::LineTo(20.0, 20.0));
13889 p.segments.push(PathSegment::LineTo(30.0, 10.0));
13890 p.segments.push(PathSegment::LineTo(20.0, 0.0));
13891 p.segments.push(PathSegment::ClosePath);
13892 p
13893 }
13894
13895 fn x_path_perturbed() -> PsPath {
13896 let mut p = PsPath::new();
13899 p.segments.push(PathSegment::MoveTo(10.001, 10.0));
13900 p.segments.push(PathSegment::LineTo(20.0, 19.999));
13901 p.segments.push(PathSegment::LineTo(30.002, 10.001));
13902 p.segments.push(PathSegment::LineTo(19.999, 0.0));
13903 p.segments.push(PathSegment::ClosePath);
13904 p
13905 }
13906
13907 fn fill(path: PsPath, alpha: f64, blend: u8) -> DisplayElement {
13908 DisplayElement::Fill {
13909 path,
13910 params: FillParams {
13911 color: DeviceColor::from_rgb(0.0, 0.0, 0.0),
13912 fill_rule: FillRule::NonZeroWinding,
13913 ctm: Matrix::identity(),
13914 is_text_glyph: false,
13915 overprint: false,
13916 overprint_mode: 0,
13917 opm_paired: false,
13918 painted_channels: 0,
13919 is_device_cmyk: false,
13920 spot_color: None,
13921 icc_color: None,
13922 rendering_intent: 0,
13923 transfer: TransferState::default(),
13924 halftone: HalftoneState::default(),
13925 bg_ucr: BgUcrState::default(),
13926 alpha,
13927 blend_mode: blend,
13928 alpha_is_shape: false,
13929 },
13930 }
13931 }
13932
13933 fn rect_path(x0: f64, y0: f64, x1: f64, y1: f64) -> PsPath {
13934 let mut p = PsPath::new();
13935 p.segments.push(PathSegment::MoveTo(x0, y0));
13936 p.segments.push(PathSegment::LineTo(x1, y0));
13937 p.segments.push(PathSegment::LineTo(x1, y1));
13938 p.segments.push(PathSegment::LineTo(x0, y1));
13939 p.segments.push(PathSegment::ClosePath);
13940 p
13941 }
13942
13943 fn clip_elem(path: PsPath) -> DisplayElement {
13944 DisplayElement::Clip {
13945 path,
13946 params: ClipParams {
13947 fill_rule: FillRule::NonZeroWinding,
13948 ctm: Matrix::identity(),
13949 stroke_params: None,
13950 },
13951 }
13952 }
13953
13954 fn group_elem(
13955 inner: Vec<DisplayElement>,
13956 bbox: [f64; 4],
13957 isolated: bool,
13958 alpha: f64,
13959 blend: u8,
13960 ) -> DisplayElement {
13961 let mut dl = DisplayList::new();
13962 for e in inner {
13963 dl.push(e);
13964 }
13965 DisplayElement::Group {
13966 elements: dl,
13967 params: stet_graphics::display_list::GroupParams {
13968 bbox,
13969 isolated,
13970 knockout: false,
13971 blend_mode: blend,
13972 alpha,
13973 color_space: stet_graphics::display_list::GroupColorSpace::Inherited,
13974 },
13975 }
13976 }
13977
13978 fn dl(elements: Vec<DisplayElement>) -> DisplayList {
13979 let mut d = DisplayList::new();
13980 for e in elements {
13981 d.push(e);
13982 }
13983 d
13984 }
13985
13986 #[test]
13987 fn obscured_skip_fires_on_matching_fill_plus_iso_group() {
13988 let parent = fill(x_path(), 1.0, 0);
13991 let inner = vec![fill(x_path_perturbed(), 1.0, 0)];
13992 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
13993 let d = dl(vec![
13994 parent,
13995 clip_elem(rect_path(0.0, -5.0, 40.0, 30.0)),
13996 grp,
13997 ]);
13998 assert_eq!(compute_obscured_fill_skips(&d), vec![0]);
13999 }
14000
14001 #[test]
14002 fn obscured_skip_does_not_fire_on_non_isolated_group() {
14003 let parent = fill(x_path(), 1.0, 0);
14004 let inner = vec![fill(x_path(), 1.0, 0)];
14005 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], false, 1.0, 0);
14006 let d = dl(vec![parent, grp]);
14007 assert!(compute_obscured_fill_skips(&d).is_empty());
14008 }
14009
14010 #[test]
14011 fn obscured_skip_does_not_fire_on_partial_alpha_group() {
14012 let parent = fill(x_path(), 1.0, 0);
14013 let inner = vec![fill(x_path(), 1.0, 0)];
14014 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 0.5, 0);
14015 let d = dl(vec![parent, grp]);
14016 assert!(compute_obscured_fill_skips(&d).is_empty());
14017 }
14018
14019 #[test]
14020 fn obscured_skip_does_not_fire_on_non_normal_blend() {
14021 let parent = fill(x_path(), 1.0, 0);
14022 let inner = vec![fill(x_path(), 1.0, 0)];
14023 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 10);
14026 let d = dl(vec![parent, grp]);
14027 assert!(compute_obscured_fill_skips(&d).is_empty());
14028 }
14029
14030 #[test]
14031 fn obscured_skip_does_not_fire_when_paths_differ() {
14032 let parent = fill(rect_path(0.0, 0.0, 5.0, 5.0), 1.0, 0);
14033 let inner = vec![fill(x_path(), 1.0, 0)];
14034 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
14035 let d = dl(vec![parent, grp]);
14036 assert!(compute_obscured_fill_skips(&d).is_empty());
14037 }
14038
14039 #[test]
14040 fn obscured_skip_does_not_fire_when_group_bbox_too_small() {
14041 let big = rect_path(0.0, 0.0, 100.0, 100.0);
14045 let parent = fill(big.clone(), 1.0, 0);
14046 let inner = vec![fill(big, 1.0, 0)];
14047 let grp = group_elem(inner, [0.0, 0.0, 10.0, 10.0], true, 1.0, 0);
14049 let d = dl(vec![parent, grp]);
14050 assert!(compute_obscured_fill_skips(&d).is_empty());
14051 }
14052
14053 #[test]
14054 fn obscured_skip_does_not_fire_when_intervening_clip_too_small() {
14055 let parent = fill(x_path(), 1.0, 0);
14059 let narrow_clip = clip_elem(rect_path(12.0, 5.0, 18.0, 15.0));
14060 let inner = vec![fill(x_path(), 1.0, 0)];
14061 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
14062 let d = dl(vec![parent, narrow_clip, grp]);
14063 assert!(compute_obscured_fill_skips(&d).is_empty());
14064 }
14065
14066 #[test]
14067 fn obscured_skip_does_not_fire_when_inner_clip_too_small() {
14068 let parent = fill(x_path(), 1.0, 0);
14070 let inner = vec![
14071 clip_elem(rect_path(12.0, 5.0, 18.0, 15.0)),
14072 fill(x_path(), 1.0, 0),
14073 ];
14074 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
14075 let d = dl(vec![parent, grp]);
14076 assert!(compute_obscured_fill_skips(&d).is_empty());
14077 }
14078
14079 #[test]
14080 fn obscured_skip_fires_when_inner_clip_is_wider_than_parent_path() {
14081 let parent = fill(x_path(), 1.0, 0);
14084 let inner = vec![
14085 clip_elem(rect_path(-10.0, -10.0, 40.0, 30.0)),
14086 fill(x_path_perturbed(), 1.0, 0),
14087 ];
14088 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
14089 let d = dl(vec![parent, grp]);
14090 assert_eq!(compute_obscured_fill_skips(&d), vec![0]);
14091 }
14092
14093 #[test]
14094 fn obscured_skip_does_not_fire_on_partial_alpha_parent() {
14095 let parent = fill(x_path(), 0.5, 0);
14098 let inner = vec![fill(x_path(), 1.0, 0)];
14099 let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
14100 let d = dl(vec![parent, grp]);
14101 assert!(compute_obscured_fill_skips(&d).is_empty());
14102 }
14103}