1use crate::color::{blend_cmyk, preserve_device_cmyk, rgba_to_cmyk_buffer};
12use kurbo::{Affine, BezPath, Point, Rect, Shape};
13use pdf_interpret::font::Glyph;
14use pdf_interpret::{
15 interpret_page, BlendMode, ClipPath, Context, Device, FillRule, GlyphDrawMode,
16 Image as PdfImage, InterpreterSettings, Paint, PathDrawMode, RasterImage, SoftMask,
17};
18use pdf_render::pdf_interpret::PageExt;
19use pdf_render::pdf_syntax::page::Page;
20use pdf_render::vello_cpu::color::palette::css::WHITE;
21use pdf_render::vello_cpu::color::{AlphaColor, Srgb};
22use pdf_render::vello_cpu::peniko::Fill as PenikoFill;
23use pdf_render::vello_cpu::{
24 Level, Pixmap, RenderContext, RenderMode, RenderSettings as CpuRenderSettings,
25};
26pub use pdf_render::RasterQuality;
27use pdf_render::{render, RenderSettings};
28
29const AXIS_EPSILON: f64 = 1e-5;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum ColorMode {
34 #[default]
36 Srgb,
37 PreserveCmyk,
39 SimulateCmyk,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
45pub enum PixelFormat {
46 #[default]
48 Rgba8,
49 Cmyk8,
51}
52
53#[derive(Debug, Clone)]
58pub struct RenderConfig {
59 pub color_mode: ColorMode,
61 pub dpi: u32,
63}
64
65impl Default for RenderConfig {
66 fn default() -> Self {
67 Self {
68 color_mode: ColorMode::default(),
69 dpi: 72,
70 }
71 }
72}
73
74impl From<&RenderConfig> for RenderOptions {
75 fn from(cfg: &RenderConfig) -> Self {
76 RenderOptions {
77 dpi: cfg.dpi as f64,
78 ..Default::default()
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
85pub struct RenderOptions {
86 pub dpi: f64,
88 pub background: [f32; 4],
90 pub render_annotations: bool,
92 pub width: Option<u16>,
94 pub height: Option<u16>,
96 pub max_pixels: Option<u32>,
105 pub quality: RasterQuality,
113}
114
115impl Default for RenderOptions {
116 fn default() -> Self {
117 Self {
118 dpi: 72.0,
119 background: [1.0, 1.0, 1.0, 1.0],
120 render_annotations: true,
121 width: None,
122 height: None,
123 max_pixels: None,
124 quality: RasterQuality::Quality,
125 }
126 }
127}
128
129#[derive(Debug, Clone)]
131pub struct RenderedPage {
132 pub width: u32,
134 pub height: u32,
136 pub pixel_format: PixelFormat,
138 pub pixels: Vec<u8>,
140}
141
142struct CmykOverlay {
143 data: Vec<Option<[u8; 4]>>,
144 width: u32,
145 height: u32,
146}
147
148impl CmykOverlay {
149 fn new(width: u32, height: u32) -> Self {
150 Self {
151 data: vec![None; width as usize * height as usize],
152 width,
153 height,
154 }
155 }
156
157 fn apply_mask(&mut self, alpha_mask: &[u8], cmyk: [u8; 4]) {
158 for (slot, alpha) in self.data.iter_mut().zip(alpha_mask.iter().copied()) {
159 match alpha {
160 0 => {}
161 255 => *slot = Some(cmyk),
162 partial => {
163 if let Some(existing) = *slot {
164 *slot = Some(blend_cmyk(existing, cmyk, partial));
165 } else {
166 *slot = None;
167 }
168 }
169 }
170 }
171 }
172
173 fn contaminate(&mut self, alpha_mask: &[u8]) {
174 for (slot, alpha) in self.data.iter_mut().zip(alpha_mask.iter().copied()) {
175 if alpha != 0 {
176 *slot = None;
177 }
178 }
179 }
180
181 fn set_exact_pixel(&mut self, x: u32, y: u32, cmyk: [u8; 4]) {
182 if x >= self.width || y >= self.height {
183 return;
184 }
185 let idx = y as usize * self.width as usize + x as usize;
186 self.data[idx] = Some(cmyk);
187 }
188
189 fn compose_with_rgba_fallback(self, rgba: &[u8]) -> Vec<u8> {
190 let mut out = rgba_to_cmyk_buffer(rgba);
191 for (idx, exact) in self.data.iter().enumerate() {
192 if let Some(exact) = exact {
193 let start = idx * 4;
194 out[start..start + 4].copy_from_slice(exact);
195 }
196 }
197 out
198 }
199}
200
201struct GroupState {
202 previous_opacity: f32,
203 unsupported: bool,
204}
205
206struct CmykOverlayDevice {
207 overlay: CmykOverlay,
208 clip_stack: Vec<ClipPath>,
209 current_opacity: f32,
210 current_soft_mask: bool,
211 current_blend_mode: BlendMode,
212 group_stack: Vec<GroupState>,
213 unsupported_depth: usize,
214 cpu_settings: CpuRenderSettings,
215}
216
217impl CmykOverlayDevice {
218 fn new(width: u16, height: u16) -> Self {
219 Self {
220 overlay: CmykOverlay::new(width as u32, height as u32),
221 clip_stack: Vec::new(),
222 current_opacity: 1.0,
223 current_soft_mask: false,
224 current_blend_mode: BlendMode::Normal,
225 group_stack: Vec::new(),
226 unsupported_depth: 0,
227 cpu_settings: CpuRenderSettings {
228 level: Level::new(),
229 num_threads: 0,
230 render_mode: RenderMode::OptimizeQuality,
233 },
234 }
235 }
236
237 fn finish(self) -> CmykOverlay {
238 self.overlay
239 }
240
241 fn exact_cmyk_for_paint(&self, paint: &Paint<'_>) -> Option<[u8; 4]> {
242 if !self.can_preserve_exact() {
243 return None;
244 }
245
246 match paint {
247 Paint::Color(color) => color
248 .device_cmyk_components()
249 .map(|[c, m, y, k]| preserve_device_cmyk(c, m, y, k)),
250 Paint::Pattern(_) => None,
251 }
252 }
253
254 fn paint_opacity(&self, paint: &Paint<'_>) -> f32 {
255 let local = match paint {
256 Paint::Color(color) => color.opacity(),
257 Paint::Pattern(_) => 1.0,
258 };
259 (local * self.current_opacity).clamp(0.0, 1.0)
260 }
261
262 fn can_preserve_exact(&self) -> bool {
263 self.unsupported_depth == 0
264 && !self.current_soft_mask
265 && self.current_blend_mode == BlendMode::Normal
266 }
267
268 fn handle_path_operation(
269 &mut self,
270 path: &BezPath,
271 transform: Affine,
272 paint: &Paint<'_>,
273 draw_mode: &PathDrawMode,
274 is_text: bool,
275 ) {
276 let alpha = self.paint_opacity(paint);
277 if alpha <= 0.0 {
278 return;
279 }
280
281 let mask = self.rasterize_path_mask(path, transform, draw_mode, alpha, is_text);
282 if let Some(cmyk) = self.exact_cmyk_for_paint(paint) {
283 self.overlay.apply_mask(&mask, cmyk);
284 } else {
285 self.overlay.contaminate(&mask);
286 }
287 }
288
289 fn rasterize_path_mask(
290 &self,
291 path: &BezPath,
292 transform: Affine,
293 draw_mode: &PathDrawMode,
294 alpha: f32,
295 is_text: bool,
296 ) -> Vec<u8> {
297 let mut ctx = RenderContext::new_with(
298 self.overlay.width as u16,
299 self.overlay.height as u16,
300 self.cpu_settings,
301 );
302 self.apply_clip_stack(&mut ctx);
303 ctx.set_paint(AlphaColor::<Srgb>::new([1.0, 1.0, 1.0, alpha]));
304 ctx.set_transform(transform);
305
306 match draw_mode {
307 PathDrawMode::Fill(fill_rule) => {
308 ctx.set_fill_rule(convert_fill_rule(*fill_rule));
309 ctx.fill_path(path);
310 }
311 PathDrawMode::Stroke(stroke_props) => {
312 ctx.set_stroke(stroke_for_path(transform, stroke_props, is_text));
313 ctx.stroke_path(path);
314 }
315 }
316
317 self.finish_mask(ctx)
318 }
319
320 fn rasterize_rect_mask(&self, rect: &Rect, transform: Affine, alpha: f32) -> Vec<u8> {
321 self.rasterize_path_mask(
322 &rect.to_path(0.1),
323 transform,
324 &PathDrawMode::Fill(FillRule::NonZero),
325 alpha,
326 false,
327 )
328 }
329
330 fn finish_mask(&self, mut ctx: RenderContext) -> Vec<u8> {
331 let mut pixmap = Pixmap::new(self.overlay.width as u16, self.overlay.height as u16);
332 ctx.flush();
333 ctx.render_to_pixmap(&mut pixmap);
334 pixmap
335 .data_as_u8_slice()
336 .chunks_exact(4)
337 .map(|px| px[3])
338 .collect()
339 }
340
341 fn apply_clip_stack(&self, ctx: &mut RenderContext) {
342 for clip in &self.clip_stack {
343 let old_transform = *ctx.transform();
344 ctx.set_fill_rule(convert_fill_rule(clip.fill));
345 ctx.set_transform(Affine::IDENTITY);
346 ctx.push_clip_path(&clip.path);
347 ctx.set_transform(old_transform);
348 }
349 }
350
351 fn handle_raster_image(&mut self, image: &RasterImage<'_>, transform: Affine) -> bool {
352 if !self.can_preserve_exact()
353 || (self.current_opacity - 1.0).abs() > f32::EPSILON
354 || self.clip_stack.len() > 1
355 {
356 return false;
357 }
358
359 let mut preserved = false;
360 image.with_device_cmyk(
361 |cmyk, alpha| {
362 if alpha.is_some() {
363 return;
364 }
365 preserved = self.apply_axis_aligned_image(transform, cmyk);
366 },
367 None,
368 );
369 preserved
370 }
371
372 fn apply_axis_aligned_image(
373 &mut self,
374 transform: Affine,
375 cmyk: pdf_interpret::CmykData,
376 ) -> bool {
377 let transform = transform
378 * Affine::scale_non_uniform(cmyk.scale_factors.0 as f64, cmyk.scale_factors.1 as f64);
379 let [sx, shy, shx, sy, tx, ty] = transform.as_coeffs();
380 if shy.abs() > AXIS_EPSILON || shx.abs() > AXIS_EPSILON {
381 return false;
382 }
383 if sx.abs() <= AXIS_EPSILON || sy.abs() <= AXIS_EPSILON {
384 return false;
385 }
386
387 let bounds = (transform
388 * Rect::new(0.0, 0.0, cmyk.width as f64, cmyk.height as f64).to_path(0.1))
389 .bounding_box()
390 .intersect(Rect::new(
391 0.0,
392 0.0,
393 self.overlay.width as f64,
394 self.overlay.height as f64,
395 ));
396 if bounds.width() <= 0.0 || bounds.height() <= 0.0 {
397 return true;
398 }
399
400 let min_x = bounds.x0.floor().max(0.0) as u32;
401 let max_x = bounds.x1.ceil().min(self.overlay.width as f64) as u32;
402 let min_y = bounds.y0.floor().max(0.0) as u32;
403 let max_y = bounds.y1.ceil().min(self.overlay.height as f64) as u32;
404 let inv_sx = 1.0 / sx;
405 let inv_sy = 1.0 / sy;
406
407 for y in min_y..max_y {
408 for x in min_x..max_x {
409 let src_x = ((x as f64 + 0.5) - tx) * inv_sx;
410 let src_y = ((y as f64 + 0.5) - ty) * inv_sy;
411 if src_x < 0.0
412 || src_x >= cmyk.width as f64
413 || src_y < 0.0
414 || src_y >= cmyk.height as f64
415 {
416 continue;
417 }
418
419 let src_x = src_x.floor() as usize;
420 let src_y = src_y.floor() as usize;
421 let idx = (src_y * cmyk.width as usize + src_x) * 4;
422 self.overlay.set_exact_pixel(
423 x,
424 y,
425 [
426 cmyk.data[idx],
427 cmyk.data[idx + 1],
428 cmyk.data[idx + 2],
429 cmyk.data[idx + 3],
430 ],
431 );
432 }
433 }
434
435 true
436 }
437
438 fn handle_image_fallback(&mut self, image: &PdfImage<'_, '_>, transform: Affine, alpha: f32) {
439 if alpha <= 0.0 {
440 return;
441 }
442 let rect = Rect::new(0.0, 0.0, image.width() as f64, image.height() as f64);
443 let mask = self.rasterize_rect_mask(&rect, transform, alpha);
444 self.overlay.contaminate(&mask);
445 }
446}
447
448impl<'a> Device<'a> for CmykOverlayDevice {
449 fn set_soft_mask(&mut self, mask: Option<SoftMask<'a>>) {
450 self.current_soft_mask = mask.is_some();
451 }
452
453 fn set_blend_mode(&mut self, blend_mode: BlendMode) {
454 self.current_blend_mode = blend_mode;
455 }
456
457 fn draw_path(
458 &mut self,
459 path: &BezPath,
460 transform: Affine,
461 paint: &Paint<'a>,
462 draw_mode: &PathDrawMode,
463 ) {
464 self.handle_path_operation(path, transform, paint, draw_mode, false);
465 }
466
467 fn push_clip_path(&mut self, clip_path: &ClipPath) {
468 self.clip_stack.push(clip_path.clone());
469 }
470
471 fn push_transparency_group(
472 &mut self,
473 opacity: f32,
474 mask: Option<SoftMask<'a>>,
475 blend_mode: BlendMode,
476 ) {
477 let unsupported = mask.is_some() || blend_mode != BlendMode::Normal;
478 self.group_stack.push(GroupState {
479 previous_opacity: self.current_opacity,
480 unsupported,
481 });
482 self.current_opacity = (self.current_opacity * opacity).clamp(0.0, 1.0);
483 if unsupported {
484 self.unsupported_depth += 1;
485 }
486 }
487
488 fn draw_glyph(
489 &mut self,
490 glyph: &Glyph<'a>,
491 transform: Affine,
492 glyph_transform: Affine,
493 paint: &Paint<'a>,
494 draw_mode: &GlyphDrawMode,
495 ) {
496 match draw_mode {
497 GlyphDrawMode::Invisible => {}
498 GlyphDrawMode::Fill => match glyph {
499 Glyph::Outline(outline) => {
500 self.handle_path_operation(
501 &outline.outline(),
502 transform * glyph_transform,
503 paint,
504 &PathDrawMode::Fill(FillRule::NonZero),
505 true,
506 );
507 }
508 Glyph::Type3(type3) => {
509 type3.interpret(self, transform, glyph_transform, paint);
510 }
511 },
512 GlyphDrawMode::Stroke(stroke_props) => match glyph {
513 Glyph::Outline(outline) => {
514 let path = glyph_transform * outline.outline();
515 self.handle_path_operation(
516 &path,
517 transform,
518 paint,
519 &PathDrawMode::Stroke(stroke_props.clone()),
520 true,
521 );
522 }
523 Glyph::Type3(type3) => {
524 type3.interpret(self, transform, glyph_transform, paint);
525 }
526 },
527 }
528 }
529
530 fn draw_image(&mut self, image: PdfImage<'a, '_>, transform: Affine) {
531 match &image {
532 PdfImage::Raster(raster) if self.handle_raster_image(raster, transform) => {}
533 PdfImage::Stencil(stencil) => {
534 let _ = stencil;
535 self.handle_image_fallback(&image, transform, self.current_opacity);
536 }
537 PdfImage::Raster(_) => {
538 self.handle_image_fallback(&image, transform, self.current_opacity);
539 }
540 }
541 }
542
543 fn pop_clip_path(&mut self) {
544 let _ = self.clip_stack.pop();
545 }
546
547 fn pop_transparency_group(&mut self) {
548 if let Some(state) = self.group_stack.pop() {
549 self.current_opacity = state.previous_opacity;
550 if state.unsupported {
551 self.unsupported_depth = self.unsupported_depth.saturating_sub(1);
552 }
553 }
554 }
555}
556
557pub(crate) fn render_page(
559 page: &Page<'_>,
560 options: &RenderOptions,
561 settings: &InterpreterSettings,
562) -> RenderedPage {
563 let (width, height, pixels) = render_rgba_pixels(page, options, settings);
564 RenderedPage {
565 width,
566 height,
567 pixel_format: PixelFormat::Rgba8,
568 pixels,
569 }
570}
571
572pub(crate) fn render_page_with_config(
579 page: &Page<'_>,
580 config: &RenderConfig,
581 settings: &InterpreterSettings,
582) -> RenderedPage {
583 let options = RenderOptions::from(config);
584 match config.color_mode {
585 ColorMode::Srgb | ColorMode::SimulateCmyk => render_page(page, &options, settings),
586 ColorMode::PreserveCmyk => {
587 let (width, height, rgba) = render_rgba_pixels(page, &options, settings);
588 let overlay = build_cmyk_overlay(page, &options, settings, width, height);
589 RenderedPage {
590 width,
591 height,
592 pixel_format: PixelFormat::Cmyk8,
593 pixels: overlay.compose_with_rgba_fallback(&rgba),
594 }
595 }
596 }
597}
598
599pub(crate) fn render_thumbnail(
601 page: &Page<'_>,
602 max_dimension: u32,
603 settings: &InterpreterSettings,
604) -> RenderedPage {
605 let (w, h) = page.render_dimensions();
606 let longest = w.max(h) as f64;
607 let scale = (max_dimension as f64 / longest) as f32;
608
609 let rs = RenderSettings {
610 x_scale: scale,
611 y_scale: scale,
612 bg_color: WHITE,
613 ..Default::default()
614 };
615
616 let pixmap = render(page, settings, &rs);
617 let pw = pixmap.width() as u32;
618 let ph = pixmap.height() as u32;
619 let pixels = pixmap.data_as_u8_slice().to_vec();
620
621 RenderedPage {
622 width: pw,
623 height: ph,
624 pixel_format: PixelFormat::Rgba8,
625 pixels,
626 }
627}
628
629fn build_cmyk_overlay(
630 page: &Page<'_>,
631 options: &RenderOptions,
632 settings: &InterpreterSettings,
633 width: u32,
634 height: u32,
635) -> CmykOverlay {
636 let scale = (options.dpi / 72.0) as f32;
637 let initial_transform =
638 Affine::scale_non_uniform(scale as f64, scale as f64) * page.initial_transform(true);
639 let mut isettings = settings.clone();
640 isettings.render_annotations = options.render_annotations;
641
642 let mut ctx = Context::new(
643 initial_transform,
644 Rect::new(0.0, 0.0, width as f64, height as f64),
645 page.xref(),
646 isettings.clone(),
647 );
648 let mut device = CmykOverlayDevice::new(width as u16, height as u16);
649
650 device.push_clip_path(&ClipPath {
651 path: Rect::new(0.0, 0.0, width as f64, height as f64).to_path(0.1),
652 fill: FillRule::NonZero,
653 });
654 device.push_transparency_group(1.0, None, BlendMode::Normal);
655 interpret_page(page, &mut ctx, &mut device);
656 device.pop_transparency_group();
657 device.pop_clip_path();
658
659 device.finish()
660}
661
662fn render_rgba_pixels(
663 page: &Page<'_>,
664 options: &RenderOptions,
665 settings: &InterpreterSettings,
666) -> (u32, u32, Vec<u8>) {
667 let mut scale = (options.dpi / 72.0) as f32;
668
669 if let Some(budget) = options.max_pixels {
672 if options.width.is_none() && options.height.is_none() && budget > 0 && scale > 0.0 {
673 let (base_w, base_h) = page.render_dimensions();
674 let predicted = (base_w as f64 * scale as f64) * (base_h as f64 * scale as f64);
675 if predicted > budget as f64 {
676 let factor = (budget as f64 / predicted).sqrt() as f32;
677 scale *= factor;
678 }
679 }
680 }
681
682 let bg = AlphaColor::<Srgb>::new(options.background);
683
684 let rs = RenderSettings {
685 x_scale: scale,
686 y_scale: scale,
687 width: options.width,
688 height: options.height,
689 bg_color: bg,
690 quality: options.quality,
691 };
692
693 let mut isettings = settings.clone();
694 isettings.render_annotations = options.render_annotations;
695
696 let pixmap = render(page, &isettings, &rs);
697 let width = pixmap.width() as u32;
698 let height = pixmap.height() as u32;
699 let pixels = pixmap.data_as_u8_slice().to_vec();
700 (width, height, pixels)
701}
702
703fn stroke_for_path(
704 transform: Affine,
705 stroke_props: &pdf_interpret::StrokeProps,
706 is_text: bool,
707) -> kurbo::Stroke {
708 let threshold = if is_text { 0.25 } else { 1.0 };
709 let min_factor = max_factor(&transform);
710 let mut line_width = stroke_props.line_width.max(0.01);
711 let transformed_width = line_width * min_factor;
712
713 if transformed_width < threshold && transformed_width > 0.0 {
714 line_width /= transformed_width;
715 line_width *= threshold;
716 }
717
718 kurbo::Stroke {
719 width: line_width as f64,
720 join: stroke_props.line_join,
721 miter_limit: stroke_props.miter_limit as f64,
722 start_cap: stroke_props.line_cap,
723 end_cap: stroke_props.line_cap,
724 dash_pattern: stroke_props.dash_array.iter().map(|n| *n as f64).collect(),
725 dash_offset: stroke_props.dash_offset as f64,
726 }
727}
728
729fn max_factor(transform: &Affine) -> f32 {
730 let [a, b, c, d, _, _] = transform.as_coeffs();
731 let x_advance = Affine::new([a, b, c, d, 0.0, 0.0]) * Point::new(1.0, 0.0);
732 let y_advance = Affine::new([a, b, c, d, 0.0, 0.0]) * Point::new(0.0, 1.0);
733 x_advance
734 .to_vec2()
735 .length()
736 .max(y_advance.to_vec2().length()) as f32
737}
738
739fn convert_fill_rule(fill_rule: FillRule) -> PenikoFill {
740 match fill_rule {
741 FillRule::NonZero => PenikoFill::NonZero,
742 FillRule::EvenOdd => PenikoFill::EvenOdd,
743 }
744}
745
746#[cfg(test)]
747mod tests {
748 use super::*;
749
750 #[test]
751 fn render_options_defaults() {
752 let opts = RenderOptions::default();
753 assert!((opts.dpi - 72.0).abs() < f64::EPSILON);
754 assert!(opts.render_annotations);
755 assert!(opts.width.is_none());
756 assert!(opts.height.is_none());
757 assert!(opts.max_pixels.is_none());
758 assert_eq!(opts.quality, RasterQuality::Quality);
761 }
762
763 #[test]
764 fn render_config_defaults() {
765 let cfg = RenderConfig::default();
766 assert_eq!(cfg.color_mode, ColorMode::Srgb);
767 assert_eq!(cfg.dpi, 72);
768 }
769
770 #[test]
771 fn rendered_page_empty() {
772 let p = RenderedPage {
773 width: 10,
774 height: 20,
775 pixel_format: PixelFormat::Rgba8,
776 pixels: vec![0; 10 * 20 * 4],
777 };
778 assert_eq!(p.pixels.len(), 800);
779 }
780
781 #[test]
782 fn rgba_to_cmyk_black() {
783 let buf = crate::color::rgba_to_cmyk_buffer(&[0, 0, 0, 255]);
784 assert_eq!(buf, [0, 0, 0, 255]);
785 }
786
787 #[test]
788 fn rgba_to_cmyk_white() {
789 let buf = crate::color::rgba_to_cmyk_buffer(&[255, 255, 255, 255]);
790 assert_eq!(buf, [0, 0, 0, 0]);
791 }
792
793 #[test]
794 fn rgba_to_cmyk_buffer_stride() {
795 let buf = crate::color::rgba_to_cmyk_buffer(&[255, 0, 0, 255, 0, 0, 0, 255]);
796 assert_eq!(buf.len(), 8);
797 }
798
799 #[test]
800 fn render_config_into_options() {
801 let cfg = RenderConfig {
802 dpi: 150,
803 ..Default::default()
804 };
805 let opts = RenderOptions::from(&cfg);
806 assert!((opts.dpi - 150.0).abs() < f64::EPSILON);
807 }
808
809 #[test]
810 fn overlay_partial_pixel_without_prior_exact_falls_back() {
811 let mut overlay = CmykOverlay::new(1, 1);
812 overlay.apply_mask(&[128], [1, 2, 3, 4]);
813 assert_eq!(overlay.data, vec![None]);
814 }
815
816 #[test]
817 fn overlay_partial_pixel_blends_existing_exact() {
818 let mut overlay = CmykOverlay::new(1, 1);
819 overlay.apply_mask(&[255], [0, 0, 0, 0]);
820 overlay.apply_mask(&[128], [255, 128, 64, 32]);
821 assert_eq!(overlay.data, vec![Some([128, 64, 32, 16])]);
822 }
823}