1use rpdfium_core::Matrix;
9use rpdfium_graphics::Bitmap;
10use rpdfium_page::display::{DisplayTree, walk};
11
12use crate::cfx_defaultrenderdevice::TinySkiaBackend;
13use crate::error::RenderError;
14use crate::image::ImageDecoder;
15use crate::page_transform::compute_page_transform;
16use crate::render_defines::RenderConfig;
17use crate::renderdevicedriver_iface::RenderBackend;
18use crate::renderer::DisplayRenderer;
19
20pub fn render(tree: &DisplayTree, config: &RenderConfig) -> Result<Bitmap, RenderError> {
22 render_internal(tree, config, None)
23}
24
25pub fn render_with_images(
27 tree: &DisplayTree,
28 config: &RenderConfig,
29 decoder: &dyn ImageDecoder,
30) -> Result<Bitmap, RenderError> {
31 render_internal(tree, config, Some(decoder))
32}
33
34fn render_internal(
35 tree: &DisplayTree,
36 config: &RenderConfig,
37 decoder: Option<&dyn ImageDecoder>,
38) -> Result<Bitmap, RenderError> {
39 let mut backend = TinySkiaBackend::new();
40 if !config.antialiasing {
41 backend.set_antialiasing(false);
42 }
43
44 let effective_bg = config
46 .forced_color_scheme
47 .as_ref()
48 .map_or(&config.background, |s| &s.background_color);
49
50 let mut surface = backend.create_surface(config.width, config.height, effective_bg);
51
52 let page_transform = if let Some(m) = config.custom_transform {
53 m
54 } else {
55 match config.media_box {
56 Some(ref mb) => {
57 compute_page_transform(mb, config.width, config.height, config.rotation)
58 }
59 None => Matrix::identity(),
60 }
61 };
62
63 {
64 let mut renderer =
65 DisplayRenderer::new(&mut backend, &mut surface, page_transform, decoder)
66 .with_per_feature_aa(
67 config.text_antialiasing,
68 config.path_antialiasing,
69 config.image_antialiasing,
70 );
71 if let Some(ref scheme) = config.forced_color_scheme {
72 renderer = renderer.with_forced_color_scheme(scheme.clone());
73 }
74 walk(tree, &mut renderer);
75 }
76
77 let mut bitmap = backend.finish(surface);
78
79 if let Some(ref clip) = config.clip_rect {
80 apply_clip_rect(&mut bitmap, clip, effective_bg);
81 }
82
83 if config.grayscale {
84 apply_grayscale(&mut bitmap);
85 }
86
87 Ok(bitmap)
88}
89
90fn apply_clip_rect(
96 bitmap: &mut Bitmap,
97 clip: &rpdfium_core::Rect,
98 bg: &crate::color_convert::RgbaColor,
99) {
100 let bw = bitmap.width as usize;
101 let bh = bitmap.height as usize;
102 let x0 = (clip.left.max(0.0) as usize).min(bw);
103 let x1 = (clip.right.max(0.0) as usize).min(bw);
104 let y0 = (clip.bottom.max(0.0) as usize).min(bh);
105 let y1 = (clip.top.max(0.0) as usize).min(bh);
106 let stride = bw * 4;
107 for y in 0..bh {
108 let in_y = y >= y0 && y < y1;
109 for x in 0..bw {
110 if !in_y || x < x0 || x >= x1 {
111 let off = y * stride + x * 4;
112 bitmap.data[off] = bg.r;
113 bitmap.data[off + 1] = bg.g;
114 bitmap.data[off + 2] = bg.b;
115 bitmap.data[off + 3] = bg.a;
116 }
117 }
118 }
119}
120
121fn apply_grayscale(bitmap: &mut Bitmap) {
124 for chunk in bitmap.data.chunks_exact_mut(4) {
125 let r = chunk[0] as u32;
126 let g = chunk[1] as u32;
127 let b = chunk[2] as u32;
128 let gray = ((r * 299 + g * 587 + b * 114) / 1000) as u8;
129 chunk[0] = gray;
130 chunk[1] = gray;
131 chunk[2] = gray;
132 }
134}
135
136pub fn render_tiled(tree: &DisplayTree, config: &RenderConfig) -> Result<Bitmap, RenderError> {
146 render_tiled_internal(tree, config, None)
147}
148
149pub fn render_tiled_with_images(
152 tree: &DisplayTree,
153 config: &RenderConfig,
154 decoder: &dyn ImageDecoder,
155) -> Result<Bitmap, RenderError> {
156 render_tiled_internal(tree, config, Some(decoder))
157}
158
159fn render_tiled_internal(
160 tree: &DisplayTree,
161 config: &RenderConfig,
162 decoder: Option<&dyn ImageDecoder>,
163) -> Result<Bitmap, RenderError> {
164 let tile_size = match config.tile_size {
165 Some(ts) if ts > 0 => ts,
166 _ => return render_internal(tree, config, decoder),
167 };
168
169 let width = config.width;
170 let height = config.height;
171
172 let cols = width.div_ceil(tile_size);
173 let rows = height.div_ceil(tile_size);
174 let total_tiles = cols * rows;
175
176 let effective_bg = config
178 .forced_color_scheme
179 .as_ref()
180 .map_or(&config.background, |s| &s.background_color);
181
182 let mut final_bitmap = Bitmap::new(width, height, rpdfium_graphics::BitmapFormat::Rgba32);
184 for pixel in final_bitmap.data.chunks_exact_mut(4) {
186 pixel[0] = effective_bg.r;
187 pixel[1] = effective_bg.g;
188 pixel[2] = effective_bg.b;
189 pixel[3] = effective_bg.a;
190 }
191
192 let page_transform = match config.media_box {
193 Some(ref mb) => compute_page_transform(mb, width, height, config.rotation),
194 None => Matrix::identity(),
195 };
196
197 for row in 0..rows {
198 for col in 0..cols {
199 let tile_x = col * tile_size;
200 let tile_y = row * tile_size;
201 let tile_w = tile_size.min(width - tile_x);
202 let tile_h = tile_size.min(height - tile_y);
203
204 let mut backend = TinySkiaBackend::new();
207 if !config.antialiasing {
208 backend.set_antialiasing(false);
209 }
210 let mut surface = backend.create_surface(tile_w, tile_h, effective_bg);
211
212 let tile_offset = Matrix::from_translation(-(tile_x as f64), -(tile_y as f64));
214 let tile_transform = tile_offset.pre_concat(&page_transform);
215
216 {
217 let mut renderer =
218 DisplayRenderer::new(&mut backend, &mut surface, tile_transform, decoder)
219 .with_per_feature_aa(
220 config.text_antialiasing,
221 config.path_antialiasing,
222 config.image_antialiasing,
223 );
224 if let Some(ref scheme) = config.forced_color_scheme {
225 renderer = renderer.with_forced_color_scheme(scheme.clone());
226 }
227 walk(tree, &mut renderer);
228 }
229
230 let tile_bitmap = backend.finish(surface);
231
232 for ty in 0..tile_h {
234 let dst_y = tile_y + ty;
235 if dst_y >= height {
236 break;
237 }
238 let src_row = tile_bitmap.scanline(ty);
239 let dst_start = (dst_y * final_bitmap.stride + tile_x * 4) as usize;
240 let copy_len = (tile_w * 4) as usize;
241 final_bitmap.data[dst_start..dst_start + copy_len]
242 .copy_from_slice(&src_row[..copy_len]);
243 }
244
245 if let Some(ref progress) = config.progress {
247 if !progress.on_tile_complete(col, row, total_tiles) {
248 return Ok(final_bitmap);
249 }
250 }
251 }
252 }
253
254 if config.grayscale {
255 apply_grayscale(&mut final_bitmap);
256 }
257
258 Ok(final_bitmap)
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use rpdfium_core::Rect;
265 use rpdfium_graphics::{BlendMode, Color, FillRule, PathOp, PathStyle};
266 use rpdfium_page::display::DisplayNode;
267
268 use crate::color_convert::RgbaColor;
269
270 #[test]
271 fn test_render_empty_tree() {
272 let tree = DisplayTree {
273 root: DisplayNode::Group {
274 blend_mode: BlendMode::Normal,
275 clip: None,
276 opacity: 1.0,
277 isolated: false,
278 knockout: false,
279 soft_mask: None,
280 children: Vec::new(),
281 },
282 };
283 let config = RenderConfig {
284 width: 50,
285 height: 50,
286 background: RgbaColor::WHITE,
287 ..RenderConfig::default()
288 };
289 let bitmap = render(&tree, &config).unwrap();
290 assert_eq!(bitmap.width, 50);
291 assert_eq!(bitmap.height, 50);
292 }
293
294 #[test]
295 fn test_render_simple_fill() {
296 let tree = DisplayTree {
297 root: DisplayNode::Group {
298 blend_mode: BlendMode::Normal,
299 clip: None,
300 opacity: 1.0,
301 isolated: false,
302 knockout: false,
303 soft_mask: None,
304 children: vec![DisplayNode::Path {
305 ops: vec![
306 PathOp::MoveTo { x: 0.0, y: 0.0 },
307 PathOp::LineTo { x: 100.0, y: 0.0 },
308 PathOp::LineTo { x: 100.0, y: 100.0 },
309 PathOp::LineTo { x: 0.0, y: 100.0 },
310 PathOp::Close,
311 ],
312 style: PathStyle {
313 fill: Some(FillRule::NonZero),
314 ..PathStyle::default()
315 },
316 matrix: Matrix::identity(),
317 fill_color: Some(Color::rgb(1.0, 0.0, 0.0)),
318 stroke_color: None,
319 fill_color_space: None,
320 stroke_color_space: None,
321 transfer_function: None,
322 overprint: false,
323 overprint_mode: 0,
324 }],
325 },
326 };
327 let config = RenderConfig {
328 width: 100,
329 height: 100,
330 background: RgbaColor::WHITE,
331 ..RenderConfig::default()
332 };
333 let bitmap = render(&tree, &config).unwrap();
334 let idx = (50 * bitmap.stride + 50 * 4) as usize;
336 assert_eq!(bitmap.data[idx], 255); assert_eq!(bitmap.data[idx + 1], 0); assert_eq!(bitmap.data[idx + 2], 0); }
340
341 #[test]
342 fn test_render_with_page_transform() {
343 let tree = DisplayTree {
346 root: DisplayNode::Group {
347 blend_mode: BlendMode::Normal,
348 clip: None,
349 opacity: 1.0,
350 isolated: false,
351 knockout: false,
352 soft_mask: None,
353 children: vec![DisplayNode::Path {
354 ops: vec![
355 PathOp::MoveTo { x: 0.0, y: 0.0 },
356 PathOp::LineTo { x: 200.0, y: 0.0 },
357 PathOp::LineTo { x: 200.0, y: 200.0 },
358 PathOp::LineTo { x: 0.0, y: 200.0 },
359 PathOp::Close,
360 ],
361 style: PathStyle {
362 fill: Some(FillRule::NonZero),
363 ..PathStyle::default()
364 },
365 matrix: Matrix::identity(),
366 fill_color: Some(Color::rgb(1.0, 0.0, 0.0)),
367 stroke_color: None,
368 fill_color_space: None,
369 stroke_color_space: None,
370 transfer_function: None,
371 overprint: false,
372 overprint_mode: 0,
373 }],
374 },
375 };
376 let config = RenderConfig {
377 width: 200,
378 height: 200,
379 background: RgbaColor::WHITE,
380 media_box: Some(Rect::new(0.0, 0.0, 200.0, 200.0)),
381 rotation: 0,
382 ..RenderConfig::default()
383 };
384 let bitmap = render(&tree, &config).unwrap();
385 let idx = (100 * bitmap.stride + 100 * 4) as usize;
387 assert_eq!(bitmap.data[idx], 255); assert_eq!(bitmap.data[idx + 1], 0); assert_eq!(bitmap.data[idx + 2], 0); }
391
392 fn make_red_rect_tree() -> DisplayTree {
395 DisplayTree {
396 root: DisplayNode::Group {
397 blend_mode: BlendMode::Normal,
398 clip: None,
399 opacity: 1.0,
400 isolated: false,
401 knockout: false,
402 soft_mask: None,
403 children: vec![DisplayNode::Path {
404 ops: vec![
405 PathOp::MoveTo { x: 0.0, y: 0.0 },
406 PathOp::LineTo { x: 100.0, y: 0.0 },
407 PathOp::LineTo { x: 100.0, y: 100.0 },
408 PathOp::LineTo { x: 0.0, y: 100.0 },
409 PathOp::Close,
410 ],
411 style: PathStyle {
412 fill: Some(FillRule::NonZero),
413 ..PathStyle::default()
414 },
415 matrix: Matrix::identity(),
416 fill_color: Some(Color::rgb(1.0, 0.0, 0.0)),
417 stroke_color: None,
418 fill_color_space: None,
419 stroke_color_space: None,
420 transfer_function: None,
421 overprint: false,
422 overprint_mode: 0,
423 }],
424 },
425 }
426 }
427
428 #[test]
429 fn test_tiled_render_matches_full_render() {
430 use super::render_tiled;
431
432 let tree = make_red_rect_tree();
433 let config = RenderConfig {
434 width: 100,
435 height: 100,
436 background: RgbaColor::WHITE,
437 ..RenderConfig::default()
438 };
439 let full = render(&tree, &config).unwrap();
440
441 let tiled_config = RenderConfig {
442 width: 100,
443 height: 100,
444 background: RgbaColor::WHITE,
445 tile_size: Some(50),
446 ..RenderConfig::default()
447 };
448 let tiled = render_tiled(&tree, &tiled_config).unwrap();
449
450 assert_eq!(full.width, tiled.width);
451 assert_eq!(full.height, tiled.height);
452 let idx = (50 * full.stride + 50 * 4) as usize;
454 assert_eq!(full.data[idx], tiled.data[idx]); assert_eq!(full.data[idx + 1], tiled.data[idx + 1]); assert_eq!(full.data[idx + 2], tiled.data[idx + 2]); }
458
459 #[test]
460 fn test_tiled_render_progress_callback_count() {
461 use super::render_tiled;
462 use std::sync::Arc;
463 use std::sync::atomic::{AtomicU32, Ordering};
464
465 use crate::render_defines::RenderProgress;
466
467 struct CountProgress {
468 count: AtomicU32,
469 }
470 impl RenderProgress for CountProgress {
471 fn on_tile_complete(&self, _tx: u32, _ty: u32, _total: u32) -> bool {
472 self.count.fetch_add(1, Ordering::Relaxed);
473 true
474 }
475 }
476
477 let tree = make_red_rect_tree();
478 let progress = Arc::new(CountProgress {
479 count: AtomicU32::new(0),
480 });
481 let config = RenderConfig {
482 width: 100,
483 height: 100,
484 background: RgbaColor::WHITE,
485 tile_size: Some(50),
486 progress: Some(progress.clone()),
487 ..RenderConfig::default()
488 };
489 let _ = render_tiled(&tree, &config).unwrap();
490
491 assert_eq!(progress.count.load(Ordering::Relaxed), 4);
493 }
494
495 #[test]
496 fn test_tiled_render_cancellation_stops_early() {
497 use super::render_tiled;
498 use std::sync::Arc;
499 use std::sync::atomic::{AtomicU32, Ordering};
500
501 use crate::render_defines::RenderProgress;
502
503 struct CancelAfterN {
504 count: AtomicU32,
505 cancel_after: u32,
506 }
507 impl RenderProgress for CancelAfterN {
508 fn on_tile_complete(&self, _tx: u32, _ty: u32, _total: u32) -> bool {
509 let c = self.count.fetch_add(1, Ordering::Relaxed) + 1;
510 c < self.cancel_after
511 }
512 }
513
514 let tree = make_red_rect_tree();
515 let progress = Arc::new(CancelAfterN {
516 count: AtomicU32::new(0),
517 cancel_after: 2,
518 });
519 let config = RenderConfig {
520 width: 100,
521 height: 100,
522 background: RgbaColor::WHITE,
523 tile_size: Some(50),
524 progress: Some(progress.clone()),
525 ..RenderConfig::default()
526 };
527 let bitmap = render_tiled(&tree, &config).unwrap();
528 assert_eq!(progress.count.load(Ordering::Relaxed), 2);
530 assert_eq!(bitmap.width, 100);
532 assert_eq!(bitmap.height, 100);
533 }
534
535 #[test]
536 fn test_tiled_render_none_tile_size_falls_back() {
537 use super::render_tiled;
538
539 let tree = make_red_rect_tree();
540 let config = RenderConfig {
541 width: 100,
542 height: 100,
543 background: RgbaColor::WHITE,
544 tile_size: None,
545 ..RenderConfig::default()
546 };
547 let bitmap = render_tiled(&tree, &config).unwrap();
549 assert_eq!(bitmap.width, 100);
550 assert_eq!(bitmap.height, 100);
551 let idx = (50 * bitmap.stride + 50 * 4) as usize;
553 assert_eq!(bitmap.data[idx], 255);
554 }
555
556 #[test]
557 fn test_tiled_render_config_builder() {
558 use std::sync::Arc;
559
560 use crate::render_defines::RenderProgress;
561
562 struct NoopProgress;
563 impl RenderProgress for NoopProgress {
564 fn on_tile_complete(&self, _tx: u32, _ty: u32, _total: u32) -> bool {
565 true
566 }
567 }
568
569 let config = RenderConfig::default()
570 .with_tile_size(128)
571 .with_progress(Arc::new(NoopProgress));
572 assert_eq!(config.tile_size, Some(128));
573 assert!(config.progress.is_some());
574 }
575
576 fn make_white_rect_50() -> DisplayNode {
577 DisplayNode::Path {
578 ops: vec![
579 PathOp::MoveTo { x: 0.0, y: 0.0 },
580 PathOp::LineTo { x: 50.0, y: 0.0 },
581 PathOp::LineTo { x: 50.0, y: 50.0 },
582 PathOp::LineTo { x: 0.0, y: 50.0 },
583 PathOp::Close,
584 ],
585 style: PathStyle {
586 fill: Some(FillRule::NonZero),
587 ..PathStyle::default()
588 },
589 matrix: Matrix::identity(),
590 fill_color: Some(Color::rgb(1.0, 1.0, 1.0)),
591 stroke_color: None,
592 fill_color_space: None,
593 stroke_color_space: None,
594 transfer_function: None,
595 overprint: false,
596 overprint_mode: 0,
597 }
598 }
599
600 #[test]
601 fn test_render_group_opacity_transparent_bg_simple() {
602 let tree = DisplayTree {
604 root: DisplayNode::Group {
605 blend_mode: BlendMode::Normal,
606 clip: None,
607 opacity: 1.0,
608 isolated: true,
609 knockout: false,
610 soft_mask: None,
611 children: vec![DisplayNode::Group {
612 blend_mode: BlendMode::Screen,
613 clip: None,
614 opacity: 0.6,
615 isolated: true,
616 knockout: false,
617 soft_mask: None,
618 children: vec![make_white_rect_50()],
619 }],
620 },
621 };
622 let config = RenderConfig {
623 width: 50,
624 height: 50,
625 background: RgbaColor::new(0, 0, 0, 0),
626 ..RenderConfig::default()
627 };
628 let bitmap = render(&tree, &config).unwrap();
629 let idx = (25 * bitmap.stride + 25 * 4) as usize;
630 let a = bitmap.data[idx + 3];
631 eprintln!(
632 "simple: RGBA=[{},{},{},{}]",
633 bitmap.data[idx],
634 bitmap.data[idx + 1],
635 bitmap.data[idx + 2],
636 a
637 );
638 assert!(a > 140 && a < 166, "Expected alpha ~153 but got {a}");
639 }
640
641 #[test]
642 fn test_render_group_opacity_transparent_bg_with_smask() {
643 use rpdfium_page::display::{SoftMask, SoftMaskSubtype};
646
647 let mask_tree = DisplayTree {
649 root: DisplayNode::Group {
650 blend_mode: BlendMode::Normal,
651 clip: None,
652 opacity: 1.0,
653 isolated: true,
654 knockout: false,
655 soft_mask: None,
656 children: vec![make_white_rect_50()],
657 },
658 };
659
660 let smask = SoftMask {
661 subtype: SoftMaskSubtype::Alpha,
662 group: mask_tree,
663 backdrop_color: None,
664 transfer_function: None,
665 };
666
667 let tree = DisplayTree {
668 root: DisplayNode::Group {
669 blend_mode: BlendMode::Normal,
670 clip: None,
671 opacity: 1.0,
672 isolated: true,
673 knockout: false,
674 soft_mask: None,
675 children: vec![DisplayNode::Group {
676 blend_mode: BlendMode::Normal,
677 clip: None,
678 opacity: 1.0,
679 isolated: true,
680 knockout: false,
681 soft_mask: Some(Box::new(smask)),
682 children: vec![DisplayNode::Group {
683 blend_mode: BlendMode::Normal,
684 clip: None,
685 opacity: 1.0,
686 isolated: true,
687 knockout: false,
688 soft_mask: None,
689 children: vec![DisplayNode::Group {
690 blend_mode: BlendMode::Screen,
691 clip: None,
692 opacity: 0.6,
693 isolated: true,
694 knockout: false,
695 soft_mask: None,
696 children: vec![make_white_rect_50()],
697 }],
698 }],
699 }],
700 },
701 };
702 let config = RenderConfig {
703 width: 50,
704 height: 50,
705 background: RgbaColor::new(0, 0, 0, 0),
706 ..RenderConfig::default()
707 };
708 let bitmap = render(&tree, &config).unwrap();
709 let idx = (25 * bitmap.stride + 25 * 4) as usize;
710 let a = bitmap.data[idx + 3];
711 eprintln!(
712 "with_smask: RGBA=[{},{},{},{}]",
713 bitmap.data[idx],
714 bitmap.data[idx + 1],
715 bitmap.data[idx + 2],
716 a
717 );
718 assert!(a > 140 && a < 166, "Expected alpha ~153 but got {a}");
719 }
720
721 #[test]
724 fn test_render_group_half_opacity_on_opaque_bg() {
725 let tree = DisplayTree {
728 root: DisplayNode::Group {
729 blend_mode: BlendMode::Normal,
730 clip: None,
731 opacity: 1.0,
732 isolated: true,
733 knockout: false,
734 soft_mask: None,
735 children: vec![DisplayNode::Group {
736 blend_mode: BlendMode::Normal,
737 clip: None,
738 opacity: 0.5,
739 isolated: true,
740 knockout: false,
741 soft_mask: None,
742 children: vec![make_white_rect_50()],
743 }],
744 },
745 };
746 let config = RenderConfig {
747 width: 50,
748 height: 50,
749 background: RgbaColor::new(255, 0, 0, 255), ..RenderConfig::default()
751 };
752 let bitmap = render(&tree, &config).unwrap();
753 let idx = (25 * bitmap.stride + 25 * 4) as usize;
754 let r = bitmap.data[idx];
755 let g = bitmap.data[idx + 1];
756 let b = bitmap.data[idx + 2];
757 let a = bitmap.data[idx + 3];
758 assert_eq!(a, 255, "alpha should be 255 on opaque bg");
760 assert!(r > 200, "R={r}: red component should stay high");
762 assert!(g > 100 && g < 160, "G={g}: green should be ~128");
763 assert!(b > 100 && b < 160, "B={b}: blue should be ~128");
764 }
765
766 #[test]
767 fn test_render_group_multiply_blend_mode() {
768 let grey_rect = DisplayNode::Path {
771 ops: vec![
772 PathOp::MoveTo { x: 0.0, y: 0.0 },
773 PathOp::LineTo { x: 50.0, y: 0.0 },
774 PathOp::LineTo { x: 50.0, y: 50.0 },
775 PathOp::LineTo { x: 0.0, y: 50.0 },
776 PathOp::Close,
777 ],
778 style: PathStyle {
779 fill: Some(FillRule::NonZero),
780 ..PathStyle::default()
781 },
782 matrix: rpdfium_core::Matrix::identity(),
783 fill_color: Some(Color::rgb(0.5, 0.5, 0.5)),
784 stroke_color: None,
785 fill_color_space: None,
786 stroke_color_space: None,
787 transfer_function: None,
788 overprint: false,
789 overprint_mode: 0,
790 };
791 let tree = DisplayTree {
792 root: DisplayNode::Group {
793 blend_mode: BlendMode::Normal,
794 clip: None,
795 opacity: 1.0,
796 isolated: true,
797 knockout: false,
798 soft_mask: None,
799 children: vec![DisplayNode::Group {
800 blend_mode: BlendMode::Multiply,
801 clip: None,
802 opacity: 1.0,
803 isolated: true,
804 knockout: false,
805 soft_mask: None,
806 children: vec![grey_rect],
807 }],
808 },
809 };
810 let config = RenderConfig {
811 width: 50,
812 height: 50,
813 background: RgbaColor::new(255, 255, 255, 255), ..RenderConfig::default()
815 };
816 let bitmap = render(&tree, &config).unwrap();
817 let idx = (25 * bitmap.stride + 25 * 4) as usize;
818 let r = bitmap.data[idx];
819 let g = bitmap.data[idx + 1];
820 let b = bitmap.data[idx + 2];
821 assert!(r > 113 && r < 143, "R={r}: expected ~128");
823 assert!(g > 113 && g < 143, "G={g}: expected ~128");
824 assert!(b > 113 && b < 143, "B={b}: expected ~128");
825 }
826
827 #[test]
828 fn test_render_group_isolated_contains_content_within_boundary() {
829 let tree = DisplayTree {
832 root: DisplayNode::Group {
833 blend_mode: BlendMode::Normal,
834 clip: None,
835 opacity: 1.0,
836 isolated: true,
837 knockout: false,
838 soft_mask: None,
839 children: vec![DisplayNode::Group {
840 blend_mode: BlendMode::Normal,
841 clip: None,
842 opacity: 0.8,
843 isolated: true,
844 knockout: false,
845 soft_mask: None,
846 children: vec![make_white_rect_50()],
847 }],
848 },
849 };
850 let config = RenderConfig {
851 width: 100,
852 height: 100,
853 background: RgbaColor::new(0, 0, 0, 0), ..RenderConfig::default()
855 };
856 let bitmap = render(&tree, &config).unwrap();
857 let inside_idx = (25 * bitmap.stride + 25 * 4) as usize;
859 let inside_a = bitmap.data[inside_idx + 3];
860 assert!(
861 inside_a > 0,
862 "inside area should have alpha > 0, got {inside_a}"
863 );
864 let outside_idx = (80 * bitmap.stride + 80 * 4) as usize;
866 let outside_a = bitmap.data[outside_idx + 3];
867 assert_eq!(
868 outside_a, 0,
869 "outside area should be transparent, got {outside_a}"
870 );
871 }
872
873 #[test]
874 fn test_forced_color_overrides_fill() {
875 let tree = make_red_rect_tree();
877 let config = RenderConfig {
878 width: 100,
879 height: 100,
880 ..RenderConfig::default()
881 }
882 .with_forced_colors(
883 RgbaColor::new(0, 255, 0, 255), RgbaColor::new(0, 0, 255, 255), );
886 let bitmap = render(&tree, &config).unwrap();
887
888 let idx = (50 * bitmap.stride + 50 * 4) as usize;
890 assert_eq!(bitmap.data[idx], 0, "R should be 0 (forced green)");
891 assert_eq!(bitmap.data[idx + 1], 255, "G should be 255 (forced green)");
892 assert_eq!(bitmap.data[idx + 2], 0, "B should be 0 (forced green)");
893 }
894
895 #[test]
896 fn test_forced_color_background_applied() {
897 let tree = DisplayTree {
899 root: DisplayNode::Group {
900 blend_mode: BlendMode::Normal,
901 clip: None,
902 opacity: 1.0,
903 isolated: false,
904 knockout: false,
905 soft_mask: None,
906 children: Vec::new(),
907 },
908 };
909 let config = RenderConfig {
910 width: 10,
911 height: 10,
912 ..RenderConfig::default()
913 }
914 .with_forced_colors(
915 RgbaColor::new(0, 0, 0, 255),
916 RgbaColor::new(0, 0, 255, 255), );
918 let bitmap = render(&tree, &config).unwrap();
919 let idx = (5 * bitmap.stride + 5 * 4) as usize;
921 assert_eq!(bitmap.data[idx], 0, "R");
922 assert_eq!(bitmap.data[idx + 1], 0, "G");
923 assert_eq!(bitmap.data[idx + 2], 255, "B");
924 assert_eq!(bitmap.data[idx + 3], 255, "A");
925 }
926}