1use rpdfium_core::Matrix;
9use rpdfium_graphics::{Bitmap, BitmapFormat, BlendMode, ClipPath, FillRule, PathOp};
10
11use crate::color_convert::RgbaColor;
12use crate::image::{DecodedImage, DecodedImageFormat};
13use crate::renderdevicedriver_iface::RenderBackend;
14use crate::stroke::StrokeStyle;
15
16pub struct SkiaSurface {
18 pixmap: tiny_skia::Pixmap,
19}
20
21impl SkiaSurface {
22 pub fn pixel(&self, x: u32, y: u32) -> Option<tiny_skia::PremultipliedColorU8> {
24 self.pixmap.pixel(x, y)
25 }
26}
27
28struct GroupEntry {
30 pixmap: tiny_skia::Pixmap,
31 blend_mode: tiny_skia::BlendMode,
32 opacity: f32,
33 #[allow(dead_code)]
34 isolated: bool,
35 knockout: bool,
36 mask: Option<tiny_skia::Mask>,
38}
39
40pub struct TinySkiaBackend {
42 clip_stack: Vec<tiny_skia::Mask>,
43 group_stack: Vec<GroupEntry>,
44 antialiasing: bool,
45}
46
47impl TinySkiaBackend {
48 pub fn new() -> Self {
50 Self {
51 clip_stack: Vec::new(),
52 group_stack: Vec::new(),
53 antialiasing: true,
54 }
55 }
56
57 pub fn set_antialiasing(&mut self, enabled: bool) {
59 self.antialiasing = enabled;
60 }
61
62 fn is_knockout(&self) -> bool {
64 self.group_stack.last().is_some_and(|e| e.knockout)
65 }
66}
67
68impl Default for TinySkiaBackend {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74fn build_path(ops: &[PathOp]) -> Option<tiny_skia::Path> {
79 let mut pb = tiny_skia::PathBuilder::new();
80 for op in ops {
81 match *op {
82 PathOp::MoveTo { x, y } => pb.move_to(x, y),
83 PathOp::LineTo { x, y } => pb.line_to(x, y),
84 PathOp::CurveTo {
85 x1,
86 y1,
87 x2,
88 y2,
89 x3,
90 y3,
91 } => pb.cubic_to(x1, y1, x2, y2, x3, y3),
92 PathOp::Close => pb.close(),
93 }
94 }
95 pb.finish()
96}
97
98fn to_transform(m: &Matrix) -> tiny_skia::Transform {
99 tiny_skia::Transform::from_row(
100 m.a as f32, m.b as f32, m.c as f32, m.d as f32, m.e as f32, m.f as f32,
101 )
102}
103
104fn to_color(c: &RgbaColor) -> tiny_skia::Color {
105 tiny_skia::Color::from_rgba8(c.r, c.g, c.b, c.a)
106}
107
108fn to_fill_rule(rule: FillRule) -> tiny_skia::FillRule {
109 match rule {
110 FillRule::NonZero => tiny_skia::FillRule::Winding,
111 FillRule::EvenOdd => tiny_skia::FillRule::EvenOdd,
112 }
113}
114
115fn to_line_cap(cap: rpdfium_graphics::LineCapStyle) -> tiny_skia::LineCap {
116 match cap {
117 rpdfium_graphics::LineCapStyle::Butt => tiny_skia::LineCap::Butt,
118 rpdfium_graphics::LineCapStyle::Round => tiny_skia::LineCap::Round,
119 rpdfium_graphics::LineCapStyle::Square => tiny_skia::LineCap::Square,
120 }
121}
122
123fn to_line_join(join: rpdfium_graphics::LineJoinStyle) -> tiny_skia::LineJoin {
124 match join {
125 rpdfium_graphics::LineJoinStyle::Miter => tiny_skia::LineJoin::Miter,
126 rpdfium_graphics::LineJoinStyle::Round => tiny_skia::LineJoin::Round,
127 rpdfium_graphics::LineJoinStyle::Bevel => tiny_skia::LineJoin::Bevel,
128 }
129}
130
131fn to_blend_mode(mode: BlendMode) -> tiny_skia::BlendMode {
132 match mode {
133 BlendMode::Normal => tiny_skia::BlendMode::SourceOver,
134 BlendMode::Multiply => tiny_skia::BlendMode::Multiply,
135 BlendMode::Screen => tiny_skia::BlendMode::Screen,
136 BlendMode::Overlay => tiny_skia::BlendMode::Overlay,
137 BlendMode::Darken => tiny_skia::BlendMode::Darken,
138 BlendMode::Lighten => tiny_skia::BlendMode::Lighten,
139 BlendMode::ColorDodge => tiny_skia::BlendMode::ColorDodge,
140 BlendMode::ColorBurn => tiny_skia::BlendMode::ColorBurn,
141 BlendMode::HardLight => tiny_skia::BlendMode::HardLight,
142 BlendMode::SoftLight => tiny_skia::BlendMode::SoftLight,
143 BlendMode::Difference => tiny_skia::BlendMode::Difference,
144 BlendMode::Exclusion => tiny_skia::BlendMode::Exclusion,
145 BlendMode::Hue => tiny_skia::BlendMode::Hue,
146 BlendMode::Saturation => tiny_skia::BlendMode::Saturation,
147 BlendMode::Color => tiny_skia::BlendMode::Color,
148 BlendMode::Luminosity => tiny_skia::BlendMode::Luminosity,
149 }
150}
151
152fn build_stroke(style: &StrokeStyle) -> tiny_skia::Stroke {
153 let mut stroke = tiny_skia::Stroke {
154 width: style.width,
155 miter_limit: style.miter_limit,
156 line_cap: to_line_cap(style.line_cap),
157 line_join: to_line_join(style.line_join),
158 dash: None,
159 };
160 if let Some(ref dash) = style.dash {
161 let mut arr: Vec<f32> = dash
166 .array
167 .iter()
168 .map(|&v| if v <= 0.000001 { 0.1 } else { v })
169 .collect();
170 if arr.len() % 2 != 0 {
172 arr.extend_from_within(..);
173 }
174 stroke.dash = tiny_skia::StrokeDash::new(arr, dash.phase);
175 }
176 stroke
177}
178
179fn make_paint(color: &RgbaColor, anti_alias: bool) -> tiny_skia::Paint<'static> {
180 let mut paint = tiny_skia::Paint::default();
181 paint.set_color(to_color(color));
182 paint.anti_alias = anti_alias;
183 paint
184}
185
186fn image_to_premul_rgba(image: &DecodedImage) -> Vec<u8> {
188 let pixel_count = (image.width * image.height) as usize;
189 let straight = match image.format {
190 DecodedImageFormat::Rgba32 => image.data.clone(),
191 DecodedImageFormat::Rgb24 => {
192 let mut out = Vec::with_capacity(pixel_count * 4);
193 for i in 0..pixel_count {
194 let base = i * 3;
195 out.push(image.data[base]);
196 out.push(image.data[base + 1]);
197 out.push(image.data[base + 2]);
198 out.push(255);
199 }
200 out
201 }
202 DecodedImageFormat::Gray8 => {
203 let mut out = Vec::with_capacity(pixel_count * 4);
204 for i in 0..pixel_count {
205 let v = image.data[i];
206 out.push(v);
207 out.push(v);
208 out.push(v);
209 out.push(255);
210 }
211 out
212 }
213 };
214 let mut premul = straight;
216 for chunk in premul.chunks_exact_mut(4) {
217 let a = chunk[3] as u16;
218 if a < 255 {
219 chunk[0] = ((chunk[0] as u16 * a + 128) / 255) as u8;
220 chunk[1] = ((chunk[1] as u16 * a + 128) / 255) as u8;
221 chunk[2] = ((chunk[2] as u16 * a + 128) / 255) as u8;
222 }
223 }
224 premul
225}
226
227impl RenderBackend for TinySkiaBackend {
232 type Surface = SkiaSurface;
233
234 fn create_surface(&self, width: u32, height: u32, bg: &RgbaColor) -> Self::Surface {
235 let w = width.clamp(1, 65535);
238 let h = height.clamp(1, 65535);
239 let mut pixmap = tiny_skia::Pixmap::new(w, h).expect("failed to create rendering surface");
240 pixmap.fill(to_color(bg));
241 SkiaSurface { pixmap }
242 }
243
244 fn fill_path(
245 &mut self,
246 surface: &mut Self::Surface,
247 ops: &[PathOp],
248 fill_rule: FillRule,
249 color: &RgbaColor,
250 transform: &Matrix,
251 ) {
252 let Some(path) = build_path(ops) else {
253 return;
254 };
255 let mut paint = make_paint(color, self.antialiasing);
256 if self.is_knockout() {
259 paint.blend_mode = tiny_skia::BlendMode::Source;
260 }
261 let mask = self.clip_stack.last();
262 surface.pixmap.fill_path(
263 &path,
264 &paint,
265 to_fill_rule(fill_rule),
266 to_transform(transform),
267 mask,
268 );
269 }
270
271 fn fill_path_no_aa(
272 &mut self,
273 surface: &mut Self::Surface,
274 ops: &[PathOp],
275 fill_rule: FillRule,
276 color: &RgbaColor,
277 transform: &Matrix,
278 ) {
279 let Some(path) = build_path(ops) else {
280 return;
281 };
282 let mut paint = make_paint(color, false);
283 if self.is_knockout() {
284 paint.blend_mode = tiny_skia::BlendMode::Source;
285 }
286 let mask = self.clip_stack.last();
287 surface.pixmap.fill_path(
288 &path,
289 &paint,
290 to_fill_rule(fill_rule),
291 to_transform(transform),
292 mask,
293 );
294 }
295
296 fn stroke_path(
297 &mut self,
298 surface: &mut Self::Surface,
299 ops: &[PathOp],
300 style: &StrokeStyle,
301 color: &RgbaColor,
302 transform: &Matrix,
303 ) {
304 let Some(path) = build_path(ops) else {
305 return;
306 };
307 let mut paint = make_paint(color, self.antialiasing);
308 if self.is_knockout() {
309 paint.blend_mode = tiny_skia::BlendMode::Source;
310 }
311 let stroke = build_stroke(style);
312 let mask = self.clip_stack.last();
313 surface
314 .pixmap
315 .stroke_path(&path, &paint, &stroke, to_transform(transform), mask);
316 }
317
318 fn draw_image(
319 &mut self,
320 surface: &mut Self::Surface,
321 image: &DecodedImage,
322 transform: &Matrix,
323 interpolate: bool,
324 ) {
325 let premul = image_to_premul_rgba(image);
326 let Some(src) = tiny_skia::PixmapRef::from_bytes(&premul, image.width, image.height) else {
327 return;
328 };
329 let mut paint = tiny_skia::PixmapPaint::default();
330 if self.is_knockout() {
331 paint.blend_mode = tiny_skia::BlendMode::Source;
332 }
333 if interpolate {
334 paint.quality = tiny_skia::FilterQuality::Bilinear;
335 }
336 let mask = self.clip_stack.last();
337 surface
338 .pixmap
339 .draw_pixmap(0, 0, src, &paint, to_transform(transform), mask);
340 }
341
342 fn push_clip(&mut self, surface: &mut Self::Surface, clip: &ClipPath, transform: &Matrix) {
343 let w = surface.pixmap.width();
344 let h = surface.pixmap.height();
345 let Some(mut mask) = tiny_skia::Mask::new(w, h) else {
346 return;
347 };
348 let ts = to_transform(transform);
349 for (i, entry) in clip.paths.iter().enumerate() {
350 let Some(path) = build_path(&entry.ops) else {
351 continue;
352 };
353 let rule = to_fill_rule(entry.fill_rule);
354 if i == 0 {
355 mask.fill_path(&path, rule, true, ts);
356 } else {
357 mask.intersect_path(&path, rule, true, ts);
358 }
359 }
360 self.clip_stack.push(mask);
361 }
362
363 fn pop_clip(&mut self, _surface: &mut Self::Surface) {
364 self.clip_stack.pop();
365 }
366
367 fn push_group(
368 &mut self,
369 surface: &mut Self::Surface,
370 blend_mode: BlendMode,
371 opacity: f32,
372 isolated: bool,
373 knockout: bool,
374 ) {
375 let w = surface.pixmap.width();
376 let h = surface.pixmap.height();
377 let group_pixmap = if isolated {
380 tiny_skia::Pixmap::new(w, h).expect("failed to create group surface")
381 } else {
382 surface.pixmap.clone()
383 };
384 let prev = std::mem::replace(&mut surface.pixmap, group_pixmap);
385 self.group_stack.push(GroupEntry {
386 pixmap: prev,
387 blend_mode: to_blend_mode(blend_mode),
388 opacity,
389 isolated,
390 knockout,
391 mask: None,
392 });
393 }
394
395 fn pop_group(&mut self, surface: &mut Self::Surface) {
396 let Some(entry) = self.group_stack.pop() else {
397 return;
398 };
399 let group = std::mem::replace(&mut surface.pixmap, entry.pixmap);
400 let paint = tiny_skia::PixmapPaint {
401 opacity: entry.opacity,
402 blend_mode: entry.blend_mode,
403 ..tiny_skia::PixmapPaint::default()
404 };
405 let mask = entry.mask.as_ref().or(self.clip_stack.last());
407 surface.pixmap.draw_pixmap(
408 0,
409 0,
410 group.as_ref(),
411 &paint,
412 tiny_skia::Transform::identity(),
413 mask,
414 );
415 }
416
417 fn set_group_mask(&mut self, alpha_data: Vec<u8>, width: u32, height: u32) {
418 let Some(entry) = self.group_stack.last_mut() else {
419 return;
420 };
421 let Some(mut mask) = tiny_skia::Mask::new(width, height) else {
422 return;
423 };
424 let mask_data = mask.data_mut();
426 let len = mask_data.len().min(alpha_data.len());
427 mask_data[..len].copy_from_slice(&alpha_data[..len]);
428 entry.mask = Some(mask);
429 }
430
431 fn draw_alpha_bitmap(
432 &mut self,
433 surface: &mut Self::Surface,
434 alpha: &[u8],
435 width: u32,
436 height: u32,
437 bearing_x: i32,
438 bearing_y: i32,
439 color: &RgbaColor,
440 transform: &Matrix,
441 ) {
442 if width == 0 || height == 0 || alpha.is_empty() {
443 return;
444 }
445
446 let Some(mut glyph_pixmap) = tiny_skia::Pixmap::new(width, height) else {
448 return;
449 };
450
451 let pixels = glyph_pixmap.pixels_mut();
453 for (i, &a) in alpha.iter().enumerate() {
454 if i >= pixels.len() {
455 break;
456 }
457 if a == 0 {
458 continue;
459 }
460 let alpha_f = a as f32 / 255.0;
461 let r = (color.r as f32 * alpha_f) as u8;
462 let g = (color.g as f32 * alpha_f) as u8;
463 let b = (color.b as f32 * alpha_f) as u8;
464 let a_out = (color.a as f32 * alpha_f) as u8;
465 if let Some(c) = tiny_skia::PremultipliedColorU8::from_rgba(r, g, b, a_out) {
466 pixels[i] = c;
467 }
468 }
469
470 let ts = tiny_skia::Transform {
472 sx: transform.a as f32,
473 kx: transform.b as f32,
474 ky: transform.c as f32,
475 sy: transform.d as f32,
476 tx: (transform.e + bearing_x as f64) as f32,
477 ty: (transform.f - bearing_y as f64) as f32,
478 };
479
480 let paint = tiny_skia::PixmapPaint {
481 opacity: 1.0,
482 blend_mode: tiny_skia::BlendMode::SourceOver,
483 quality: tiny_skia::FilterQuality::Bilinear,
484 };
485
486 let mask = self.clip_stack.last();
487 surface
488 .pixmap
489 .draw_pixmap(0, 0, glyph_pixmap.as_ref(), &paint, ts, mask);
490 }
491
492 fn composite_over(&mut self, dst: &mut Self::Surface, src: &Self::Surface) {
493 dst.pixmap.draw_pixmap(
494 0,
495 0,
496 src.pixmap.as_ref(),
497 &tiny_skia::PixmapPaint::default(),
498 tiny_skia::Transform::identity(),
499 None,
500 );
501 }
502
503 fn set_antialiasing(&mut self, enabled: bool) {
504 self.antialiasing = enabled;
505 }
506
507 fn surface_dimensions(&self, surface: &Self::Surface) -> (u32, u32) {
508 (surface.pixmap.width(), surface.pixmap.height())
509 }
510
511 fn surface_pixels(&self, surface: &Self::Surface) -> Vec<u8> {
512 surface.pixmap.data().to_vec()
513 }
514
515 fn finish(self, surface: Self::Surface) -> Bitmap {
516 let w = surface.pixmap.width();
517 let h = surface.pixmap.height();
518 let data = surface.pixmap.take();
519 Bitmap {
520 width: w,
521 height: h,
522 format: BitmapFormat::Rgba32,
523 stride: w * 4,
524 data,
525 }
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 #[test]
534 fn test_build_path_triangle() {
535 let ops = vec![
536 PathOp::MoveTo { x: 0.0, y: 0.0 },
537 PathOp::LineTo { x: 100.0, y: 0.0 },
538 PathOp::LineTo { x: 50.0, y: 100.0 },
539 PathOp::Close,
540 ];
541 assert!(build_path(&ops).is_some());
542 }
543
544 #[test]
545 fn test_build_path_curve() {
546 let ops = vec![
547 PathOp::MoveTo { x: 0.0, y: 0.0 },
548 PathOp::CurveTo {
549 x1: 10.0,
550 y1: 20.0,
551 x2: 30.0,
552 y2: 40.0,
553 x3: 50.0,
554 y3: 0.0,
555 },
556 ];
557 assert!(build_path(&ops).is_some());
558 }
559
560 #[test]
561 fn test_build_path_empty() {
562 assert!(build_path(&[]).is_none());
563 }
564
565 #[test]
566 fn test_transform_identity() {
567 let t = to_transform(&Matrix::identity());
568 assert!(t.is_identity());
569 }
570
571 #[test]
572 fn test_transform_scale() {
573 let m = Matrix::from_scale(2.0, 3.0);
574 let t = to_transform(&m);
575 assert_eq!(t.sx, 2.0);
576 assert_eq!(t.sy, 3.0);
577 }
578
579 #[test]
580 fn test_fill_produces_colored_pixels() {
581 let mut backend = TinySkiaBackend::new();
582 let mut surface = backend.create_surface(100, 100, &RgbaColor::WHITE);
583 let ops = vec![
584 PathOp::MoveTo { x: 10.0, y: 10.0 },
585 PathOp::LineTo { x: 90.0, y: 10.0 },
586 PathOp::LineTo { x: 90.0, y: 90.0 },
587 PathOp::LineTo { x: 10.0, y: 90.0 },
588 PathOp::Close,
589 ];
590 let red = RgbaColor::new(255, 0, 0, 255);
591 backend.fill_path(
592 &mut surface,
593 &ops,
594 FillRule::NonZero,
595 &red,
596 &Matrix::identity(),
597 );
598 let px = surface.pixel(50, 50).unwrap();
600 assert_eq!(px.red(), 255);
601 assert_eq!(px.green(), 0);
602 assert_eq!(px.blue(), 0);
603 }
604
605 #[test]
606 fn test_stroke_produces_pixels() {
607 let mut backend = TinySkiaBackend::new();
608 let mut surface = backend.create_surface(100, 100, &RgbaColor::WHITE);
609 let ops = vec![
610 PathOp::MoveTo { x: 10.0, y: 50.0 },
611 PathOp::LineTo { x: 90.0, y: 50.0 },
612 ];
613 let black = RgbaColor::BLACK;
614 let style = StrokeStyle {
615 width: 2.0,
616 line_cap: rpdfium_graphics::LineCapStyle::Butt,
617 line_join: rpdfium_graphics::LineJoinStyle::Miter,
618 miter_limit: 10.0,
619 dash: None,
620 };
621 backend.stroke_path(&mut surface, &ops, &style, &black, &Matrix::identity());
622 let px = surface.pixel(50, 50).unwrap();
624 assert!(px.red() < 255 || px.green() < 255 || px.blue() < 255);
625 }
626
627 #[test]
628 fn test_finish_returns_bitmap() {
629 let backend = TinySkiaBackend::new();
630 let surface = backend.create_surface(50, 30, &RgbaColor::WHITE);
631 let bitmap = backend.finish(surface);
632 assert_eq!(bitmap.width, 50);
633 assert_eq!(bitmap.height, 30);
634 assert_eq!(bitmap.format, BitmapFormat::Rgba32);
635 assert_eq!(bitmap.stride, 200);
636 assert_eq!(bitmap.data.len(), 200 * 30);
637 }
638
639 #[test]
640 fn test_blend_mode_mapping() {
641 assert_eq!(
642 to_blend_mode(BlendMode::Normal),
643 tiny_skia::BlendMode::SourceOver
644 );
645 assert_eq!(
646 to_blend_mode(BlendMode::Multiply),
647 tiny_skia::BlendMode::Multiply
648 );
649 assert_eq!(to_blend_mode(BlendMode::Hue), tiny_skia::BlendMode::Hue);
650 assert_eq!(
651 to_blend_mode(BlendMode::Luminosity),
652 tiny_skia::BlendMode::Luminosity
653 );
654 }
655
656 #[test]
657 fn test_group_compositing() {
658 let mut backend = TinySkiaBackend::new();
659 let mut surface = backend.create_surface(50, 50, &RgbaColor::WHITE);
660
661 backend.push_group(&mut surface, BlendMode::Normal, 0.5, true, false);
662 let ops = vec![
663 PathOp::MoveTo { x: 0.0, y: 0.0 },
664 PathOp::LineTo { x: 50.0, y: 0.0 },
665 PathOp::LineTo { x: 50.0, y: 50.0 },
666 PathOp::LineTo { x: 0.0, y: 50.0 },
667 PathOp::Close,
668 ];
669 let red = RgbaColor::new(255, 0, 0, 255);
670 backend.fill_path(
671 &mut surface,
672 &ops,
673 FillRule::NonZero,
674 &red,
675 &Matrix::identity(),
676 );
677 backend.pop_group(&mut surface);
678
679 let px = surface.pixel(25, 25).unwrap();
681 assert!(px.red() > 100);
683 assert!(px.green() > 50);
684 }
685
686 #[test]
687 fn test_default_backend() {
688 let _backend = TinySkiaBackend::default();
689 }
690
691 #[test]
692 fn test_image_to_premul_gray() {
693 let img = DecodedImage {
694 width: 2,
695 height: 1,
696 data: vec![128, 255],
697 format: DecodedImageFormat::Gray8,
698 };
699 let premul = image_to_premul_rgba(&img);
700 assert_eq!(premul.len(), 8);
701 assert_eq!(&premul[0..4], &[128, 128, 128, 255]);
703 assert_eq!(&premul[4..8], &[255, 255, 255, 255]);
705 }
706
707 #[test]
708 fn test_image_to_premul_rgb() {
709 let img = DecodedImage {
710 width: 1,
711 height: 1,
712 data: vec![255, 0, 128],
713 format: DecodedImageFormat::Rgb24,
714 };
715 let premul = image_to_premul_rgba(&img);
716 assert_eq!(premul.len(), 4);
717 assert_eq!(&premul[..], &[255, 0, 128, 255]);
719 }
720
721 #[test]
722 fn test_image_to_premul_rgba_with_alpha() {
723 let img = DecodedImage {
724 width: 1,
725 height: 1,
726 data: vec![200, 100, 50, 128],
727 format: DecodedImageFormat::Rgba32,
728 };
729 let premul = image_to_premul_rgba(&img);
730 assert_eq!(premul.len(), 4);
731 assert!(premul[0] > 90 && premul[0] < 110);
733 assert_eq!(premul[3], 128);
734 }
735
736 #[test]
737 fn test_clip_path_fill() {
738 let mut backend = TinySkiaBackend::new();
739 let mut surface = backend.create_surface(100, 100, &RgbaColor::WHITE);
740
741 let mut clip = ClipPath::new();
743 clip.push(
744 vec![
745 PathOp::MoveTo { x: 40.0, y: 40.0 },
746 PathOp::LineTo { x: 60.0, y: 40.0 },
747 PathOp::LineTo { x: 60.0, y: 60.0 },
748 PathOp::LineTo { x: 40.0, y: 60.0 },
749 PathOp::Close,
750 ],
751 FillRule::NonZero,
752 );
753 backend.push_clip(&mut surface, &clip, &Matrix::identity());
754
755 let ops = vec![
757 PathOp::MoveTo { x: 0.0, y: 0.0 },
758 PathOp::LineTo { x: 100.0, y: 0.0 },
759 PathOp::LineTo { x: 100.0, y: 100.0 },
760 PathOp::LineTo { x: 0.0, y: 100.0 },
761 PathOp::Close,
762 ];
763 let red = RgbaColor::new(255, 0, 0, 255);
764 backend.fill_path(
765 &mut surface,
766 &ops,
767 FillRule::NonZero,
768 &red,
769 &Matrix::identity(),
770 );
771
772 let px = surface.pixel(50, 50).unwrap();
774 assert_eq!(px.red(), 255);
775 assert_eq!(px.green(), 0);
776
777 let corner = surface.pixel(5, 5).unwrap();
779 assert_eq!(corner.red(), 255);
780 assert_eq!(corner.green(), 255);
781
782 backend.pop_clip(&mut surface);
783 }
784
785 #[test]
786 fn test_stroke_with_dash() {
787 let mut backend = TinySkiaBackend::new();
788 let mut surface = backend.create_surface(100, 100, &RgbaColor::WHITE);
789 let ops = vec![
790 PathOp::MoveTo { x: 10.0, y: 50.0 },
791 PathOp::LineTo { x: 90.0, y: 50.0 },
792 ];
793 let style = StrokeStyle {
794 width: 4.0,
795 line_cap: rpdfium_graphics::LineCapStyle::Butt,
796 line_join: rpdfium_graphics::LineJoinStyle::Miter,
797 miter_limit: 10.0,
798 dash: Some(rpdfium_graphics::DashPattern {
799 array: vec![10.0, 5.0, 10.0, 5.0],
800 phase: 0.0,
801 }),
802 };
803 let black = RgbaColor::BLACK;
804 backend.stroke_path(&mut surface, &ops, &style, &black, &Matrix::identity());
805 let bitmap = backend.finish(surface);
807 let has_dark = bitmap.data.chunks_exact(4).any(|px| px[0] < 200);
809 assert!(has_dark);
810 }
811
812 #[test]
813 fn test_group_with_alpha_mask() {
814 let mut backend = TinySkiaBackend::new();
815 let mut surface = backend.create_surface(50, 50, &RgbaColor::WHITE);
816
817 backend.push_group(&mut surface, BlendMode::Normal, 1.0, true, false);
818
819 let mut alpha_data = vec![0u8; 50 * 50];
821 for y in 0..25u32 {
822 for x in 0..50u32 {
823 alpha_data[(y * 50 + x) as usize] = 255;
824 }
825 }
826 backend.set_group_mask(alpha_data, 50, 50);
828
829 let ops = vec![
831 PathOp::MoveTo { x: 0.0, y: 0.0 },
832 PathOp::LineTo { x: 50.0, y: 0.0 },
833 PathOp::LineTo { x: 50.0, y: 50.0 },
834 PathOp::LineTo { x: 0.0, y: 50.0 },
835 PathOp::Close,
836 ];
837 let red = RgbaColor::new(255, 0, 0, 255);
838 backend.fill_path(
839 &mut surface,
840 &ops,
841 FillRule::NonZero,
842 &red,
843 &Matrix::identity(),
844 );
845 backend.pop_group(&mut surface);
846
847 let top = surface.pixel(25, 12).unwrap();
849 assert_eq!(top.red(), 255);
850 assert_eq!(top.green(), 0);
851
852 let bottom = surface.pixel(25, 37).unwrap();
854 assert_eq!(bottom.red(), 255);
855 assert_eq!(bottom.green(), 255);
856 assert_eq!(bottom.blue(), 255);
857 }
858
859 #[test]
860 fn test_group_without_mask_unchanged() {
861 let mut backend = TinySkiaBackend::new();
863 let mut surface = backend.create_surface(50, 50, &RgbaColor::WHITE);
864
865 backend.push_group(&mut surface, BlendMode::Normal, 1.0, true, false);
866 let ops = vec![
867 PathOp::MoveTo { x: 0.0, y: 0.0 },
868 PathOp::LineTo { x: 50.0, y: 0.0 },
869 PathOp::LineTo { x: 50.0, y: 50.0 },
870 PathOp::LineTo { x: 0.0, y: 50.0 },
871 PathOp::Close,
872 ];
873 let red = RgbaColor::new(255, 0, 0, 255);
874 backend.fill_path(
875 &mut surface,
876 &ops,
877 FillRule::NonZero,
878 &red,
879 &Matrix::identity(),
880 );
881 backend.pop_group(&mut surface);
882
883 let px = surface.pixel(25, 25).unwrap();
884 assert_eq!(px.red(), 255);
885 assert_eq!(px.green(), 0);
886 assert_eq!(px.blue(), 0);
887 }
888
889 #[test]
890 fn test_surface_dimensions_and_pixels() {
891 let backend = TinySkiaBackend::new();
892 let surface = backend.create_surface(40, 30, &RgbaColor::WHITE);
893 let (w, h) = backend.surface_dimensions(&surface);
894 assert_eq!((w, h), (40, 30));
895 let pixels = backend.surface_pixels(&surface);
896 assert_eq!(pixels.len(), (40 * 30 * 4) as usize);
897 }
898
899 #[test]
900 fn test_nested_group_opacity_on_transparent() {
901 let mut backend = TinySkiaBackend::new();
903 let transparent = RgbaColor::new(0, 0, 0, 0);
904 let mut surface = backend.create_surface(50, 50, &transparent);
905
906 backend.push_group(&mut surface, BlendMode::Normal, 1.0, true, false);
908
909 let mask_data = vec![255u8; 50 * 50];
911 backend.set_group_mask(mask_data, 50, 50);
912
913 backend.push_group(&mut surface, BlendMode::Screen, 0.6, true, false);
915 let ops = vec![
916 PathOp::MoveTo { x: 0.0, y: 0.0 },
917 PathOp::LineTo { x: 50.0, y: 0.0 },
918 PathOp::LineTo { x: 50.0, y: 50.0 },
919 PathOp::LineTo { x: 0.0, y: 50.0 },
920 PathOp::Close,
921 ];
922 let white = RgbaColor::new(255, 255, 255, 255);
923 backend.fill_path(
924 &mut surface,
925 &ops,
926 FillRule::NonZero,
927 &white,
928 &Matrix::identity(),
929 );
930 backend.pop_group(&mut surface); backend.pop_group(&mut surface); let px = surface.pixel(25, 25).unwrap();
935 eprintln!(
936 "Nested group: R={}, G={}, B={}, A={}",
937 px.red(),
938 px.green(),
939 px.blue(),
940 px.alpha()
941 );
942 assert!(
944 px.alpha() > 140 && px.alpha() < 166,
945 "Expected alpha ~153 but got {}",
946 px.alpha()
947 );
948 }
949
950 fn rect_path(x: f32, y: f32, w: f32, h: f32) -> Vec<PathOp> {
956 vec![
957 PathOp::MoveTo { x, y },
958 PathOp::LineTo { x: x + w, y },
959 PathOp::LineTo { x: x + w, y: y + h },
960 PathOp::LineTo { x, y: y + h },
961 PathOp::Close,
962 ]
963 }
964
965 fn blend_test(
969 bg_r: u8,
970 bg_g: u8,
971 bg_b: u8,
972 fg_r: u8,
973 fg_g: u8,
974 fg_b: u8,
975 mode: BlendMode,
976 ) -> (u8, u8, u8) {
977 let mut backend = TinySkiaBackend::new();
978 let white = RgbaColor {
980 r: 255,
981 g: 255,
982 b: 255,
983 a: 255,
984 };
985 let mut surface = backend.create_surface(4, 4, &white);
986
987 let bg_color = RgbaColor {
989 r: bg_r,
990 g: bg_g,
991 b: bg_b,
992 a: 255,
993 };
994 let full_rect = rect_path(0.0, 0.0, 4.0, 4.0);
995 backend.fill_path(
996 &mut surface,
997 &full_rect,
998 FillRule::NonZero,
999 &bg_color,
1000 &Matrix::identity(),
1001 );
1002
1003 backend.push_group(&mut surface, mode, 1.0, true, false);
1005 let fg_color = RgbaColor {
1006 r: fg_r,
1007 g: fg_g,
1008 b: fg_b,
1009 a: 255,
1010 };
1011 backend.fill_path(
1012 &mut surface,
1013 &full_rect,
1014 FillRule::NonZero,
1015 &fg_color,
1016 &Matrix::identity(),
1017 );
1018 backend.pop_group(&mut surface);
1019
1020 let pixels = backend.surface_pixels(&surface);
1022 (pixels[0], pixels[1], pixels[2])
1024 }
1025
1026 fn assert_pixel_near(actual: (u8, u8, u8), expected: (u8, u8, u8), tolerance: u8, msg: &str) {
1028 let dr = (actual.0 as i16 - expected.0 as i16).unsigned_abs() as u8;
1029 let dg = (actual.1 as i16 - expected.1 as i16).unsigned_abs() as u8;
1030 let db = (actual.2 as i16 - expected.2 as i16).unsigned_abs() as u8;
1031 assert!(
1032 dr <= tolerance && dg <= tolerance && db <= tolerance,
1033 "{msg}: expected ({}, {}, {}), got ({}, {}, {}), diff ({dr}, {dg}, {db})",
1034 expected.0,
1035 expected.1,
1036 expected.2,
1037 actual.0,
1038 actual.1,
1039 actual.2
1040 );
1041 }
1042
1043 #[test]
1044 fn test_blend_mode_normal_pixel() {
1045 let result = blend_test(204, 153, 102, 153, 102, 51, BlendMode::Normal);
1046 assert_pixel_near(result, (153, 102, 51), 2, "Normal");
1047 }
1048
1049 #[test]
1050 fn test_blend_mode_multiply_pixel() {
1051 let result = blend_test(204, 153, 102, 153, 102, 51, BlendMode::Multiply);
1052 assert_pixel_near(result, (122, 61, 20), 2, "Multiply");
1053 }
1054
1055 #[test]
1056 fn test_blend_mode_screen_pixel() {
1057 let result = blend_test(204, 153, 102, 153, 102, 51, BlendMode::Screen);
1058 assert_pixel_near(result, (235, 194, 133), 2, "Screen");
1059 }
1060
1061 #[test]
1062 fn test_blend_mode_overlay_pixel() {
1063 let result = blend_test(204, 153, 102, 153, 102, 51, BlendMode::Overlay);
1064 assert_pixel_near(result, (214, 133, 41), 3, "Overlay");
1065 }
1066
1067 #[test]
1068 fn test_blend_mode_darken_pixel() {
1069 let result = blend_test(204, 153, 102, 153, 102, 51, BlendMode::Darken);
1070 assert_pixel_near(result, (153, 102, 51), 2, "Darken");
1071 }
1072
1073 #[test]
1074 fn test_blend_mode_lighten_pixel() {
1075 let result = blend_test(204, 153, 102, 153, 102, 51, BlendMode::Lighten);
1076 assert_pixel_near(result, (204, 153, 102), 2, "Lighten");
1077 }
1078
1079 #[test]
1080 fn test_blend_mode_color_dodge_pixel() {
1081 let result = blend_test(204, 153, 102, 153, 102, 51, BlendMode::ColorDodge);
1082 assert_pixel_near(result, (255, 255, 128), 3, "ColorDodge");
1083 }
1084
1085 #[test]
1086 fn test_blend_mode_color_burn_pixel() {
1087 let result = blend_test(204, 153, 102, 153, 102, 51, BlendMode::ColorBurn);
1088 assert_pixel_near(result, (170, 0, 0), 3, "ColorBurn");
1089 }
1090
1091 #[test]
1092 fn test_blend_mode_hard_light_pixel() {
1093 let result = blend_test(204, 153, 102, 153, 102, 51, BlendMode::HardLight);
1094 assert_pixel_near(result, (214, 122, 41), 3, "HardLight");
1095 }
1096
1097 #[test]
1098 fn test_blend_mode_difference_pixel() {
1099 let result = blend_test(204, 153, 102, 153, 102, 51, BlendMode::Difference);
1100 assert_pixel_near(result, (51, 51, 51), 2, "Difference");
1101 }
1102
1103 #[test]
1104 fn test_blend_mode_exclusion_pixel() {
1105 let result = blend_test(204, 153, 102, 153, 102, 51, BlendMode::Exclusion);
1106 assert_pixel_near(result, (112, 133, 112), 3, "Exclusion");
1107 }
1108
1109 #[test]
1111 fn test_blend_mode_soft_light_pixel() {
1112 let result = blend_test(204, 153, 102, 153, 102, 51, BlendMode::SoftLight);
1113 assert!(
1115 result.0 >= 140 && result.0 <= 220,
1116 "SoftLight R out of range: {}",
1117 result.0
1118 );
1119 assert!(
1120 result.1 >= 100 && result.1 <= 170,
1121 "SoftLight G out of range: {}",
1122 result.1
1123 );
1124 assert!(
1125 result.2 >= 30 && result.2 <= 120,
1126 "SoftLight B out of range: {}",
1127 result.2
1128 );
1129 }
1130
1131 #[test]
1135 fn test_blend_mode_hue_pixel() {
1136 let result = blend_test(204, 153, 102, 153, 102, 51, BlendMode::Hue);
1139 assert!(
1140 result.0 > 0 || result.1 > 0 || result.2 > 0,
1141 "Hue produced all-black pixels: {:?}",
1142 result
1143 );
1144 assert_pixel_near(result, (204, 153, 102), 5, "Hue (same-hue inputs)");
1148 }
1149
1150 #[test]
1151 fn test_blend_mode_hue_pixel_distinct_hue() {
1152 let result = blend_test(255, 0, 0, 0, 0, 255, BlendMode::Hue);
1155 assert!(
1158 result.2 > result.0,
1159 "Hue should adopt source blue hue: got {:?}",
1160 result
1161 );
1162 }
1163
1164 #[test]
1165 fn test_blend_mode_saturation_pixel() {
1166 let result = blend_test(204, 153, 102, 153, 102, 51, BlendMode::Saturation);
1168 assert!(
1169 result.0 > 0 || result.1 > 0 || result.2 > 0,
1170 "Saturation produced all-black pixels: {:?}",
1171 result
1172 );
1173 assert_pixel_near(result, (204, 153, 102), 5, "Saturation (equal-sat inputs)");
1176 }
1177
1178 #[test]
1179 fn test_blend_mode_saturation_pixel_desaturate() {
1180 let result = blend_test(255, 0, 0, 128, 128, 128, BlendMode::Saturation);
1182 let spread = result.0.max(result.1).max(result.2) - result.0.min(result.1).min(result.2);
1185 assert!(
1186 spread <= 3,
1187 "Saturation of gray source should produce near-gray: {:?} (spread {spread})",
1188 result
1189 );
1190 }
1191
1192 #[test]
1193 fn test_blend_mode_color_mode_pixel() {
1194 let result = blend_test(204, 153, 102, 153, 102, 51, BlendMode::Color);
1195 assert!(
1196 result.0 > 0 || result.1 > 0 || result.2 > 0,
1197 "Color produced all-black pixels: {:?}",
1198 result
1199 );
1200 assert_pixel_near(result, (204, 153, 102), 5, "Color (same-hue-sat inputs)");
1203 }
1204
1205 #[test]
1206 fn test_blend_mode_luminosity_pixel() {
1207 let result = blend_test(204, 153, 102, 153, 102, 51, BlendMode::Luminosity);
1209 assert!(
1210 result.0 > 0 || result.1 > 0 || result.2 > 0,
1211 "Luminosity produced all-black pixels: {:?}",
1212 result
1213 );
1214 let result_lum =
1219 0.299 * result.0 as f64 + 0.587 * result.1 as f64 + 0.114 * result.2 as f64;
1220 let backdrop_lum = 0.299 * 204.0 + 0.587 * 153.0 + 0.114 * 102.0;
1221 assert!(
1222 result_lum < backdrop_lum - 10.0,
1223 "Luminosity result should be darker: result_lum={result_lum:.1}, backdrop_lum={backdrop_lum:.1}"
1224 );
1225 }
1226
1227 #[test]
1228 fn test_blend_mode_luminosity_pixel_preserves_hue() {
1229 let result = blend_test(200, 50, 50, 200, 200, 200, BlendMode::Luminosity);
1232 assert!(
1234 result.0 > result.1 && result.0 > result.2,
1235 "Luminosity should preserve red hue: got {:?}",
1236 result
1237 );
1238 }
1239
1240 #[test]
1241 fn test_group_opacity_on_transparent() {
1242 let mut backend = TinySkiaBackend::new();
1244 let transparent = RgbaColor::new(0, 0, 0, 0);
1245 let mut surface = backend.create_surface(50, 50, &transparent);
1246
1247 backend.push_group(&mut surface, BlendMode::Screen, 0.6, true, false);
1249 let ops = vec![
1250 PathOp::MoveTo { x: 0.0, y: 0.0 },
1251 PathOp::LineTo { x: 50.0, y: 0.0 },
1252 PathOp::LineTo { x: 50.0, y: 50.0 },
1253 PathOp::LineTo { x: 0.0, y: 50.0 },
1254 PathOp::Close,
1255 ];
1256 let white = RgbaColor::new(255, 255, 255, 255);
1257 backend.fill_path(
1258 &mut surface,
1259 &ops,
1260 FillRule::NonZero,
1261 &white,
1262 &Matrix::identity(),
1263 );
1264 backend.pop_group(&mut surface);
1265
1266 let px = surface.pixel(25, 25).unwrap();
1269 eprintln!(
1270 "Group 0.6 opacity on transparent: R={}, G={}, B={}, A={}",
1271 px.red(),
1272 px.green(),
1273 px.blue(),
1274 px.alpha()
1275 );
1276 assert!(
1278 px.alpha() > 140 && px.alpha() < 166,
1279 "Expected alpha ~153 but got {}",
1280 px.alpha()
1281 );
1282 }
1283
1284 #[test]
1289 fn test_draw_alpha_bitmap_produces_colored_pixel() {
1290 let mut backend = TinySkiaBackend::new();
1291 let mut surface = backend.create_surface(10, 10, &RgbaColor::WHITE);
1292 let alpha = vec![255, 255, 255, 255];
1294 let red = RgbaColor::new(255, 0, 0, 255);
1295 backend.draw_alpha_bitmap(
1296 &mut surface,
1297 &alpha,
1298 2,
1299 2,
1300 0,
1301 2, &red,
1303 &Matrix::identity(),
1304 );
1305 let mut surface2 = backend.create_surface(10, 10, &RgbaColor::WHITE);
1308 backend.draw_alpha_bitmap(&mut surface2, &alpha, 2, 2, 0, 0, &red, &Matrix::identity());
1309 let px2 = surface2.pixel(0, 0).unwrap();
1310 assert_eq!(px2.red(), 255);
1311 assert_eq!(px2.green(), 0);
1312 assert_eq!(px2.blue(), 0);
1313 }
1314
1315 #[test]
1316 fn test_draw_alpha_bitmap_zero_alpha_leaves_bg() {
1317 let mut backend = TinySkiaBackend::new();
1318 let mut surface = backend.create_surface(10, 10, &RgbaColor::WHITE);
1319 let alpha = vec![0, 0, 0, 0];
1321 let red = RgbaColor::new(255, 0, 0, 255);
1322 backend.draw_alpha_bitmap(&mut surface, &alpha, 2, 2, 0, 0, &red, &Matrix::identity());
1323 let px = surface.pixel(0, 0).unwrap();
1325 assert_eq!(px.red(), 255);
1326 assert_eq!(px.green(), 255);
1327 assert_eq!(px.blue(), 255);
1328 }
1329
1330 #[test]
1331 fn test_draw_alpha_bitmap_empty_is_noop() {
1332 let mut backend = TinySkiaBackend::new();
1333 let mut surface = backend.create_surface(10, 10, &RgbaColor::WHITE);
1334 backend.draw_alpha_bitmap(
1336 &mut surface,
1337 &[],
1338 0,
1339 0,
1340 0,
1341 0,
1342 &RgbaColor::BLACK,
1343 &Matrix::identity(),
1344 );
1345 let px = surface.pixel(5, 5).unwrap();
1347 assert_eq!(px.red(), 255);
1348 }
1349
1350 #[test]
1351 fn test_draw_alpha_bitmap_half_alpha() {
1352 let mut backend = TinySkiaBackend::new();
1353 let mut surface = backend.create_surface(10, 10, &RgbaColor::WHITE);
1354 let alpha = vec![128];
1356 let red = RgbaColor::new(255, 0, 0, 255);
1357 backend.draw_alpha_bitmap(&mut surface, &alpha, 1, 1, 0, 0, &red, &Matrix::identity());
1358 let px = surface.pixel(0, 0).unwrap();
1360 assert_eq!(px.red(), 255);
1362 assert!(px.green() > 100 && px.green() < 200, "green={}", px.green());
1364 }
1365
1366 #[test]
1377 fn test_clip_box_default_full_surface() {
1378 let mut backend = TinySkiaBackend::new();
1379 let mut surface = backend.create_surface(16, 16, &RgbaColor::WHITE);
1380
1381 let ops = vec![
1383 PathOp::MoveTo { x: 0.0, y: 0.0 },
1384 PathOp::LineTo { x: 16.0, y: 0.0 },
1385 PathOp::LineTo { x: 16.0, y: 16.0 },
1386 PathOp::LineTo { x: 0.0, y: 16.0 },
1387 PathOp::Close,
1388 ];
1389 let red = RgbaColor::new(255, 0, 0, 255);
1390 backend.fill_path(
1391 &mut surface,
1392 &ops,
1393 FillRule::NonZero,
1394 &red,
1395 &Matrix::identity(),
1396 );
1397
1398 for (x, y) in [(0, 0), (15, 0), (0, 15), (15, 15)] {
1400 let px = surface.pixel(x, y).unwrap();
1401 assert_eq!(px.red(), 255, "pixel ({x},{y}) should be red");
1402 assert_eq!(px.green(), 0, "pixel ({x},{y}) should have no green");
1403 }
1404 }
1405
1406 #[test]
1410 fn test_clip_box_path_fill_restricts_painting() {
1411 let mut backend = TinySkiaBackend::new();
1412 let mut surface = backend.create_surface(16, 16, &RgbaColor::WHITE);
1413
1414 let mut clip = ClipPath::new();
1416 clip.push(
1417 vec![
1418 PathOp::MoveTo { x: 4.0, y: 4.0 },
1419 PathOp::LineTo { x: 12.0, y: 4.0 },
1420 PathOp::LineTo { x: 12.0, y: 12.0 },
1421 PathOp::LineTo { x: 4.0, y: 12.0 },
1422 PathOp::Close,
1423 ],
1424 FillRule::EvenOdd,
1425 );
1426 backend.push_clip(&mut surface, &clip, &Matrix::identity());
1427
1428 let full_rect = vec![
1430 PathOp::MoveTo { x: 0.0, y: 0.0 },
1431 PathOp::LineTo { x: 16.0, y: 0.0 },
1432 PathOp::LineTo { x: 16.0, y: 16.0 },
1433 PathOp::LineTo { x: 0.0, y: 16.0 },
1434 PathOp::Close,
1435 ];
1436 let red = RgbaColor::new(255, 0, 0, 255);
1437 backend.fill_path(
1438 &mut surface,
1439 &full_rect,
1440 FillRule::NonZero,
1441 &red,
1442 &Matrix::identity(),
1443 );
1444
1445 let center = surface.pixel(8, 8).unwrap();
1447 assert_eq!(center.red(), 255);
1448 assert_eq!(center.green(), 0);
1449
1450 let corner = surface.pixel(1, 1).unwrap();
1452 assert_eq!(corner.red(), 255);
1453 assert_eq!(corner.green(), 255);
1454
1455 backend.pop_clip(&mut surface);
1456 }
1457
1458 #[test]
1463 fn test_clip_box_rect_restricts_painting() {
1464 let mut backend = TinySkiaBackend::new();
1465 let mut surface = backend.create_surface(16, 16, &RgbaColor::WHITE);
1466
1467 let mut clip = ClipPath::new();
1469 clip.push(
1470 vec![
1471 PathOp::MoveTo { x: 2.0, y: 4.0 },
1472 PathOp::LineTo { x: 14.0, y: 4.0 },
1473 PathOp::LineTo { x: 14.0, y: 12.0 },
1474 PathOp::LineTo { x: 2.0, y: 12.0 },
1475 PathOp::Close,
1476 ],
1477 FillRule::NonZero,
1478 );
1479 backend.push_clip(&mut surface, &clip, &Matrix::identity());
1480
1481 let full_rect = vec![
1482 PathOp::MoveTo { x: 0.0, y: 0.0 },
1483 PathOp::LineTo { x: 16.0, y: 0.0 },
1484 PathOp::LineTo { x: 16.0, y: 16.0 },
1485 PathOp::LineTo { x: 0.0, y: 16.0 },
1486 PathOp::Close,
1487 ];
1488 let blue = RgbaColor::new(0, 0, 255, 255);
1489 backend.fill_path(
1490 &mut surface,
1491 &full_rect,
1492 FillRule::NonZero,
1493 &blue,
1494 &Matrix::identity(),
1495 );
1496
1497 let inside = surface.pixel(8, 8).unwrap();
1499 assert_eq!(inside.blue(), 255);
1500 assert_eq!(inside.red(), 0);
1501
1502 let outside = surface.pixel(0, 0).unwrap();
1504 assert_eq!(outside.red(), 255);
1505 assert_eq!(outside.green(), 255);
1506
1507 backend.pop_clip(&mut surface);
1508 }
1509
1510 #[test]
1514 fn test_clip_box_empty_prevents_painting() {
1515 let mut backend = TinySkiaBackend::new();
1516 let mut surface = backend.create_surface(16, 16, &RgbaColor::WHITE);
1517
1518 let mut clip = ClipPath::new();
1520 clip.push(
1521 vec![
1522 PathOp::MoveTo { x: 2.0, y: 8.0 },
1523 PathOp::LineTo { x: 14.0, y: 8.0 },
1524 PathOp::LineTo { x: 14.0, y: 8.0 },
1525 PathOp::LineTo { x: 2.0, y: 8.0 },
1526 PathOp::Close,
1527 ],
1528 FillRule::NonZero,
1529 );
1530 backend.push_clip(&mut surface, &clip, &Matrix::identity());
1531
1532 let full_rect = vec![
1533 PathOp::MoveTo { x: 0.0, y: 0.0 },
1534 PathOp::LineTo { x: 16.0, y: 0.0 },
1535 PathOp::LineTo { x: 16.0, y: 16.0 },
1536 PathOp::LineTo { x: 0.0, y: 16.0 },
1537 PathOp::Close,
1538 ];
1539 let red = RgbaColor::new(255, 0, 0, 255);
1540 backend.fill_path(
1541 &mut surface,
1542 &full_rect,
1543 FillRule::NonZero,
1544 &red,
1545 &Matrix::identity(),
1546 );
1547
1548 let px = surface.pixel(8, 8).unwrap();
1550 assert_eq!(px.red(), 255);
1551 assert_eq!(px.green(), 255);
1552 assert_eq!(px.blue(), 255);
1553
1554 backend.pop_clip(&mut surface);
1555 }
1556
1557 #[test]
1562 #[ignore = "rpdfium does not expose stroke-based clip path API"]
1563 fn test_clip_box_path_stroke() {
1564 }
1566}