1use std::collections::HashMap;
9use std::sync::Arc;
10
11use rpdfium_core::{Matrix, Name};
12use rpdfium_font::ResolvedFont;
13use rpdfium_font::font_type::PdfFontType;
14#[cfg(test)]
15use rpdfium_graphics::TextRenderingMode;
16use rpdfium_graphics::{
17 BlendMode, ClipPath, Color, ColorSpaceFamily, FillRule, ImageRef, PathOp, PathStyle,
18};
19use rpdfium_page::ShadingDict;
20use rpdfium_page::display::{DisplayTree, DisplayVisitor, SoftMask, SoftMaskSubtype, TextRun};
21use rpdfium_page::pattern::TilingPattern;
22use rpdfium_parser::Operand;
23
24use crate::cfx_glyphcache::{MAX_CACHED_SIZE, RasterGlyph, RasterGlyphKey, RasterizedGlyphCache};
25use crate::color_convert::RgbaColor;
26use crate::image::ImageDecoder;
27use crate::render_defines::ColorScheme;
28use crate::renderdevicedriver_iface::RenderBackend;
29use crate::stroke::StrokeStyle;
30
31#[derive(Debug, Clone, Copy)]
33enum GroupAction {
34 Nothing,
35 ClipOnly,
36 GroupOnly,
37 ClipAndGroup,
38}
39
40const MAX_GLYPH_CACHE_SIZE: usize = 4096;
42
43pub struct DisplayRenderer<'a, B: RenderBackend> {
46 backend: &'a mut B,
47 surface: &'a mut B::Surface,
48 page_transform: Matrix,
49 image_decoder: Option<&'a dyn ImageDecoder>,
50 group_stack: Vec<GroupAction>,
51 fallback_fonts: Vec<Arc<ResolvedFont>>,
53 text_clip_ops: Vec<PathOp>,
56 text_clip_depth: usize,
59 glyph_cache: HashMap<(usize, u16), Option<Vec<PathOp>>>,
62 raster_cache: RasterizedGlyphCache,
64 forced_color_scheme: Option<ColorScheme>,
66 text_antialiasing: bool,
68 path_antialiasing: bool,
69 image_antialiasing: bool,
70}
71
72impl<'a, B: RenderBackend> DisplayRenderer<'a, B> {
73 pub fn new(
75 backend: &'a mut B,
76 surface: &'a mut B::Surface,
77 page_transform: Matrix,
78 image_decoder: Option<&'a dyn ImageDecoder>,
79 ) -> Self {
80 Self {
81 backend,
82 surface,
83 page_transform,
84 image_decoder,
85 group_stack: Vec::new(),
86 fallback_fonts: Vec::new(),
87 text_clip_ops: Vec::new(),
88 text_clip_depth: 0,
89 glyph_cache: HashMap::new(),
90 raster_cache: RasterizedGlyphCache::new(),
91 forced_color_scheme: None,
92 text_antialiasing: true,
93 path_antialiasing: true,
94 image_antialiasing: true,
95 }
96 }
97
98 pub fn with_fallback_fonts(mut self, fonts: Vec<Arc<ResolvedFont>>) -> Self {
100 self.fallback_fonts = fonts;
101 self
102 }
103
104 pub fn with_forced_color_scheme(mut self, scheme: ColorScheme) -> Self {
106 self.forced_color_scheme = Some(scheme);
107 self
108 }
109
110 pub fn with_per_feature_aa(mut self, text_aa: bool, path_aa: bool, image_aa: bool) -> Self {
112 self.text_antialiasing = text_aa;
113 self.path_antialiasing = path_aa;
114 self.image_antialiasing = image_aa;
115 self
116 }
117}
118
119impl<B: RenderBackend> DisplayRenderer<'_, B> {
120 fn render_text_run(&mut self, run: &TextRun) {
122 self.backend.set_antialiasing(self.text_antialiasing);
124
125 let resolved_font = match run.resolved_font.as_ref() {
126 Some(font) => font,
127 None => return, };
129
130 if resolved_font.font_type == PdfFontType::Type3 {
132 self.render_type3_text_run(run, resolved_font);
133 return;
134 }
135
136 let units_per_em = resolved_font.units_per_em().unwrap_or(1000) as f32;
137 if units_per_em == 0.0 {
138 return;
139 }
140
141 let rendering_mode = run.rendering_mode;
142 if rendering_mode.is_invisible() {
144 return;
145 }
146
147 let should_fill = rendering_mode.is_fill();
148 let should_stroke = rendering_mode.is_stroke();
149 let should_clip = rendering_mode.is_clip();
150
151 let fill_rgba = if let Some(ref scheme) = self.forced_color_scheme {
152 scheme.text_color
153 } else if should_fill {
154 run.fill_color
155 .as_ref()
156 .map(|c| RgbaColor::from_pdf_color(c, 1.0))
157 .unwrap_or(RgbaColor::BLACK)
158 } else {
159 RgbaColor::BLACK
160 };
161
162 let stroke_rgba = if let Some(ref scheme) = self.forced_color_scheme {
163 scheme.text_color
164 } else if should_stroke {
165 run.stroke_color
166 .as_ref()
167 .map(|c| RgbaColor::from_pdf_color(c, 1.0))
168 .unwrap_or(RgbaColor::BLACK)
169 } else {
170 RgbaColor::BLACK
171 };
172
173 let font_size = run.font_size;
174 let scale = font_size / units_per_em;
175 let rise = run.rise;
176 let is_vertical = run.is_vertical;
177
178 let mut x_pos: f32 = 0.0;
182 let mut y_pos: f32 = 0.0;
183
184 let can_apply_spacing_heuristic = !is_vertical
187 && !resolved_font.is_embedded
188 && matches!(
189 resolved_font.font_type,
190 PdfFontType::TrueType | PdfFontType::CIDFontType2
191 )
192 && !resolved_font.widths.is_empty();
193
194 for (i, &byte) in run.text.iter().enumerate() {
195 let (glyph_id, active_font, active_scale) =
197 match resolved_font.glyph_from_char_code(byte as u16) {
198 Some(gid) => (gid, resolved_font.as_ref(), scale),
199 None => {
200 match self.find_fallback_glyph(byte as u16, font_size) {
202 Some(info) => info,
203 None => {
204 if i < run.positions.len() {
206 if is_vertical {
207 y_pos -= run.positions[i];
208 } else {
209 x_pos += run.positions[i];
210 }
211 }
212 continue;
213 }
214 }
215 }
216 };
217
218 let using_fallback = !std::ptr::eq(active_font, resolved_font.as_ref());
220
221 let mut x_shift: f32 = 0.0;
226 let mut h_scale: f32 = 1.0;
227 if can_apply_spacing_heuristic && using_fallback {
228 let pdf_glyph_width = resolved_font.char_width(byte as u16) as f32;
229 if let Some(outline) = active_font.glyph_outline(glyph_id) {
230 let font_upem = active_font.units_per_em().unwrap_or(1000) as f32;
231 if font_upem > 0.0 {
232 let font_glyph_width = outline.advance_width / font_upem * 1000.0;
234 if pdf_glyph_width > font_glyph_width + 1.0 {
235 x_shift = (pdf_glyph_width - font_glyph_width) * font_size / 2000.0;
237 } else if pdf_glyph_width > 0.0
238 && font_glyph_width > 0.0
239 && pdf_glyph_width < font_glyph_width
240 {
241 h_scale = pdf_glyph_width / font_glyph_width;
243 }
244 }
245 }
246 }
247
248 let font_key = active_font as *const ResolvedFont as usize;
250 let cache_key = (font_key, glyph_id);
251 if !self.glyph_cache.contains_key(&cache_key) {
252 let ops = active_font.glyph_outline(glyph_id).map(|o| o.ops.clone());
253 if self.glyph_cache.len() >= MAX_GLYPH_CACHE_SIZE {
255 self.glyph_cache.clear();
256 }
257 self.glyph_cache.insert(cache_key, ops);
258 }
259
260 let outline_ops = match self.glyph_cache.get(&cache_key) {
261 Some(Some(ops)) => ops,
262 _ => {
263 if i < run.positions.len() {
264 if is_vertical {
265 y_pos -= run.positions[i];
266 } else {
267 x_pos += run.positions[i];
268 }
269 }
270 continue;
271 }
272 };
273
274 let glyph_transform = if is_vertical {
280 let (vx, vy) = run.vert_origins.get(i).copied().unwrap_or((0, 880));
281 let gx = -(vx as f32) * font_size / 1000.0;
282 let gy = y_pos - vy as f32 * font_size / 1000.0;
283 let glyph_translate = Matrix::from_translation(gx as f64, gy as f64);
284 let glyph_scale =
285 Matrix::new(active_scale as f64, 0.0, 0.0, active_scale as f64, 0.0, 0.0);
286 run.matrix
287 .pre_concat(&glyph_translate)
288 .pre_concat(&glyph_scale)
289 } else {
290 let glyph_translate =
291 Matrix::from_translation((x_pos + x_shift) as f64, rise as f64);
292 let glyph_scale = Matrix::new(
293 (active_scale * h_scale) as f64,
294 0.0,
295 0.0,
296 active_scale as f64,
297 0.0,
298 0.0,
299 );
300 run.matrix
301 .pre_concat(&glyph_translate)
302 .pre_concat(&glyph_scale)
303 };
304
305 let final_transform = self.page_transform.pre_concat(&glyph_transform);
307
308 if !is_vertical
311 && should_fill
312 && !should_stroke
313 && !should_clip
314 && font_size < MAX_CACHED_SIZE
315 {
316 let raster_key = RasterGlyphKey {
317 font_id: font_key,
318 glyph_id,
319 size_q: RasterizedGlyphCache::quantize_size(font_size),
320 };
321
322 if let Some(cached) = self.raster_cache.get(&raster_key) {
323 match cached {
324 Some(glyph) => {
325 self.backend.draw_alpha_bitmap(
326 self.surface,
327 &glyph.alpha,
328 glyph.width,
329 glyph.height,
330 glyph.bearing_x,
331 glyph.bearing_y,
332 &fill_rgba,
333 &final_transform,
334 );
335 if i < run.positions.len() {
336 x_pos += run.positions[i];
337 }
338 continue;
339 }
340 None => {
341 if i < run.positions.len() {
343 x_pos += run.positions[i];
344 }
345 continue;
346 }
347 }
348 }
349
350 let pixel_size = font_size.ceil() as u32;
353 let glyph_dim = (pixel_size * 2).min(256);
354 if glyph_dim > 0 {
355 let raster =
356 rasterize_glyph_alpha(outline_ops, active_scale, pixel_size, glyph_dim);
357 if let Some(ref rg) = raster {
358 self.backend.draw_alpha_bitmap(
359 self.surface,
360 &rg.alpha,
361 rg.width,
362 rg.height,
363 rg.bearing_x,
364 rg.bearing_y,
365 &fill_rgba,
366 &final_transform,
367 );
368 }
369 self.raster_cache.insert(raster_key, raster);
370 if i < run.positions.len() {
371 x_pos += run.positions[i];
372 }
373 continue;
374 }
375 }
376
377 if should_fill {
379 self.backend.fill_path(
380 self.surface,
381 outline_ops,
382 FillRule::NonZero,
383 &fill_rgba,
384 &final_transform,
385 );
386 }
387 if should_stroke {
388 let stroke_style = StrokeStyle {
389 width: 1.0, line_cap: rpdfium_graphics::LineCapStyle::default(),
391 line_join: rpdfium_graphics::LineJoinStyle::default(),
392 miter_limit: 10.0,
393 dash: None,
394 };
395 self.backend.stroke_path(
396 self.surface,
397 outline_ops,
398 &stroke_style,
399 &stroke_rgba,
400 &final_transform,
401 );
402 }
403
404 if should_clip {
406 for op in outline_ops {
408 self.text_clip_ops
409 .push(transform_path_op(op, &final_transform));
410 }
411 }
412
413 if i < run.positions.len() {
415 if is_vertical {
416 y_pos -= run.positions[i];
417 } else {
418 x_pos += run.positions[i];
419 }
420 }
421 }
422 }
423
424 fn find_fallback_glyph(
429 &self,
430 char_code: u16,
431 font_size: f32,
432 ) -> Option<(u16, &ResolvedFont, f32)> {
433 for fb_font in &self.fallback_fonts {
434 if let Some(gid) = fb_font.glyph_from_char_code(char_code) {
435 let fb_upem = fb_font.units_per_em().unwrap_or(1000) as f32;
436 if fb_upem == 0.0 {
437 continue;
438 }
439 let fb_scale = font_size / fb_upem;
440 return Some((gid, fb_font.as_ref(), fb_scale));
441 }
442 }
443 None
444 }
445
446 fn render_type3_text_run(&mut self, run: &TextRun, resolved_font: &rpdfium_font::ResolvedFont) {
452 let type3 = match resolved_font.type3.as_ref() {
453 Some(t3) => t3,
454 None => return,
455 };
456
457 let rendering_mode = run.rendering_mode;
458 if rendering_mode.is_invisible() {
459 return;
460 }
461
462 let should_fill = rendering_mode.is_fill();
463 let should_stroke = rendering_mode.is_stroke();
464 let should_clip = rendering_mode.is_clip();
465
466 let fill_rgba = if let Some(ref scheme) = self.forced_color_scheme {
467 scheme.text_color
468 } else if should_fill {
469 run.fill_color
470 .as_ref()
471 .map(|c| RgbaColor::from_pdf_color(c, 1.0))
472 .unwrap_or(RgbaColor::BLACK)
473 } else {
474 RgbaColor::BLACK
475 };
476
477 let stroke_rgba = if let Some(ref scheme) = self.forced_color_scheme {
478 scheme.text_color
479 } else if should_stroke {
480 run.stroke_color
481 .as_ref()
482 .map(|c| RgbaColor::from_pdf_color(c, 1.0))
483 .unwrap_or(RgbaColor::BLACK)
484 } else {
485 RgbaColor::BLACK
486 };
487
488 let fm = &type3.font_matrix;
490 let font_matrix = Matrix::new(
491 fm[0] as f64,
492 fm[1] as f64,
493 fm[2] as f64,
494 fm[3] as f64,
495 fm[4] as f64,
496 fm[5] as f64,
497 );
498
499 let font_size = run.font_size;
500 let rise = run.rise;
501 let mut x_pos: f32 = 0.0;
502
503 for (i, &byte) in run.text.iter().enumerate() {
504 let glyph_translate = Matrix::from_translation(x_pos as f64, rise as f64);
506 let size_scale = Matrix::new(font_size as f64, 0.0, 0.0, font_size as f64, 0.0, 0.0);
507 let glyph_transform = run
508 .matrix
509 .pre_concat(&glyph_translate)
510 .pre_concat(&size_scale)
511 .pre_concat(&font_matrix);
512
513 let final_transform = self.page_transform.pre_concat(&glyph_transform);
514
515 let glyph_ops = run.type3_glyph_ops.as_ref().and_then(|map| map.get(&byte));
517
518 let ops_to_render: &[PathOp];
519 let placeholder_ops;
520
521 if let Some(ops) = glyph_ops {
522 if !ops.is_empty() {
523 ops_to_render = ops;
524 } else {
525 if i < run.positions.len() {
527 x_pos += run.positions[i];
528 }
529 continue;
530 }
531 } else if type3.char_proc_for_code(byte).is_some() {
532 let width = type3.char_width_f(byte);
534 placeholder_ops = vec![
535 PathOp::MoveTo { x: 0.0, y: 0.0 },
536 PathOp::LineTo { x: width, y: 0.0 },
537 PathOp::LineTo {
538 x: width,
539 y: 1000.0,
540 },
541 PathOp::LineTo { x: 0.0, y: 1000.0 },
542 PathOp::Close,
543 ];
544 ops_to_render = &placeholder_ops;
545 } else {
546 if i < run.positions.len() {
548 x_pos += run.positions[i];
549 }
550 continue;
551 }
552
553 if should_fill {
554 self.backend.fill_path(
555 self.surface,
556 ops_to_render,
557 FillRule::NonZero,
558 &fill_rgba,
559 &final_transform,
560 );
561 }
562 if should_stroke {
563 let stroke_style = StrokeStyle {
564 width: 1.0,
565 line_cap: rpdfium_graphics::LineCapStyle::default(),
566 line_join: rpdfium_graphics::LineJoinStyle::default(),
567 miter_limit: 10.0,
568 dash: None,
569 };
570 self.backend.stroke_path(
571 self.surface,
572 ops_to_render,
573 &stroke_style,
574 &stroke_rgba,
575 &final_transform,
576 );
577 }
578
579 if should_clip {
581 for op in ops_to_render {
582 self.text_clip_ops
583 .push(transform_path_op(op, &final_transform));
584 }
585 }
586
587 if i < run.positions.len() {
589 x_pos += run.positions[i];
590 }
591 }
592 }
593}
594
595fn transform_path_op(op: &PathOp, m: &Matrix) -> PathOp {
601 use rpdfium_core::Point;
602 match *op {
603 PathOp::MoveTo { x, y } => {
604 let p = m.transform_point(Point {
605 x: x as f64,
606 y: y as f64,
607 });
608 PathOp::MoveTo {
609 x: p.x as f32,
610 y: p.y as f32,
611 }
612 }
613 PathOp::LineTo { x, y } => {
614 let p = m.transform_point(Point {
615 x: x as f64,
616 y: y as f64,
617 });
618 PathOp::LineTo {
619 x: p.x as f32,
620 y: p.y as f32,
621 }
622 }
623 PathOp::CurveTo {
624 x1,
625 y1,
626 x2,
627 y2,
628 x3,
629 y3,
630 } => {
631 let p1 = m.transform_point(Point {
632 x: x1 as f64,
633 y: y1 as f64,
634 });
635 let p2 = m.transform_point(Point {
636 x: x2 as f64,
637 y: y2 as f64,
638 });
639 let p3 = m.transform_point(Point {
640 x: x3 as f64,
641 y: y3 as f64,
642 });
643 PathOp::CurveTo {
644 x1: p1.x as f32,
645 y1: p1.y as f32,
646 x2: p2.x as f32,
647 y2: p2.y as f32,
648 x3: p3.x as f32,
649 y3: p3.y as f32,
650 }
651 }
652 PathOp::Close => PathOp::Close,
653 }
654}
655
656fn rasterize_glyph_alpha(
662 outline_ops: &[PathOp],
663 scale: f32,
664 _pixel_size: u32,
665 dim: u32,
666) -> Option<RasterGlyph> {
667 if outline_ops.is_empty() || dim == 0 {
668 return None;
669 }
670
671 let mut min_x = f32::MAX;
673 let mut min_y = f32::MAX;
674 let mut max_x = f32::MIN;
675 let mut max_y = f32::MIN;
676 for op in outline_ops {
677 match *op {
678 PathOp::MoveTo { x, y } | PathOp::LineTo { x, y } => {
679 min_x = min_x.min(x);
680 min_y = min_y.min(y);
681 max_x = max_x.max(x);
682 max_y = max_y.max(y);
683 }
684 PathOp::CurveTo {
685 x1,
686 y1,
687 x2,
688 y2,
689 x3,
690 y3,
691 } => {
692 min_x = min_x.min(x1).min(x2).min(x3);
693 min_y = min_y.min(y1).min(y2).min(y3);
694 max_x = max_x.max(x1).max(x2).max(x3);
695 max_y = max_y.max(y1).max(y2).max(y3);
696 }
697 PathOp::Close => {}
698 }
699 }
700
701 if min_x >= max_x || min_y >= max_y {
702 return None;
703 }
704
705 let width = ((max_x - min_x) * scale).ceil() as u32 + 2; let height = ((max_y - min_y) * scale).ceil() as u32 + 2;
708 let width = width.min(dim).max(1);
709 let height = height.min(dim).max(1);
710
711 let mut pixmap = tiny_skia::Pixmap::new(width, height)?;
712
713 let mut pb = tiny_skia::PathBuilder::new();
715 for op in outline_ops {
716 match *op {
717 PathOp::MoveTo { x, y } => pb.move_to(x, y),
718 PathOp::LineTo { x, y } => pb.line_to(x, y),
719 PathOp::CurveTo {
720 x1,
721 y1,
722 x2,
723 y2,
724 x3,
725 y3,
726 } => pb.cubic_to(x1, y1, x2, y2, x3, y3),
727 PathOp::Close => pb.close(),
728 }
729 }
730 let path = pb.finish()?;
731
732 let ts = tiny_skia::Transform::from_row(
734 scale,
735 0.0,
736 0.0,
737 -scale, -min_x * scale + 1.0, max_y * scale + 1.0,
740 );
741
742 let mut paint = tiny_skia::Paint::default();
743 paint.set_color(tiny_skia::Color::WHITE);
744 paint.anti_alias = true;
745
746 pixmap.fill_path(&path, &paint, tiny_skia::FillRule::Winding, ts, None);
747
748 let pixels = pixmap.data();
750 let alpha: Vec<u8> = pixels.chunks_exact(4).map(|px| px[3]).collect();
751
752 let bearing_x = (min_x * scale).floor() as i32 - 1;
753 let bearing_y = (max_y * scale).ceil() as i32 + 1;
754
755 Some(RasterGlyph {
756 width,
757 height,
758 bearing_x,
759 bearing_y,
760 alpha,
761 })
762}
763
764fn is_cmyk_family(cs: Option<&ColorSpaceFamily>) -> bool {
769 matches!(cs, Some(ColorSpaceFamily::DeviceCMYK))
770}
771
772fn image_unit_transform(img: &crate::image::DecodedImage, transform: &Matrix) -> Matrix {
785 let w = img.width.max(1) as f64;
786 let h = img.height.max(1) as f64;
787 let pre_scale = Matrix::new(1.0 / w, 0.0, 0.0, -1.0 / h, 0.0, 1.0);
788 transform.pre_concat(&pre_scale)
789}
790
791fn should_interpolate_image(img: &crate::image::DecodedImage, transform: &Matrix) -> bool {
801 let src_w = img.width as f64;
805 let src_h = img.height as f64;
806
807 let dest_w = src_w * (transform.a * transform.a + transform.b * transform.b).sqrt();
812 let dest_h = src_h * (transform.c * transform.c + transform.d * transform.d).sqrt();
813
814 if dest_w < 1.0 || dest_h < 1.0 {
815 return false;
816 }
817
818 dest_h / 8.0 < src_w * src_h / dest_w
820}
821
822fn apply_transfer(color: &mut RgbaColor, tf: &rpdfium_page::function::TransferFunction) {
827 use rpdfium_page::function::{TransferFunction, evaluate};
828 match tf {
829 TransferFunction::Identity => {}
830 TransferFunction::Single(f) => {
831 let rv = evaluate(f, &[color.r as f32 / 255.0]);
832 let gv = evaluate(f, &[color.g as f32 / 255.0]);
833 let bv = evaluate(f, &[color.b as f32 / 255.0]);
834 color.r = (rv.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0) as u8;
835 color.g = (gv.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0) as u8;
836 color.b = (bv.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0) as u8;
837 }
838 TransferFunction::PerComponent { r, g, b, k: _ } => {
839 let rv = evaluate(r.as_ref(), &[color.r as f32 / 255.0]);
840 let gv = evaluate(g.as_ref(), &[color.g as f32 / 255.0]);
841 let bv = evaluate(b.as_ref(), &[color.b as f32 / 255.0]);
842 color.r = (rv.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0) as u8;
843 color.g = (gv.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0) as u8;
844 color.b = (bv.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0) as u8;
845 }
846 }
847}
848
849const MAX_PATTERN_TILES: i32 = 512;
851
852#[allow(clippy::too_many_arguments)]
861fn render_pattern_fill<B: RenderBackend>(
862 backend: &mut B,
863 surface: &mut B::Surface,
864 path_ops: &[PathOp],
865 fill_rule: FillRule,
866 pattern: &TilingPattern,
867 pattern_tree: &DisplayTree,
868 page_transform: &Matrix,
869 ctm: &Matrix,
870 image_decoder: Option<&dyn ImageDecoder>,
871) {
872 use rpdfium_page::display::walk as walk_tree;
873
874 let x_step = pattern.x_step.abs();
875 let y_step = pattern.y_step.abs();
876 if x_step < 0.001 || y_step < 0.001 {
877 return; }
879
880 let device_ctm = page_transform.pre_concat(ctm);
882 let combined = device_ctm.pre_concat(&pattern.matrix);
883 let cell_w = (x_step as f64 * combined.a.abs().max(combined.c.abs())).ceil() as u32;
884 let cell_h = (y_step as f64 * combined.d.abs().max(combined.b.abs())).ceil() as u32;
885 if cell_w == 0 || cell_h == 0 || cell_w > 4096 || cell_h > 4096 {
886 return; }
888
889 let cell_transform =
891 Matrix::from_scale(cell_w as f64 / x_step as f64, cell_h as f64 / y_step as f64);
892 let mut cell_surface = backend.create_surface(cell_w, cell_h, &RgbaColor::TRANSPARENT);
893 {
894 let mut sub_renderer =
895 DisplayRenderer::new(backend, &mut cell_surface, cell_transform, image_decoder);
896 walk_tree(pattern_tree, &mut sub_renderer);
897 }
898 let cell_pixels = backend.surface_pixels(&cell_surface);
899
900 let mut clip = ClipPath::new();
902 clip.push(path_ops.to_vec(), fill_rule);
903 backend.push_clip(surface, &clip, &device_ctm);
904
905 let bbox = path_bounding_box(path_ops);
907
908 let p1 = combined.transform_point(rpdfium_core::Point {
910 x: bbox[0] as f64,
911 y: bbox[1] as f64,
912 });
913 let p2 = combined.transform_point(rpdfium_core::Point {
914 x: bbox[2] as f64,
915 y: bbox[3] as f64,
916 });
917 let px_min_x = p1.x.min(p2.x) as i32;
918 let px_min_y = p1.y.min(p2.y) as i32;
919 let px_max_x = p1.x.max(p2.x).ceil() as i32;
920 let px_max_y = p1.y.max(p2.y).ceil() as i32;
921
922 let cell_w_i = cell_w as i32;
924 let cell_h_i = cell_h as i32;
925 if cell_w_i == 0 || cell_h_i == 0 {
926 backend.pop_clip(surface);
927 return;
928 }
929
930 let start_col = px_min_x.div_euclid(cell_w_i) - 1;
932 let end_col = px_max_x.div_euclid(cell_w_i) + 1;
933 let start_row = px_min_y.div_euclid(cell_h_i) - 1;
934 let end_row = px_max_y.div_euclid(cell_h_i) + 1;
935
936 let n_cols = (end_col - start_col).min(MAX_PATTERN_TILES);
938 let n_rows = (end_row - start_row).min(MAX_PATTERN_TILES);
939
940 let decoded = crate::image::DecodedImage {
942 width: cell_w,
943 height: cell_h,
944 data: cell_pixels,
945 format: crate::image::DecodedImageFormat::Rgba32,
946 };
947
948 for row in 0..n_rows {
949 for col in 0..n_cols {
950 let tile_x = (start_col + col) * cell_w_i;
951 let tile_y = (start_row + row) * cell_h_i;
952 let tile_transform = Matrix::new(
955 cell_w as f64,
956 0.0,
957 0.0,
958 cell_h as f64,
959 tile_x as f64,
960 tile_y as f64,
961 );
962 backend.draw_image(surface, &decoded, &tile_transform, false);
963 }
964 }
965
966 backend.pop_clip(surface);
967}
968
969fn path_bounding_box(ops: &[PathOp]) -> [f32; 4] {
972 let mut min_x = f32::MAX;
973 let mut min_y = f32::MAX;
974 let mut max_x = f32::MIN;
975 let mut max_y = f32::MIN;
976
977 for op in ops {
978 match *op {
979 PathOp::MoveTo { x, y } | PathOp::LineTo { x, y } => {
980 min_x = min_x.min(x);
981 min_y = min_y.min(y);
982 max_x = max_x.max(x);
983 max_y = max_y.max(y);
984 }
985 PathOp::CurveTo {
986 x1,
987 y1,
988 x2,
989 y2,
990 x3,
991 y3,
992 } => {
993 min_x = min_x.min(x1).min(x2).min(x3);
994 min_y = min_y.min(y1).min(y2).min(y3);
995 max_x = max_x.max(x1).max(x2).max(x3);
996 max_y = max_y.max(y1).max(y2).max(y3);
997 }
998 PathOp::Close => {}
999 }
1000 }
1001
1002 if min_x > max_x {
1003 [0.0, 0.0, 0.0, 0.0]
1004 } else {
1005 [min_x, min_y, max_x, max_y]
1006 }
1007}
1008
1009fn render_soft_mask_alpha<B: RenderBackend>(
1015 backend: &mut B,
1016 surface: &B::Surface,
1017 sm: &SoftMask,
1018 page_transform: Matrix,
1019 image_decoder: Option<&dyn ImageDecoder>,
1020) -> Option<(Vec<u8>, u32, u32)> {
1021 use rpdfium_page::display::walk as walk_tree;
1022
1023 let (w, h) = backend.surface_dimensions(surface);
1024 if w == 0 || h == 0 {
1025 return None;
1026 }
1027
1028 let bg = match sm.backdrop_color {
1033 Some(ref bc) => {
1034 let r = bc.first().copied().unwrap_or(0.0);
1035 let g = bc.get(1).copied().unwrap_or(r);
1036 let b = bc.get(2).copied().unwrap_or(r);
1037 RgbaColor {
1038 r: (r.clamp(0.0, 1.0) * 255.0).round() as u8,
1039 g: (g.clamp(0.0, 1.0) * 255.0).round() as u8,
1040 b: (b.clamp(0.0, 1.0) * 255.0).round() as u8,
1041 a: 255,
1042 }
1043 }
1044 None => RgbaColor::TRANSPARENT,
1045 };
1046
1047 let mut mask_surface = backend.create_surface(w, h, &bg);
1049 {
1050 let mut sub_renderer =
1051 DisplayRenderer::new(backend, &mut mask_surface, page_transform, image_decoder);
1052 walk_tree(&sm.group, &mut sub_renderer);
1053 }
1054
1055 let pixels = backend.surface_pixels(&mask_surface);
1057
1058 let pixel_count = (w * h) as usize;
1060 let mut alpha = vec![0u8; pixel_count];
1061
1062 match sm.subtype {
1063 SoftMaskSubtype::Alpha => {
1064 for (i, a) in alpha.iter_mut().enumerate() {
1066 let idx = i * 4 + 3;
1067 if idx < pixels.len() {
1068 *a = pixels[idx];
1069 }
1070 }
1071 }
1072 SoftMaskSubtype::Luminosity => {
1073 for (i, a) in alpha.iter_mut().enumerate() {
1076 let base = i * 4;
1077 if base + 3 >= pixels.len() {
1078 break;
1079 }
1080 let pa = pixels[base + 3] as f32;
1081 if pa == 0.0 {
1082 *a = 0;
1083 continue;
1084 }
1085 let factor = 255.0 / pa;
1086 let r = (pixels[base] as f32 * factor).min(255.0);
1087 let g = (pixels[base + 1] as f32 * factor).min(255.0);
1088 let b = (pixels[base + 2] as f32 * factor).min(255.0);
1089 let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
1090 *a = (lum.round() as u32).min(255) as u8;
1091 }
1092 }
1093 }
1094
1095 if let Some(ref tf) = sm.transfer_function {
1097 use rpdfium_page::function::{TransferFunction, evaluate};
1098 match tf.as_ref() {
1099 TransferFunction::Identity => {}
1100 TransferFunction::Single(f) => {
1101 for a in alpha.iter_mut() {
1102 let input = *a as f32 / 255.0;
1103 let output = evaluate(f, &[input])
1104 .first()
1105 .copied()
1106 .unwrap_or(input)
1107 .clamp(0.0, 1.0);
1108 *a = (output * 255.0).round() as u8;
1109 }
1110 }
1111 TransferFunction::PerComponent { r, .. } => {
1112 for a in alpha.iter_mut() {
1114 let input = *a as f32 / 255.0;
1115 let output = evaluate(r.as_ref(), &[input])
1116 .first()
1117 .copied()
1118 .unwrap_or(input)
1119 .clamp(0.0, 1.0);
1120 *a = (output * 255.0).round() as u8;
1121 }
1122 }
1123 }
1124 }
1125
1126 Some((alpha, w, h))
1127}
1128
1129fn apply_image_mask(
1141 mut img: crate::image::DecodedImage,
1142 mask: Option<&rpdfium_page::display::ImageMask>,
1143 fill_color: Option<&Color>,
1144 decoder: Option<&dyn ImageDecoder>,
1145) -> crate::image::DecodedImage {
1146 use crate::image::DecodedImageFormat;
1147 use rpdfium_page::display::ImageMask;
1148
1149 let mask = match mask {
1150 Some(m) => m,
1151 None => return img,
1152 };
1153
1154 match mask {
1155 ImageMask::Stencil => {
1156 let rgba = fill_color
1157 .map(|c| RgbaColor::from_pdf_color(c, 1.0))
1158 .unwrap_or(RgbaColor::BLACK);
1159
1160 let w = img.width as usize;
1161 let h = img.height as usize;
1162 let pixel_count = w * h;
1163 let mut out = vec![0u8; pixel_count * 4];
1164
1165 match img.format {
1166 DecodedImageFormat::Gray8 => {
1167 for i in 0..pixel_count.min(img.data.len()) {
1169 let dst = i * 4;
1170 if img.data[i] != 0 {
1171 out[dst] = rgba.r;
1172 out[dst + 1] = rgba.g;
1173 out[dst + 2] = rgba.b;
1174 out[dst + 3] = rgba.a;
1175 }
1176 }
1178 }
1179 DecodedImageFormat::Rgb24 => {
1180 for i in 0..pixel_count {
1182 let src = i * 3;
1183 let dst = i * 4;
1184 if src + 2 < img.data.len() {
1185 let lum = img.data[src] as u16
1186 + img.data[src + 1] as u16
1187 + img.data[src + 2] as u16;
1188 if lum > 0 {
1189 out[dst] = rgba.r;
1190 out[dst + 1] = rgba.g;
1191 out[dst + 2] = rgba.b;
1192 out[dst + 3] = rgba.a;
1193 }
1194 }
1195 }
1196 }
1197 DecodedImageFormat::Rgba32 => {
1198 for i in 0..pixel_count {
1200 let src = i * 4;
1201 let dst = i * 4;
1202 if src + 3 < img.data.len() && img.data[src + 3] != 0 {
1203 out[dst] = rgba.r;
1204 out[dst + 1] = rgba.g;
1205 out[dst + 2] = rgba.b;
1206 out[dst + 3] = rgba.a;
1207 }
1208 }
1209 }
1210 }
1211
1212 img.data = out;
1213 img.format = DecodedImageFormat::Rgba32;
1214 img
1215 }
1216 ImageMask::ColorKey { ranges } => {
1217 let w = img.width as usize;
1219 let h = img.height as usize;
1220 let pixel_count = w * h;
1221
1222 match img.format {
1223 DecodedImageFormat::Gray8 => {
1224 let mut out = vec![0u8; pixel_count * 4];
1226 for i in 0..pixel_count.min(img.data.len()) {
1227 let g = img.data[i];
1228 let dst = i * 4;
1229 out[dst] = g;
1230 out[dst + 1] = g;
1231 out[dst + 2] = g;
1232 let masked = ranges
1233 .first()
1234 .is_some_and(|r| (g as u32) >= r[0] && (g as u32) <= r[1]);
1235 out[dst + 3] = if masked { 0 } else { 255 };
1236 }
1237 img.data = out;
1238 img.format = DecodedImageFormat::Rgba32;
1239 }
1240 DecodedImageFormat::Rgb24 => {
1241 let mut out = vec![0u8; pixel_count * 4];
1243 for i in 0..pixel_count {
1244 let src = i * 3;
1245 let dst = i * 4;
1246 if src + 2 >= img.data.len() {
1247 break;
1248 }
1249 let r = img.data[src];
1250 let g = img.data[src + 1];
1251 let b = img.data[src + 2];
1252 out[dst] = r;
1253 out[dst + 1] = g;
1254 out[dst + 2] = b;
1255
1256 let components = [r, g, b];
1257 let masked = components.iter().enumerate().all(|(ci, &val)| {
1258 ranges
1259 .get(ci)
1260 .is_some_and(|rng| (val as u32) >= rng[0] && (val as u32) <= rng[1])
1261 }) && !ranges.is_empty();
1262 out[dst + 3] = if masked { 0 } else { 255 };
1263 }
1264 img.data = out;
1265 img.format = DecodedImageFormat::Rgba32;
1266 }
1267 DecodedImageFormat::Rgba32 => {
1268 for i in 0..pixel_count {
1270 let base = i * 4;
1271 if base + 3 >= img.data.len() {
1272 break;
1273 }
1274 let components = [img.data[base], img.data[base + 1], img.data[base + 2]];
1275 let masked = components.iter().enumerate().all(|(ci, &val)| {
1276 ranges
1277 .get(ci)
1278 .is_some_and(|rng| (val as u32) >= rng[0] && (val as u32) <= rng[1])
1279 }) && !ranges.is_empty();
1280 if masked {
1281 img.data[base + 3] = 0;
1282 }
1283 }
1284 }
1285 }
1286 img
1287 }
1288 ImageMask::ExplicitMask { mask_object_id } => {
1289 let Some(dec) = decoder else {
1291 return img;
1292 };
1293 let mask_ref = ImageRef {
1294 object_id: *mask_object_id,
1295 };
1296 let mask_img = match dec.decode_image(&mask_ref, &rpdfium_core::Matrix::identity()) {
1297 Ok(m) => m,
1298 Err(_) => return img,
1299 };
1300
1301 apply_grayscale_mask_alpha(&mut img, &mask_img, None);
1302 img
1303 }
1304 ImageMask::SoftMask {
1305 smask_object_id,
1306 matte,
1307 } => {
1308 let Some(dec) = decoder else {
1310 return img;
1311 };
1312 let mask_ref = ImageRef {
1313 object_id: *smask_object_id,
1314 };
1315 let mask_img = match dec.decode_image(&mask_ref, &rpdfium_core::Matrix::identity()) {
1316 Ok(m) => m,
1317 Err(_) => return img,
1318 };
1319
1320 apply_grayscale_mask_alpha(&mut img, &mask_img, matte.as_deref());
1321 img
1322 }
1323 }
1324}
1325
1326fn apply_grayscale_mask_alpha(
1335 img: &mut crate::image::DecodedImage,
1336 mask: &crate::image::DecodedImage,
1337 matte: Option<&[f32]>,
1338) {
1339 use crate::image::DecodedImageFormat;
1340
1341 let w = img.width as usize;
1342 let h = img.height as usize;
1343 let pixel_count = w * h;
1344
1345 let original_components: usize = match img.format {
1348 DecodedImageFormat::Gray8 => 1,
1349 DecodedImageFormat::Rgb24 | DecodedImageFormat::Rgba32 => 3,
1350 };
1351 let validated_matte: Option<&[f32]> = matte.filter(|m| m.len() == original_components);
1352
1353 let mask_values: Vec<u8> = match mask.format {
1355 DecodedImageFormat::Gray8 => mask.data.clone(),
1356 DecodedImageFormat::Rgb24 => mask
1357 .data
1358 .chunks_exact(3)
1359 .map(|p| ((p[0] as u16 + p[1] as u16 + p[2] as u16) / 3) as u8)
1360 .collect(),
1361 DecodedImageFormat::Rgba32 => mask.data.chunks_exact(4).map(|p| p[0]).collect(),
1362 };
1363
1364 let scaled_mask: Vec<u8> = if mask.width == img.width && mask.height == img.height {
1366 mask_values
1367 } else {
1368 let mw = mask.width as usize;
1369 let mh = mask.height as usize;
1370 if mw == 0 || mh == 0 {
1371 return;
1372 }
1373 let mut scaled = vec![0u8; pixel_count];
1374 for y in 0..h {
1375 for x in 0..w {
1376 let mx = x * mw / w;
1377 let my = y * mh / h;
1378 let mi = my * mw + mx;
1379 scaled[y * w + x] = mask_values.get(mi).copied().unwrap_or(0);
1380 }
1381 }
1382 scaled
1383 };
1384
1385 match img.format {
1387 DecodedImageFormat::Gray8 => {
1388 let mut rgba = vec![0u8; pixel_count * 4];
1389 for i in 0..pixel_count.min(img.data.len()) {
1390 let g = img.data[i];
1391 let base = i * 4;
1392 rgba[base] = g;
1393 rgba[base + 1] = g;
1394 rgba[base + 2] = g;
1395 rgba[base + 3] = 255;
1396 }
1397 img.data = rgba;
1398 img.format = DecodedImageFormat::Rgba32;
1399 }
1400 DecodedImageFormat::Rgb24 => {
1401 let mut rgba = vec![0u8; pixel_count * 4];
1402 for i in 0..pixel_count {
1403 let src = i * 3;
1404 let dst = i * 4;
1405 if src + 2 < img.data.len() {
1406 rgba[dst] = img.data[src];
1407 rgba[dst + 1] = img.data[src + 1];
1408 rgba[dst + 2] = img.data[src + 2];
1409 rgba[dst + 3] = 255;
1410 }
1411 }
1412 img.data = rgba;
1413 img.format = DecodedImageFormat::Rgba32;
1414 }
1415 DecodedImageFormat::Rgba32 => {}
1416 }
1417
1418 for i in 0..pixel_count {
1420 let base = i * 4;
1421 if base + 3 >= img.data.len() {
1422 break;
1423 }
1424 let alpha = scaled_mask.get(i).copied().unwrap_or(0);
1425
1426 if let Some(m) = validated_matte {
1430 if alpha > 0 {
1431 let mask_i = alpha as i32;
1432 for c in 0..3 {
1433 let mc = m.get(c).copied().unwrap_or(0.0);
1434 let matte_int = (mc * 255.0).round() as i32;
1435 let pixel_val = img.data[base + c] as i32;
1436 let orig = (pixel_val - matte_int) * 255 / mask_i + matte_int;
1437 img.data[base + c] = orig.clamp(0, 255) as u8;
1438 }
1439 }
1440 }
1441
1442 img.data[base + 3] = alpha;
1443 }
1444}
1445
1446fn apply_transfer_to_image(
1448 img: &mut crate::image::DecodedImage,
1449 tf: &rpdfium_page::function::TransferFunction,
1450) {
1451 use crate::image::DecodedImageFormat;
1452 use rpdfium_page::function::{TransferFunction, evaluate};
1453
1454 if matches!(tf, TransferFunction::Identity) {
1455 return;
1456 }
1457
1458 match img.format {
1459 DecodedImageFormat::Gray8 => {
1460 for pixel in img.data.iter_mut() {
1461 let v = *pixel as f32 / 255.0;
1462 let result = match tf {
1463 TransferFunction::Identity => v,
1464 TransferFunction::Single(f) => evaluate(f, &[v]).first().copied().unwrap_or(v),
1465 TransferFunction::PerComponent { r, .. } => {
1466 evaluate(r.as_ref(), &[v]).first().copied().unwrap_or(v)
1467 }
1468 };
1469 *pixel = (result.clamp(0.0, 1.0) * 255.0) as u8;
1470 }
1471 }
1472 DecodedImageFormat::Rgb24 => {
1473 for chunk in img.data.chunks_exact_mut(3) {
1474 for (c, byte) in chunk.iter_mut().enumerate() {
1475 let v = *byte as f32 / 255.0;
1476 let result = match tf {
1477 TransferFunction::Identity => v,
1478 TransferFunction::Single(f) => {
1479 evaluate(f, &[v]).first().copied().unwrap_or(v)
1480 }
1481 TransferFunction::PerComponent { r, g, b, .. } => {
1482 let f = match c {
1483 0 => r.as_ref(),
1484 1 => g.as_ref(),
1485 _ => b.as_ref(),
1486 };
1487 evaluate(f, &[v]).first().copied().unwrap_or(v)
1488 }
1489 };
1490 *byte = (result.clamp(0.0, 1.0) * 255.0) as u8;
1491 }
1492 }
1493 }
1494 DecodedImageFormat::Rgba32 => {
1495 for chunk in img.data.chunks_exact_mut(4) {
1496 #[allow(clippy::needless_range_loop)]
1497 for c in 0..3 {
1498 let v = chunk[c] as f32 / 255.0;
1499 let result = match tf {
1500 TransferFunction::Identity => v,
1501 TransferFunction::Single(f) => {
1502 evaluate(f, &[v]).first().copied().unwrap_or(v)
1503 }
1504 TransferFunction::PerComponent { r, g, b, .. } => {
1505 let f = match c {
1506 0 => r.as_ref(),
1507 1 => g.as_ref(),
1508 _ => b.as_ref(),
1509 };
1510 evaluate(f, &[v]).first().copied().unwrap_or(v)
1511 }
1512 };
1513 chunk[c] = (result.clamp(0.0, 1.0) * 255.0) as u8;
1514 }
1515 }
1517 }
1518 }
1519}
1520
1521impl<B: RenderBackend> DisplayVisitor for DisplayRenderer<'_, B> {
1522 fn enter_group(
1523 &mut self,
1524 blend_mode: BlendMode,
1525 clip: Option<&ClipPath>,
1526 opacity: f32,
1527 isolated: bool,
1528 knockout: bool,
1529 soft_mask: &Option<Box<SoftMask>>,
1530 ) -> bool {
1531 let has_clip = clip.is_some_and(|c| !c.is_empty());
1532 let has_smask = soft_mask.is_some();
1533 let has_group = blend_mode != BlendMode::Normal || opacity < 1.0 || knockout || has_smask;
1534
1535 if has_clip {
1536 self.backend
1537 .push_clip(self.surface, clip.unwrap(), &self.page_transform);
1538 }
1539 if has_group {
1540 self.backend
1541 .push_group(self.surface, blend_mode, opacity, isolated, knockout);
1542 }
1543
1544 if let Some(sm) = soft_mask {
1546 let alpha = render_soft_mask_alpha(
1547 self.backend,
1548 self.surface,
1549 sm,
1550 self.page_transform,
1551 self.image_decoder,
1552 );
1553 if let Some((data, w, h)) = alpha {
1554 self.backend.set_group_mask(data, w, h);
1555 }
1556 }
1557
1558 let action = match (has_clip, has_group) {
1559 (false, false) => GroupAction::Nothing,
1560 (true, false) => GroupAction::ClipOnly,
1561 (false, true) => GroupAction::GroupOnly,
1562 (true, true) => GroupAction::ClipAndGroup,
1563 };
1564 self.group_stack.push(action);
1565 true }
1567
1568 fn leave_group(&mut self) {
1569 while self.text_clip_depth > 0 {
1571 self.backend.pop_clip(self.surface);
1572 self.text_clip_depth -= 1;
1573 }
1574
1575 if let Some(action) = self.group_stack.pop() {
1576 match action {
1577 GroupAction::Nothing => {}
1578 GroupAction::ClipOnly => {
1579 self.backend.pop_clip(self.surface);
1580 }
1581 GroupAction::GroupOnly => {
1582 self.backend.pop_group(self.surface);
1583 }
1584 GroupAction::ClipAndGroup => {
1585 self.backend.pop_group(self.surface);
1586 self.backend.pop_clip(self.surface);
1587 }
1588 }
1589 }
1590 }
1591
1592 fn visit_path(
1593 &mut self,
1594 ops: &[PathOp],
1595 style: &PathStyle,
1596 matrix: &Matrix,
1597 fill_color: Option<&Color>,
1598 stroke_color: Option<&Color>,
1599 fill_color_space: Option<&ColorSpaceFamily>,
1600 stroke_color_space: Option<&ColorSpaceFamily>,
1601 transfer_function: Option<&rpdfium_page::function::TransferFunction>,
1602 overprint: bool,
1603 _overprint_mode: u32,
1604 ) {
1605 let transform = self.page_transform.pre_concat(matrix);
1607
1608 self.backend.set_antialiasing(self.path_antialiasing);
1610
1611 let fill_overprint_darken = overprint && is_cmyk_family(fill_color_space);
1614 let stroke_overprint_darken = overprint && is_cmyk_family(stroke_color_space);
1615
1616 if let (Some(fill_rule), Some(color)) = (style.fill, fill_color) {
1618 if fill_overprint_darken {
1619 self.backend
1620 .push_group(self.surface, BlendMode::Darken, 1.0, false, false);
1621 }
1622 let mut rgba = if let Some(ref scheme) = self.forced_color_scheme {
1623 scheme.text_color
1624 } else {
1625 RgbaColor::from_pdf_color(color, 1.0)
1626 };
1627 if let Some(tf) = transfer_function {
1628 apply_transfer(&mut rgba, tf);
1629 }
1630 self.backend
1631 .fill_path(self.surface, ops, fill_rule, &rgba, &transform);
1632 if fill_overprint_darken {
1633 self.backend.pop_group(self.surface);
1634 }
1635 }
1636 if style.stroke {
1638 if let Some(color) = stroke_color {
1639 if stroke_overprint_darken {
1640 self.backend
1641 .push_group(self.surface, BlendMode::Darken, 1.0, false, false);
1642 }
1643 let mut rgba = if let Some(ref scheme) = self.forced_color_scheme {
1644 scheme.text_color
1645 } else {
1646 RgbaColor::from_pdf_color(color, 1.0)
1647 };
1648 if let Some(tf) = transfer_function {
1649 apply_transfer(&mut rgba, tf);
1650 }
1651 let stroke_style = StrokeStyle::from_path_style(style);
1652 self.backend
1653 .stroke_path(self.surface, ops, &stroke_style, &rgba, &transform);
1654 if stroke_overprint_darken {
1655 self.backend.pop_group(self.surface);
1656 }
1657 }
1658 }
1659 }
1660
1661 fn visit_image(
1662 &mut self,
1663 image_ref: &ImageRef,
1664 matrix: &Matrix,
1665 mask: Option<&rpdfium_page::display::ImageMask>,
1666 fill_color: Option<&Color>,
1667 transfer_function: Option<&rpdfium_page::function::TransferFunction>,
1668 ) {
1669 if let Some(decoder) = self.image_decoder {
1670 let transform = self.page_transform.pre_concat(matrix);
1671 match decoder.decode_image(image_ref, &transform) {
1672 Ok(img) => {
1673 let mut masked = apply_image_mask(img, mask, fill_color, self.image_decoder);
1674 if let Some(tf) = transfer_function {
1675 apply_transfer_to_image(&mut masked, tf);
1676 }
1677 let img_transform = image_unit_transform(&masked, &transform);
1682 let interpolate = self.image_antialiasing
1683 && should_interpolate_image(&masked, &img_transform);
1684 self.backend
1685 .draw_image(self.surface, &masked, &img_transform, interpolate);
1686 }
1687 Err(e) => tracing::warn!(
1688 object_id = %image_ref.object_id,
1689 error = %e,
1690 "failed to decode image XObject"
1691 ),
1692 }
1693 }
1694 }
1695
1696 fn visit_inline_image(
1697 &mut self,
1698 properties: &HashMap<Name, Operand>,
1699 data: &[u8],
1700 matrix: &Matrix,
1701 ) {
1702 if let Some(decoder) = self.image_decoder {
1703 let transform = self.page_transform.pre_concat(matrix);
1704 match decoder.decode_inline_image(properties, data, &transform) {
1705 Ok(img) => {
1706 let img_transform = image_unit_transform(&img, &transform);
1707 let interpolate = should_interpolate_image(&img, &img_transform);
1708 self.backend
1709 .draw_image(self.surface, &img, &img_transform, interpolate);
1710 }
1711 Err(e) => tracing::warn!(
1712 data_len = data.len(),
1713 error = %e,
1714 "failed to decode inline image"
1715 ),
1716 }
1717 }
1718 }
1719
1720 fn visit_pattern_fill(
1721 &mut self,
1722 path_ops: &[PathOp],
1723 fill_rule: FillRule,
1724 pattern: &TilingPattern,
1725 pattern_tree: &DisplayTree,
1726 _fill_color: Option<&Color>,
1727 matrix: &Matrix,
1728 ) {
1729 render_pattern_fill(
1730 self.backend,
1731 self.surface,
1732 path_ops,
1733 fill_rule,
1734 pattern,
1735 pattern_tree,
1736 &self.page_transform,
1737 matrix,
1738 self.image_decoder,
1739 );
1740 }
1741
1742 fn visit_shading_fill(&mut self, shading: &ShadingDict, matrix: &Matrix) {
1743 let transform = self.page_transform.pre_concat(matrix);
1744 crate::render_shading::render_shading(
1747 self.backend,
1748 self.surface,
1749 shading,
1750 &transform,
1751 true,
1752 );
1753 }
1754
1755 fn visit_text(&mut self, runs: &[TextRun]) {
1756 for run in runs {
1757 self.render_text_run(run);
1758 }
1759
1760 if !self.text_clip_ops.is_empty() {
1763 let ops = std::mem::take(&mut self.text_clip_ops);
1764 let mut clip = ClipPath::new();
1765 clip.push(ops, FillRule::NonZero);
1768 self.backend
1769 .push_clip(self.surface, &clip, &Matrix::identity());
1770 self.text_clip_depth += 1;
1771 }
1772 }
1773}
1774
1775#[cfg(test)]
1776mod tests {
1777 use super::*;
1778 use rpdfium_graphics::{Bitmap, BitmapFormat, FillRule};
1779 use rpdfium_page::display::{DisplayNode, DisplayTree, walk};
1780 use std::sync::Mutex;
1781
1782 use crate::image::DecodedImage;
1783
1784 struct MockSurface;
1786
1787 struct MockBackend {
1789 log: Mutex<Vec<String>>,
1790 }
1791
1792 impl MockBackend {
1793 fn new() -> Self {
1794 Self {
1795 log: Mutex::new(Vec::new()),
1796 }
1797 }
1798
1799 fn log(&self) -> Vec<String> {
1800 self.log.lock().unwrap().clone()
1801 }
1802 }
1803
1804 impl RenderBackend for MockBackend {
1805 type Surface = MockSurface;
1806
1807 fn create_surface(&self, _w: u32, _h: u32, _bg: &RgbaColor) -> Self::Surface {
1808 MockSurface
1809 }
1810
1811 fn fill_path(
1812 &mut self,
1813 _surface: &mut Self::Surface,
1814 _ops: &[PathOp],
1815 _fill_rule: FillRule,
1816 _color: &RgbaColor,
1817 _transform: &Matrix,
1818 ) {
1819 self.log.lock().unwrap().push("fill_path".to_string());
1820 }
1821
1822 fn stroke_path(
1823 &mut self,
1824 _surface: &mut Self::Surface,
1825 _ops: &[PathOp],
1826 _style: &StrokeStyle,
1827 _color: &RgbaColor,
1828 _transform: &Matrix,
1829 ) {
1830 self.log.lock().unwrap().push("stroke_path".to_string());
1831 }
1832
1833 fn draw_image(
1834 &mut self,
1835 _surface: &mut Self::Surface,
1836 _image: &DecodedImage,
1837 _transform: &Matrix,
1838 _interpolate: bool,
1839 ) {
1840 self.log.lock().unwrap().push("draw_image".to_string());
1841 }
1842
1843 fn push_clip(
1844 &mut self,
1845 _surface: &mut Self::Surface,
1846 _clip: &ClipPath,
1847 _transform: &Matrix,
1848 ) {
1849 self.log.lock().unwrap().push("push_clip".to_string());
1850 }
1851
1852 fn pop_clip(&mut self, _surface: &mut Self::Surface) {
1853 self.log.lock().unwrap().push("pop_clip".to_string());
1854 }
1855
1856 fn push_group(
1857 &mut self,
1858 _surface: &mut Self::Surface,
1859 _blend_mode: BlendMode,
1860 _opacity: f32,
1861 _isolated: bool,
1862 _knockout: bool,
1863 ) {
1864 self.log.lock().unwrap().push("push_group".to_string());
1865 }
1866
1867 fn pop_group(&mut self, _surface: &mut Self::Surface) {
1868 self.log.lock().unwrap().push("pop_group".to_string());
1869 }
1870
1871 fn surface_dimensions(&self, _surface: &Self::Surface) -> (u32, u32) {
1872 (100, 100)
1873 }
1874
1875 fn composite_over(&mut self, _dst: &mut Self::Surface, _src: &Self::Surface) {}
1876
1877 fn surface_pixels(&self, _surface: &Self::Surface) -> Vec<u8> {
1878 vec![0u8; 100 * 100 * 4]
1879 }
1880
1881 fn finish(self, _surface: Self::Surface) -> Bitmap {
1882 Bitmap::new(1, 1, BitmapFormat::Rgba32)
1883 }
1884 }
1885
1886 fn run_mock_renderer(tree: &DisplayTree) -> Vec<String> {
1888 let mut backend = MockBackend::new();
1889 let mut surface = backend.create_surface(100, 100, &RgbaColor::WHITE);
1890 {
1891 let mut renderer =
1892 DisplayRenderer::new(&mut backend, &mut surface, Matrix::identity(), None);
1893 walk(tree, &mut renderer);
1894 }
1895 backend.log()
1896 }
1897
1898 #[test]
1899 fn test_renderer_empty_tree() {
1900 let tree = DisplayTree {
1901 root: DisplayNode::Group {
1902 blend_mode: BlendMode::Normal,
1903 clip: None,
1904 opacity: 1.0,
1905 isolated: false,
1906 knockout: false,
1907 soft_mask: None,
1908 children: Vec::new(),
1909 },
1910 };
1911 assert!(run_mock_renderer(&tree).is_empty());
1913 }
1914
1915 #[test]
1916 fn test_renderer_fill_path() {
1917 let tree = DisplayTree {
1918 root: DisplayNode::Group {
1919 blend_mode: BlendMode::Normal,
1920 clip: None,
1921 opacity: 1.0,
1922 isolated: false,
1923 knockout: false,
1924 soft_mask: None,
1925 children: vec![DisplayNode::Path {
1926 ops: vec![
1927 PathOp::MoveTo { x: 0.0, y: 0.0 },
1928 PathOp::LineTo { x: 100.0, y: 0.0 },
1929 PathOp::LineTo { x: 100.0, y: 100.0 },
1930 PathOp::Close,
1931 ],
1932 style: PathStyle {
1933 fill: Some(FillRule::NonZero),
1934 ..PathStyle::default()
1935 },
1936 matrix: Matrix::identity(),
1937 fill_color: Some(Color::rgb(1.0, 0.0, 0.0)),
1938 stroke_color: None,
1939 fill_color_space: None,
1940 stroke_color_space: None,
1941 transfer_function: None,
1942 overprint: false,
1943 overprint_mode: 0,
1944 }],
1945 },
1946 };
1947 assert_eq!(run_mock_renderer(&tree), vec!["fill_path"]);
1948 }
1949
1950 #[test]
1951 fn test_renderer_stroke_path() {
1952 let tree = DisplayTree {
1953 root: DisplayNode::Group {
1954 blend_mode: BlendMode::Normal,
1955 clip: None,
1956 opacity: 1.0,
1957 isolated: false,
1958 knockout: false,
1959 soft_mask: None,
1960 children: vec![DisplayNode::Path {
1961 ops: vec![
1962 PathOp::MoveTo { x: 0.0, y: 0.0 },
1963 PathOp::LineTo { x: 100.0, y: 0.0 },
1964 ],
1965 style: PathStyle {
1966 stroke: true,
1967 ..PathStyle::default()
1968 },
1969 matrix: Matrix::identity(),
1970 fill_color: None,
1971 stroke_color: Some(Color::gray(0.0)),
1972 fill_color_space: None,
1973 stroke_color_space: None,
1974 transfer_function: None,
1975 overprint: false,
1976 overprint_mode: 0,
1977 }],
1978 },
1979 };
1980 assert_eq!(run_mock_renderer(&tree), vec!["stroke_path"]);
1981 }
1982
1983 #[test]
1984 fn test_renderer_fill_and_stroke() {
1985 let tree = DisplayTree {
1986 root: DisplayNode::Group {
1987 blend_mode: BlendMode::Normal,
1988 clip: None,
1989 opacity: 1.0,
1990 isolated: false,
1991 knockout: false,
1992 soft_mask: None,
1993 children: vec![DisplayNode::Path {
1994 ops: vec![
1995 PathOp::MoveTo { x: 0.0, y: 0.0 },
1996 PathOp::LineTo { x: 50.0, y: 0.0 },
1997 PathOp::LineTo { x: 50.0, y: 50.0 },
1998 PathOp::Close,
1999 ],
2000 style: PathStyle {
2001 fill: Some(FillRule::EvenOdd),
2002 stroke: true,
2003 ..PathStyle::default()
2004 },
2005 matrix: Matrix::identity(),
2006 fill_color: Some(Color::rgb(0.0, 1.0, 0.0)),
2007 stroke_color: Some(Color::gray(0.0)),
2008 fill_color_space: None,
2009 stroke_color_space: None,
2010 transfer_function: None,
2011 overprint: false,
2012 overprint_mode: 0,
2013 }],
2014 },
2015 };
2016 assert_eq!(run_mock_renderer(&tree), vec!["fill_path", "stroke_path"]);
2017 }
2018
2019 #[test]
2020 fn test_renderer_group_with_clip() {
2021 let mut clip = ClipPath::new();
2022 clip.push(
2023 vec![
2024 PathOp::MoveTo { x: 0.0, y: 0.0 },
2025 PathOp::LineTo { x: 50.0, y: 50.0 },
2026 PathOp::Close,
2027 ],
2028 FillRule::NonZero,
2029 );
2030 let tree = DisplayTree {
2031 root: DisplayNode::Group {
2032 blend_mode: BlendMode::Normal,
2033 clip: Some(clip),
2034 opacity: 1.0,
2035 isolated: false,
2036 knockout: false,
2037 soft_mask: None,
2038 children: Vec::new(),
2039 },
2040 };
2041 assert_eq!(run_mock_renderer(&tree), vec!["push_clip", "pop_clip"]);
2042 }
2043
2044 #[test]
2045 fn test_renderer_group_with_blend_mode() {
2046 let tree = DisplayTree {
2047 root: DisplayNode::Group {
2048 blend_mode: BlendMode::Multiply,
2049 clip: None,
2050 opacity: 0.5,
2051 isolated: false,
2052 knockout: false,
2053 soft_mask: None,
2054 children: Vec::new(),
2055 },
2056 };
2057 assert_eq!(run_mock_renderer(&tree), vec!["push_group", "pop_group"]);
2058 }
2059
2060 #[test]
2061 fn test_renderer_group_with_clip_and_blend() {
2062 let mut clip = ClipPath::new();
2063 clip.push(
2064 vec![PathOp::MoveTo { x: 0.0, y: 0.0 }, PathOp::Close],
2065 FillRule::NonZero,
2066 );
2067 let tree = DisplayTree {
2068 root: DisplayNode::Group {
2069 blend_mode: BlendMode::Screen,
2070 clip: Some(clip),
2071 opacity: 0.8,
2072 isolated: false,
2073 knockout: false,
2074 soft_mask: None,
2075 children: Vec::new(),
2076 },
2077 };
2078 assert_eq!(
2080 run_mock_renderer(&tree),
2081 vec!["push_clip", "push_group", "pop_group", "pop_clip"]
2082 );
2083 }
2084
2085 #[test]
2086 fn test_renderer_text_no_font_is_noop() {
2087 let tree = DisplayTree {
2088 root: DisplayNode::Group {
2089 blend_mode: BlendMode::Normal,
2090 clip: None,
2091 opacity: 1.0,
2092 isolated: false,
2093 knockout: false,
2094 soft_mask: None,
2095 children: vec![DisplayNode::Text { runs: vec![] }],
2096 },
2097 };
2098 assert!(run_mock_renderer(&tree).is_empty());
2100 }
2101
2102 #[test]
2103 fn test_renderer_type3_text_fills_glyphs() {
2104 use rpdfium_font::font_type::PdfFontType;
2105 use rpdfium_font::resolved::ResolvedFont;
2106 use rpdfium_font::type3_font::{Type3Encoding, Type3Font};
2107 use std::sync::Arc;
2108
2109 let mut char_procs = std::collections::HashMap::new();
2110 char_procs.insert(Name::from("a"), rpdfium_core::ObjectId::new(10, 0));
2111
2112 let type3 = Type3Font {
2113 font_matrix: [0.001, 0.0, 0.0, 0.001, 0.0, 0.0],
2114 char_procs,
2115 encoding: Type3Encoding::Differences(vec![(65, Name::from("a"))]),
2116 first_char: 65,
2117 last_char: 65,
2118 widths: vec![600.0],
2119 };
2120
2121 let font = Arc::new(ResolvedFont {
2122 font_type: PdfFontType::Type3,
2123 base_font_name: "TestType3".to_string(),
2124 widths: vec![600],
2125 default_width: 600,
2126 first_char: 65,
2127 ascent: 800,
2128 descent: -200,
2129 italic_angle: 0.0,
2130 is_embedded: false,
2131 to_unicode: None,
2132 encoding: rpdfium_font::fx_codepage::FontEncoding::Standard,
2133 font_data: None,
2134 cid_to_gid: None,
2135 wmode: 0,
2136 vertical_metrics: None,
2137 cid_cmap: None,
2138 type3: Some(type3),
2139 weight: None,
2140 flags: None,
2141 cid_coding: rpdfium_font::cid_font::CidCoding::Unknown,
2142 ttc_face_index: 0,
2143 glyph_outline_cache: std::sync::OnceLock::new(),
2144 });
2145
2146 let tree = DisplayTree {
2147 root: DisplayNode::Group {
2148 blend_mode: BlendMode::Normal,
2149 clip: None,
2150 opacity: 1.0,
2151 isolated: false,
2152 knockout: false,
2153 soft_mask: None,
2154 children: vec![DisplayNode::Text {
2155 runs: vec![TextRun {
2156 font_name: Name::from("F1"),
2157 font_size: 12.0,
2158 matrix: Matrix::identity(),
2159 text: vec![65], positions: vec![7.2],
2161 resolved_font: Some(font),
2162 rendering_mode: TextRenderingMode::Fill,
2163 rise: 0.0,
2164 fill_color: Some(Color::gray(0.0)),
2165 stroke_color: None,
2166 actual_text: None,
2167 type3_glyph_ops: None,
2168 actual_text_id: None,
2169 is_vertical: false,
2170 vert_origins: vec![],
2171 }],
2172 }],
2173 },
2174 };
2175 assert_eq!(run_mock_renderer(&tree), vec!["fill_path"]);
2177 }
2178
2179 #[test]
2180 fn test_renderer_type3_no_type3_data_is_noop() {
2181 use rpdfium_font::font_type::PdfFontType;
2182 use rpdfium_font::resolved::ResolvedFont;
2183 use std::sync::Arc;
2184
2185 let font = Arc::new(ResolvedFont {
2187 font_type: PdfFontType::Type3,
2188 base_font_name: "TestType3".to_string(),
2189 widths: vec![],
2190 default_width: 600,
2191 first_char: 0,
2192 ascent: 800,
2193 descent: -200,
2194 italic_angle: 0.0,
2195 is_embedded: false,
2196 to_unicode: None,
2197 encoding: rpdfium_font::fx_codepage::FontEncoding::Standard,
2198 font_data: None,
2199 cid_to_gid: None,
2200 wmode: 0,
2201 vertical_metrics: None,
2202 cid_cmap: None,
2203 type3: None, weight: None,
2205 flags: None,
2206 cid_coding: rpdfium_font::cid_font::CidCoding::Unknown,
2207 ttc_face_index: 0,
2208 glyph_outline_cache: std::sync::OnceLock::new(),
2209 });
2210
2211 let tree = DisplayTree {
2212 root: DisplayNode::Group {
2213 blend_mode: BlendMode::Normal,
2214 clip: None,
2215 opacity: 1.0,
2216 isolated: false,
2217 knockout: false,
2218 soft_mask: None,
2219 children: vec![DisplayNode::Text {
2220 runs: vec![TextRun {
2221 font_name: Name::from("F1"),
2222 font_size: 12.0,
2223 matrix: Matrix::identity(),
2224 text: vec![65],
2225 positions: vec![7.2],
2226 resolved_font: Some(font),
2227 rendering_mode: TextRenderingMode::Fill,
2228 rise: 0.0,
2229 fill_color: Some(Color::gray(0.0)),
2230 stroke_color: None,
2231 actual_text: None,
2232 type3_glyph_ops: None,
2233 actual_text_id: None,
2234 is_vertical: false,
2235 vert_origins: vec![],
2236 }],
2237 }],
2238 },
2239 };
2240 assert!(run_mock_renderer(&tree).is_empty());
2242 }
2243
2244 #[test]
2245 fn test_renderer_image_without_decoder_is_noop() {
2246 let tree = DisplayTree {
2247 root: DisplayNode::Group {
2248 blend_mode: BlendMode::Normal,
2249 clip: None,
2250 opacity: 1.0,
2251 isolated: false,
2252 knockout: false,
2253 soft_mask: None,
2254 children: vec![DisplayNode::Image {
2255 image_ref: ImageRef {
2256 object_id: rpdfium_core::ObjectId::new(1, 0),
2257 },
2258 matrix: Matrix::identity(),
2259 mask: None,
2260 fill_color: None,
2261 transfer_function: None,
2262 }],
2263 },
2264 };
2265 assert!(run_mock_renderer(&tree).is_empty());
2267 }
2268
2269 #[test]
2270 fn test_renderer_group_with_smask_pushes_group() {
2271 use rpdfium_page::display::SoftMaskSubtype;
2272 let mask_tree = DisplayTree {
2274 root: DisplayNode::Group {
2275 blend_mode: BlendMode::Normal,
2276 clip: None,
2277 opacity: 1.0,
2278 isolated: true,
2279 knockout: false,
2280 soft_mask: None,
2281 children: vec![DisplayNode::Path {
2282 ops: vec![
2283 PathOp::MoveTo { x: 0.0, y: 0.0 },
2284 PathOp::LineTo { x: 50.0, y: 0.0 },
2285 PathOp::LineTo { x: 50.0, y: 50.0 },
2286 PathOp::Close,
2287 ],
2288 style: PathStyle {
2289 fill: Some(FillRule::NonZero),
2290 ..PathStyle::default()
2291 },
2292 matrix: Matrix::identity(),
2293 fill_color: Some(Color::gray(0.5)),
2294 stroke_color: None,
2295 fill_color_space: None,
2296 stroke_color_space: None,
2297 transfer_function: None,
2298 overprint: false,
2299 overprint_mode: 0,
2300 }],
2301 },
2302 };
2303 let smask = SoftMask {
2304 subtype: SoftMaskSubtype::Alpha,
2305 group: mask_tree,
2306 backdrop_color: None,
2307 transfer_function: None,
2308 };
2309 let tree = DisplayTree {
2310 root: DisplayNode::Group {
2311 blend_mode: BlendMode::Normal,
2312 clip: None,
2313 opacity: 1.0,
2314 isolated: true,
2315 knockout: false,
2316 soft_mask: Some(Box::new(smask)),
2317 children: vec![DisplayNode::Path {
2318 ops: vec![
2319 PathOp::MoveTo { x: 0.0, y: 0.0 },
2320 PathOp::LineTo { x: 100.0, y: 100.0 },
2321 PathOp::Close,
2322 ],
2323 style: PathStyle {
2324 fill: Some(FillRule::NonZero),
2325 ..PathStyle::default()
2326 },
2327 matrix: Matrix::identity(),
2328 fill_color: Some(Color::rgb(1.0, 0.0, 0.0)),
2329 stroke_color: None,
2330 fill_color_space: None,
2331 stroke_color_space: None,
2332 transfer_function: None,
2333 overprint: false,
2334 overprint_mode: 0,
2335 }],
2336 },
2337 };
2338 let log = run_mock_renderer(&tree);
2339 assert!(log.contains(&"push_group".to_string()));
2341 assert!(log.contains(&"pop_group".to_string()));
2342 assert!(log.contains(&"fill_path".to_string()));
2343 }
2344
2345 #[test]
2346 fn test_renderer_group_without_smask_unchanged() {
2347 let tree = DisplayTree {
2349 root: DisplayNode::Group {
2350 blend_mode: BlendMode::Normal,
2351 clip: None,
2352 opacity: 1.0,
2353 isolated: false,
2354 knockout: false,
2355 soft_mask: None,
2356 children: vec![DisplayNode::Path {
2357 ops: vec![
2358 PathOp::MoveTo { x: 0.0, y: 0.0 },
2359 PathOp::LineTo { x: 50.0, y: 0.0 },
2360 PathOp::Close,
2361 ],
2362 style: PathStyle {
2363 fill: Some(FillRule::NonZero),
2364 ..PathStyle::default()
2365 },
2366 matrix: Matrix::identity(),
2367 fill_color: Some(Color::rgb(0.0, 1.0, 0.0)),
2368 stroke_color: None,
2369 fill_color_space: None,
2370 stroke_color_space: None,
2371 transfer_function: None,
2372 overprint: false,
2373 overprint_mode: 0,
2374 }],
2375 },
2376 };
2377 let log = run_mock_renderer(&tree);
2378 assert_eq!(log, vec!["fill_path"]);
2380 }
2381
2382 #[test]
2383 fn test_renderer_with_fallback_fonts_builder() {
2384 use rpdfium_font::resolved::ResolvedFont;
2385 use std::sync::Arc;
2386
2387 let fallback = Arc::new(ResolvedFont {
2388 font_type: rpdfium_font::font_type::PdfFontType::TrueType,
2389 base_font_name: "FallbackFont".to_string(),
2390 widths: vec![],
2391 default_width: 500,
2392 first_char: 0,
2393 ascent: 800,
2394 descent: -200,
2395 italic_angle: 0.0,
2396 is_embedded: false,
2397 to_unicode: None,
2398 encoding: rpdfium_font::fx_codepage::FontEncoding::Standard,
2399 font_data: None,
2400 cid_to_gid: None,
2401 wmode: 0,
2402 vertical_metrics: None,
2403 cid_cmap: None,
2404 type3: None,
2405 weight: None,
2406 flags: None,
2407 cid_coding: rpdfium_font::cid_font::CidCoding::Unknown,
2408 ttc_face_index: 0,
2409 glyph_outline_cache: std::sync::OnceLock::new(),
2410 });
2411
2412 let mut backend = MockBackend::new();
2413 let mut surface = backend.create_surface(100, 100, &RgbaColor::WHITE);
2414 let renderer = DisplayRenderer::new(&mut backend, &mut surface, Matrix::identity(), None)
2415 .with_fallback_fonts(vec![fallback]);
2416 assert_eq!(renderer.fallback_fonts.len(), 1);
2417 }
2418
2419 #[test]
2420 fn test_renderer_find_fallback_glyph_no_fallbacks() {
2421 let mut backend = MockBackend::new();
2422 let mut surface = backend.create_surface(100, 100, &RgbaColor::WHITE);
2423 let renderer = DisplayRenderer::new(&mut backend, &mut surface, Matrix::identity(), None);
2424 assert!(renderer.find_fallback_glyph(65, 12.0).is_none());
2426 }
2427
2428 #[test]
2429 fn test_renderer_find_fallback_glyph_no_font_data() {
2430 use rpdfium_font::resolved::ResolvedFont;
2431 use std::sync::Arc;
2432
2433 let fallback = Arc::new(ResolvedFont {
2435 font_type: rpdfium_font::font_type::PdfFontType::TrueType,
2436 base_font_name: "NoData".to_string(),
2437 widths: vec![],
2438 default_width: 500,
2439 first_char: 0,
2440 ascent: 800,
2441 descent: -200,
2442 italic_angle: 0.0,
2443 is_embedded: false,
2444 to_unicode: None,
2445 encoding: rpdfium_font::fx_codepage::FontEncoding::Standard,
2446 font_data: None,
2447 cid_to_gid: None,
2448 wmode: 0,
2449 vertical_metrics: None,
2450 cid_cmap: None,
2451 type3: None,
2452 weight: None,
2453 flags: None,
2454 cid_coding: rpdfium_font::cid_font::CidCoding::Unknown,
2455 ttc_face_index: 0,
2456 glyph_outline_cache: std::sync::OnceLock::new(),
2457 });
2458
2459 let mut backend = MockBackend::new();
2460 let mut surface = backend.create_surface(100, 100, &RgbaColor::WHITE);
2461 let renderer = DisplayRenderer::new(&mut backend, &mut surface, Matrix::identity(), None)
2462 .with_fallback_fonts(vec![fallback]);
2463 assert!(renderer.find_fallback_glyph(65, 12.0).is_none());
2465 }
2466
2467 #[test]
2468 fn test_renderer_text_missing_glyph_no_fallback_skips() {
2469 use rpdfium_font::resolved::ResolvedFont;
2470 use std::sync::Arc;
2471
2472 let font = Arc::new(ResolvedFont {
2474 font_type: rpdfium_font::font_type::PdfFontType::TrueType,
2475 base_font_name: "NoGlyphs".to_string(),
2476 widths: vec![500],
2477 default_width: 500,
2478 first_char: 65,
2479 ascent: 800,
2480 descent: -200,
2481 italic_angle: 0.0,
2482 is_embedded: false,
2483 to_unicode: None,
2484 encoding: rpdfium_font::fx_codepage::FontEncoding::Standard,
2485 font_data: None,
2486 cid_to_gid: None,
2487 wmode: 0,
2488 vertical_metrics: None,
2489 cid_cmap: None,
2490 type3: None,
2491 weight: None,
2492 flags: None,
2493 cid_coding: rpdfium_font::cid_font::CidCoding::Unknown,
2494 ttc_face_index: 0,
2495 glyph_outline_cache: std::sync::OnceLock::new(),
2496 });
2497
2498 let tree = DisplayTree {
2499 root: DisplayNode::Group {
2500 blend_mode: BlendMode::Normal,
2501 clip: None,
2502 opacity: 1.0,
2503 isolated: false,
2504 knockout: false,
2505 soft_mask: None,
2506 children: vec![DisplayNode::Text {
2507 runs: vec![TextRun {
2508 font_name: Name::from("F1"),
2509 font_size: 12.0,
2510 matrix: Matrix::identity(),
2511 text: vec![65],
2512 positions: vec![7.2],
2513 resolved_font: Some(font),
2514 rendering_mode: TextRenderingMode::Fill,
2515 rise: 0.0,
2516 fill_color: Some(Color::gray(0.0)),
2517 stroke_color: None,
2518 actual_text: None,
2519 type3_glyph_ops: None,
2520 actual_text_id: None,
2521 is_vertical: false,
2522 vert_origins: vec![],
2523 }],
2524 }],
2525 },
2526 };
2527 assert!(run_mock_renderer(&tree).is_empty());
2529 }
2530
2531 #[test]
2532 fn test_renderer_luminosity_smask_pushes_group() {
2533 use rpdfium_page::display::SoftMaskSubtype;
2534 let mask_tree = DisplayTree {
2535 root: DisplayNode::Group {
2536 blend_mode: BlendMode::Normal,
2537 clip: None,
2538 opacity: 1.0,
2539 isolated: true,
2540 knockout: false,
2541 soft_mask: None,
2542 children: vec![DisplayNode::Path {
2543 ops: vec![
2544 PathOp::MoveTo { x: 0.0, y: 0.0 },
2545 PathOp::LineTo { x: 100.0, y: 0.0 },
2546 PathOp::LineTo { x: 100.0, y: 100.0 },
2547 PathOp::LineTo { x: 0.0, y: 100.0 },
2548 PathOp::Close,
2549 ],
2550 style: PathStyle {
2551 fill: Some(FillRule::NonZero),
2552 ..PathStyle::default()
2553 },
2554 matrix: Matrix::identity(),
2555 fill_color: Some(Color::rgb(1.0, 1.0, 1.0)),
2556 stroke_color: None,
2557 fill_color_space: None,
2558 stroke_color_space: None,
2559 transfer_function: None,
2560 overprint: false,
2561 overprint_mode: 0,
2562 }],
2563 },
2564 };
2565 let smask = SoftMask {
2566 subtype: SoftMaskSubtype::Luminosity,
2567 group: mask_tree,
2568 backdrop_color: None,
2569 transfer_function: None,
2570 };
2571 let tree = DisplayTree {
2572 root: DisplayNode::Group {
2573 blend_mode: BlendMode::Normal,
2574 clip: None,
2575 opacity: 1.0,
2576 isolated: true,
2577 knockout: false,
2578 soft_mask: Some(Box::new(smask)),
2579 children: Vec::new(),
2580 },
2581 };
2582 let log = run_mock_renderer(&tree);
2583 assert!(log.contains(&"push_group".to_string()));
2584 assert!(log.contains(&"pop_group".to_string()));
2585 }
2586
2587 fn make_type3_font() -> Arc<rpdfium_font::resolved::ResolvedFont> {
2591 use rpdfium_font::font_type::PdfFontType;
2592 use rpdfium_font::resolved::ResolvedFont;
2593 use rpdfium_font::type3_font::{Type3Encoding, Type3Font};
2594
2595 let mut char_procs = std::collections::HashMap::new();
2596 char_procs.insert(Name::from("a"), rpdfium_core::ObjectId::new(10, 0));
2597
2598 let type3 = Type3Font {
2599 font_matrix: [0.001, 0.0, 0.0, 0.001, 0.0, 0.0],
2600 char_procs,
2601 encoding: Type3Encoding::Differences(vec![(65, Name::from("a"))]),
2602 first_char: 65,
2603 last_char: 65,
2604 widths: vec![600.0],
2605 };
2606
2607 Arc::new(ResolvedFont {
2608 font_type: PdfFontType::Type3,
2609 base_font_name: "TestType3".to_string(),
2610 widths: vec![600],
2611 default_width: 600,
2612 first_char: 65,
2613 ascent: 800,
2614 descent: -200,
2615 italic_angle: 0.0,
2616 is_embedded: false,
2617 to_unicode: None,
2618 encoding: rpdfium_font::fx_codepage::FontEncoding::Standard,
2619 font_data: None,
2620 cid_to_gid: None,
2621 wmode: 0,
2622 vertical_metrics: None,
2623 cid_cmap: None,
2624 type3: Some(type3),
2625 weight: None,
2626 flags: None,
2627 cid_coding: rpdfium_font::cid_font::CidCoding::Unknown,
2628 ttc_face_index: 0,
2629 glyph_outline_cache: std::sync::OnceLock::new(),
2630 })
2631 }
2632
2633 #[test]
2634 fn test_text_clip_mode4_fill_and_clip() {
2635 let font = make_type3_font();
2637 let tree = DisplayTree {
2638 root: DisplayNode::Group {
2639 blend_mode: BlendMode::Normal,
2640 clip: None,
2641 opacity: 1.0,
2642 isolated: false,
2643 knockout: false,
2644 soft_mask: None,
2645 children: vec![DisplayNode::Text {
2646 runs: vec![TextRun {
2647 font_name: Name::from("F1"),
2648 font_size: 12.0,
2649 matrix: Matrix::identity(),
2650 text: vec![65],
2651 positions: vec![7.2],
2652 resolved_font: Some(font),
2653 rendering_mode: TextRenderingMode::FillClip,
2654 rise: 0.0,
2655 fill_color: Some(Color::gray(0.0)),
2656 stroke_color: None,
2657 actual_text: None,
2658 type3_glyph_ops: None,
2659 actual_text_id: None,
2660 is_vertical: false,
2661 vert_origins: vec![],
2662 }],
2663 }],
2664 },
2665 };
2666 let log = run_mock_renderer(&tree);
2667 assert!(
2669 log.contains(&"fill_path".to_string()),
2670 "expected fill_path in {:?}",
2671 log
2672 );
2673 assert!(
2674 log.contains(&"push_clip".to_string()),
2675 "expected push_clip in {:?}",
2676 log
2677 );
2678 assert!(
2680 log.contains(&"pop_clip".to_string()),
2681 "expected pop_clip in {:?}",
2682 log
2683 );
2684 }
2685
2686 #[test]
2687 fn test_text_clip_mode7_clip_only() {
2688 let font = make_type3_font();
2690 let tree = DisplayTree {
2691 root: DisplayNode::Group {
2692 blend_mode: BlendMode::Normal,
2693 clip: None,
2694 opacity: 1.0,
2695 isolated: false,
2696 knockout: false,
2697 soft_mask: None,
2698 children: vec![DisplayNode::Text {
2699 runs: vec![TextRun {
2700 font_name: Name::from("F1"),
2701 font_size: 12.0,
2702 matrix: Matrix::identity(),
2703 text: vec![65],
2704 positions: vec![7.2],
2705 resolved_font: Some(font),
2706 rendering_mode: TextRenderingMode::Clip,
2707 rise: 0.0,
2708 fill_color: Some(Color::gray(0.0)),
2709 stroke_color: None,
2710 actual_text: None,
2711 type3_glyph_ops: None,
2712 actual_text_id: None,
2713 is_vertical: false,
2714 vert_origins: vec![],
2715 }],
2716 }],
2717 },
2718 };
2719 let log = run_mock_renderer(&tree);
2720 assert!(
2722 !log.contains(&"fill_path".to_string()),
2723 "unexpected fill_path in {:?}",
2724 log
2725 );
2726 assert!(
2727 !log.contains(&"stroke_path".to_string()),
2728 "unexpected stroke_path in {:?}",
2729 log
2730 );
2731 assert!(
2733 log.contains(&"push_clip".to_string()),
2734 "expected push_clip in {:?}",
2735 log
2736 );
2737 assert!(
2738 log.contains(&"pop_clip".to_string()),
2739 "expected pop_clip in {:?}",
2740 log
2741 );
2742 }
2743
2744 #[test]
2745 fn test_text_fill_mode0_no_clip() {
2746 let font = make_type3_font();
2748 let tree = DisplayTree {
2749 root: DisplayNode::Group {
2750 blend_mode: BlendMode::Normal,
2751 clip: None,
2752 opacity: 1.0,
2753 isolated: false,
2754 knockout: false,
2755 soft_mask: None,
2756 children: vec![DisplayNode::Text {
2757 runs: vec![TextRun {
2758 font_name: Name::from("F1"),
2759 font_size: 12.0,
2760 matrix: Matrix::identity(),
2761 text: vec![65],
2762 positions: vec![7.2],
2763 resolved_font: Some(font),
2764 rendering_mode: TextRenderingMode::Fill,
2765 rise: 0.0,
2766 fill_color: Some(Color::gray(0.0)),
2767 stroke_color: None,
2768 actual_text: None,
2769 type3_glyph_ops: None,
2770 actual_text_id: None,
2771 is_vertical: false,
2772 vert_origins: vec![],
2773 }],
2774 }],
2775 },
2776 };
2777 let log = run_mock_renderer(&tree);
2778 assert!(log.contains(&"fill_path".to_string()));
2780 assert!(
2781 !log.contains(&"push_clip".to_string()),
2782 "unexpected push_clip in {:?}",
2783 log
2784 );
2785 }
2786
2787 #[test]
2790 fn test_text_clip_mode5_stroke_clip() {
2791 let font = make_type3_font();
2793 let tree = DisplayTree {
2794 root: DisplayNode::Group {
2795 blend_mode: BlendMode::Normal,
2796 clip: None,
2797 opacity: 1.0,
2798 isolated: false,
2799 knockout: false,
2800 soft_mask: None,
2801 children: vec![DisplayNode::Text {
2802 runs: vec![TextRun {
2803 font_name: Name::from("F1"),
2804 font_size: 12.0,
2805 matrix: Matrix::identity(),
2806 text: vec![65],
2807 positions: vec![7.2],
2808 resolved_font: Some(font),
2809 rendering_mode: TextRenderingMode::StrokeClip,
2810 rise: 0.0,
2811 fill_color: Some(Color::gray(0.0)),
2812 stroke_color: Some(Color::gray(0.0)),
2813 actual_text: None,
2814 type3_glyph_ops: None,
2815 actual_text_id: None,
2816 is_vertical: false,
2817 vert_origins: vec![],
2818 }],
2819 }],
2820 },
2821 };
2822 let log = run_mock_renderer(&tree);
2823 assert!(
2825 log.contains(&"stroke_path".to_string()),
2826 "expected stroke_path in {log:?}"
2827 );
2828 assert!(
2829 !log.contains(&"fill_path".to_string()),
2830 "unexpected fill_path in {log:?}"
2831 );
2832 assert!(
2833 log.contains(&"push_clip".to_string()),
2834 "expected push_clip in {log:?}"
2835 );
2836 assert!(
2837 log.contains(&"pop_clip".to_string()),
2838 "expected pop_clip in {log:?}"
2839 );
2840 }
2841
2842 #[test]
2843 fn test_text_clip_mode6_fill_stroke_clip() {
2844 let font = make_type3_font();
2846 let tree = DisplayTree {
2847 root: DisplayNode::Group {
2848 blend_mode: BlendMode::Normal,
2849 clip: None,
2850 opacity: 1.0,
2851 isolated: false,
2852 knockout: false,
2853 soft_mask: None,
2854 children: vec![DisplayNode::Text {
2855 runs: vec![TextRun {
2856 font_name: Name::from("F1"),
2857 font_size: 12.0,
2858 matrix: Matrix::identity(),
2859 text: vec![65],
2860 positions: vec![7.2],
2861 resolved_font: Some(font),
2862 rendering_mode: TextRenderingMode::FillStrokeClip,
2863 rise: 0.0,
2864 fill_color: Some(Color::gray(0.0)),
2865 stroke_color: Some(Color::gray(0.0)),
2866 actual_text: None,
2867 type3_glyph_ops: None,
2868 actual_text_id: None,
2869 is_vertical: false,
2870 vert_origins: vec![],
2871 }],
2872 }],
2873 },
2874 };
2875 let log = run_mock_renderer(&tree);
2876 assert!(
2878 log.contains(&"fill_path".to_string()),
2879 "expected fill_path in {log:?}"
2880 );
2881 assert!(
2882 log.contains(&"stroke_path".to_string()),
2883 "expected stroke_path in {log:?}"
2884 );
2885 assert!(
2886 log.contains(&"push_clip".to_string()),
2887 "expected push_clip in {log:?}"
2888 );
2889 assert!(
2890 log.contains(&"pop_clip".to_string()),
2891 "expected pop_clip in {log:?}"
2892 );
2893 }
2894
2895 #[test]
2896 fn test_text_clip_mode3_invisible_no_draw_no_clip() {
2897 let font = make_type3_font();
2899 let tree = DisplayTree {
2900 root: DisplayNode::Group {
2901 blend_mode: BlendMode::Normal,
2902 clip: None,
2903 opacity: 1.0,
2904 isolated: false,
2905 knockout: false,
2906 soft_mask: None,
2907 children: vec![DisplayNode::Text {
2908 runs: vec![TextRun {
2909 font_name: Name::from("F1"),
2910 font_size: 12.0,
2911 matrix: Matrix::identity(),
2912 text: vec![65],
2913 positions: vec![7.2],
2914 resolved_font: Some(font),
2915 rendering_mode: TextRenderingMode::Invisible,
2916 rise: 0.0,
2917 fill_color: Some(Color::gray(0.0)),
2918 stroke_color: None,
2919 actual_text: None,
2920 type3_glyph_ops: None,
2921 actual_text_id: None,
2922 is_vertical: false,
2923 vert_origins: vec![],
2924 }],
2925 }],
2926 },
2927 };
2928 let log = run_mock_renderer(&tree);
2929 assert!(
2931 !log.contains(&"fill_path".to_string()),
2932 "unexpected fill_path in {log:?}"
2933 );
2934 assert!(
2935 !log.contains(&"stroke_path".to_string()),
2936 "unexpected stroke_path in {log:?}"
2937 );
2938 assert!(
2939 !log.contains(&"push_clip".to_string()),
2940 "unexpected push_clip in {log:?}"
2941 );
2942 }
2943
2944 #[test]
2945 fn test_transform_path_op_identity() {
2946 let m = Matrix::identity();
2947 let op = PathOp::MoveTo { x: 10.0, y: 20.0 };
2948 let result = transform_path_op(&op, &m);
2949 match result {
2950 PathOp::MoveTo { x, y } => {
2951 assert!((x - 10.0).abs() < 0.01);
2952 assert!((y - 20.0).abs() < 0.01);
2953 }
2954 _ => panic!("expected MoveTo"),
2955 }
2956 }
2957
2958 #[test]
2959 fn test_transform_path_op_scale() {
2960 let m = Matrix::from_scale(2.0, 3.0);
2961 let op = PathOp::LineTo { x: 5.0, y: 10.0 };
2962 let result = transform_path_op(&op, &m);
2963 match result {
2964 PathOp::LineTo { x, y } => {
2965 assert!((x - 10.0).abs() < 0.01);
2966 assert!((y - 30.0).abs() < 0.01);
2967 }
2968 _ => panic!("expected LineTo"),
2969 }
2970 }
2971
2972 #[test]
2973 fn test_transform_path_op_close() {
2974 let m = Matrix::from_scale(2.0, 3.0);
2975 let result = transform_path_op(&PathOp::Close, &m);
2976 assert!(matches!(result, PathOp::Close));
2977 }
2978
2979 #[test]
2982 fn test_path_bounding_box_basic() {
2983 let ops = vec![
2984 PathOp::MoveTo { x: 10.0, y: 20.0 },
2985 PathOp::LineTo { x: 100.0, y: 20.0 },
2986 PathOp::LineTo { x: 100.0, y: 200.0 },
2987 PathOp::LineTo { x: 10.0, y: 200.0 },
2988 PathOp::Close,
2989 ];
2990 let bbox = path_bounding_box(&ops);
2991 assert_eq!(bbox, [10.0, 20.0, 100.0, 200.0]);
2992 }
2993
2994 #[test]
2995 fn test_path_bounding_box_empty() {
2996 let bbox = path_bounding_box(&[]);
2997 assert_eq!(bbox, [0.0, 0.0, 0.0, 0.0]);
2998 }
2999
3000 #[test]
3001 fn test_path_bounding_box_with_curves() {
3002 let ops = vec![
3003 PathOp::MoveTo { x: 0.0, y: 0.0 },
3004 PathOp::CurveTo {
3005 x1: 50.0,
3006 y1: 100.0,
3007 x2: 150.0,
3008 y2: 100.0,
3009 x3: 200.0,
3010 y3: 0.0,
3011 },
3012 ];
3013 let bbox = path_bounding_box(&ops);
3014 assert_eq!(bbox[0], 0.0);
3015 assert_eq!(bbox[1], 0.0);
3016 assert_eq!(bbox[2], 200.0);
3017 assert_eq!(bbox[3], 100.0);
3018 }
3019
3020 #[test]
3021 fn test_renderer_pattern_fill_clips_and_draws() {
3022 use rpdfium_page::pattern::{PaintType, TilingPattern, TilingType};
3023
3024 let pattern_tree = DisplayTree {
3026 root: DisplayNode::Group {
3027 blend_mode: BlendMode::Normal,
3028 clip: None,
3029 opacity: 1.0,
3030 isolated: true,
3031 knockout: false,
3032 soft_mask: None,
3033 children: vec![DisplayNode::Path {
3034 ops: vec![
3035 PathOp::MoveTo { x: 0.0, y: 0.0 },
3036 PathOp::LineTo { x: 10.0, y: 0.0 },
3037 PathOp::LineTo { x: 10.0, y: 10.0 },
3038 PathOp::LineTo { x: 0.0, y: 10.0 },
3039 PathOp::Close,
3040 ],
3041 style: PathStyle {
3042 fill: Some(FillRule::NonZero),
3043 ..PathStyle::default()
3044 },
3045 matrix: Matrix::identity(),
3046 fill_color: Some(Color::rgb(1.0, 0.0, 0.0)),
3047 stroke_color: None,
3048 fill_color_space: None,
3049 stroke_color_space: None,
3050 transfer_function: None,
3051 overprint: false,
3052 overprint_mode: 0,
3053 }],
3054 },
3055 };
3056
3057 let pattern = TilingPattern {
3058 paint_type: PaintType::Colored,
3059 tiling_type: TilingType::ConstantSpacing,
3060 bbox: [0.0, 0.0, 10.0, 10.0],
3061 x_step: 10.0,
3062 y_step: 10.0,
3063 matrix: Matrix::identity(),
3064 resources_id: None,
3065 stream_id: rpdfium_core::ObjectId::new(1, 0),
3066 };
3067
3068 let tree = DisplayTree {
3069 root: DisplayNode::Group {
3070 blend_mode: BlendMode::Normal,
3071 clip: None,
3072 opacity: 1.0,
3073 isolated: false,
3074 knockout: false,
3075 soft_mask: None,
3076 children: vec![DisplayNode::PatternFill {
3077 path_ops: vec![
3078 PathOp::MoveTo { x: 0.0, y: 0.0 },
3079 PathOp::LineTo { x: 50.0, y: 0.0 },
3080 PathOp::LineTo { x: 50.0, y: 50.0 },
3081 PathOp::LineTo { x: 0.0, y: 50.0 },
3082 PathOp::Close,
3083 ],
3084 fill_rule: FillRule::NonZero,
3085 pattern,
3086 pattern_tree: Box::new(pattern_tree),
3087 fill_color: None,
3088 matrix: Matrix::identity(),
3089 }],
3090 },
3091 };
3092 let log = run_mock_renderer(&tree);
3093 assert!(
3095 log.contains(&"push_clip".to_string()),
3096 "expected push_clip in {:?}",
3097 log
3098 );
3099 assert!(
3100 log.contains(&"pop_clip".to_string()),
3101 "expected pop_clip in {:?}",
3102 log
3103 );
3104 }
3105
3106 #[test]
3109 fn test_apply_image_mask_none_passes_through() {
3110 use crate::image::{DecodedImage, DecodedImageFormat};
3111
3112 let img = DecodedImage {
3113 width: 2,
3114 height: 2,
3115 data: vec![100, 200, 50, 255],
3116 format: DecodedImageFormat::Gray8,
3117 };
3118 let result = apply_image_mask(img, None, None, None);
3119 assert_eq!(result.format, DecodedImageFormat::Gray8);
3120 assert_eq!(result.data, vec![100, 200, 50, 255]);
3121 }
3122
3123 #[test]
3124 fn test_apply_stencil_mask_gray8() {
3125 use crate::image::{DecodedImage, DecodedImageFormat};
3126 use rpdfium_page::display::ImageMask;
3127
3128 let img = DecodedImage {
3130 width: 2,
3131 height: 1,
3132 data: vec![255, 0],
3133 format: DecodedImageFormat::Gray8,
3134 };
3135 let fill = Color::rgb(1.0, 0.0, 0.0);
3136 let mask = ImageMask::Stencil;
3137 let result = apply_image_mask(img, Some(&mask), Some(&fill), None);
3138
3139 assert_eq!(result.format, DecodedImageFormat::Rgba32);
3140 assert_eq!(result.data.len(), 8); assert_eq!(result.data[0], 255); assert_eq!(result.data[1], 0); assert_eq!(result.data[2], 0); assert_eq!(result.data[3], 255); assert_eq!(result.data[4], 0);
3148 assert_eq!(result.data[5], 0);
3149 assert_eq!(result.data[6], 0);
3150 assert_eq!(result.data[7], 0);
3151 }
3152
3153 #[test]
3154 fn test_apply_stencil_mask_no_fill_uses_black() {
3155 use crate::image::{DecodedImage, DecodedImageFormat};
3156 use rpdfium_page::display::ImageMask;
3157
3158 let img = DecodedImage {
3159 width: 1,
3160 height: 1,
3161 data: vec![128],
3162 format: DecodedImageFormat::Gray8,
3163 };
3164 let mask = ImageMask::Stencil;
3165 let result = apply_image_mask(img, Some(&mask), None, None);
3166
3167 assert_eq!(result.format, DecodedImageFormat::Rgba32);
3168 assert_eq!(result.data[0], 0); assert_eq!(result.data[1], 0); assert_eq!(result.data[2], 0); assert_eq!(result.data[3], 255); }
3174
3175 #[test]
3176 fn test_apply_color_key_mask_gray8() {
3177 use crate::image::{DecodedImage, DecodedImageFormat};
3178 use rpdfium_page::display::ImageMask;
3179
3180 let img = DecodedImage {
3183 width: 3,
3184 height: 1,
3185 data: vec![100, 150, 200],
3186 format: DecodedImageFormat::Gray8,
3187 };
3188 let mask = ImageMask::ColorKey {
3189 ranges: vec![[100, 160]],
3190 };
3191 let result = apply_image_mask(img, Some(&mask), None, None);
3192
3193 assert_eq!(result.format, DecodedImageFormat::Rgba32);
3194 assert_eq!(result.data[3], 0);
3196 assert_eq!(result.data[7], 0);
3198 assert_eq!(result.data[11], 255);
3200 }
3201
3202 #[test]
3203 fn test_apply_color_key_mask_rgb24() {
3204 use crate::image::{DecodedImage, DecodedImageFormat};
3205 use rpdfium_page::display::ImageMask;
3206
3207 let img = DecodedImage {
3210 width: 2,
3211 height: 1,
3212 data: vec![255, 0, 0, 0, 255, 0],
3213 format: DecodedImageFormat::Rgb24,
3214 };
3215 let mask = ImageMask::ColorKey {
3216 ranges: vec![[200, 255], [0, 10], [0, 10]],
3217 };
3218 let result = apply_image_mask(img, Some(&mask), None, None);
3219
3220 assert_eq!(result.format, DecodedImageFormat::Rgba32);
3221 assert_eq!(result.data[0], 255); assert_eq!(result.data[3], 0); assert_eq!(result.data[4], 0); assert_eq!(result.data[5], 255); assert_eq!(result.data[7], 255); }
3229
3230 #[test]
3231 fn test_apply_explicit_mask_passes_through() {
3232 use crate::image::{DecodedImage, DecodedImageFormat};
3233 use rpdfium_page::display::ImageMask;
3234
3235 let img = DecodedImage {
3236 width: 1,
3237 height: 1,
3238 data: vec![128],
3239 format: DecodedImageFormat::Gray8,
3240 };
3241 let mask = ImageMask::ExplicitMask {
3242 mask_object_id: rpdfium_core::ObjectId::new(5, 0),
3243 };
3244 let result = apply_image_mask(img, Some(&mask), None, None);
3245
3246 assert_eq!(result.format, DecodedImageFormat::Gray8);
3248 assert_eq!(result.data, vec![128]);
3249 }
3250
3251 #[test]
3252 fn test_apply_soft_mask_passes_through() {
3253 use crate::image::{DecodedImage, DecodedImageFormat};
3254 use rpdfium_page::display::ImageMask;
3255
3256 let img = DecodedImage {
3257 width: 1,
3258 height: 1,
3259 data: vec![64, 128, 192],
3260 format: DecodedImageFormat::Rgb24,
3261 };
3262 let mask = ImageMask::SoftMask {
3263 smask_object_id: rpdfium_core::ObjectId::new(6, 0),
3264 matte: None,
3265 };
3266 let result = apply_image_mask(img, Some(&mask), None, None);
3267
3268 assert_eq!(result.format, DecodedImageFormat::Rgb24);
3270 assert_eq!(result.data, vec![64, 128, 192]);
3271 }
3272
3273 #[test]
3274 fn test_renderer_pattern_fill_with_non_identity_matrix() {
3275 use rpdfium_page::pattern::{PaintType, TilingPattern, TilingType};
3276
3277 let pattern_tree = DisplayTree {
3278 root: DisplayNode::Group {
3279 blend_mode: BlendMode::Normal,
3280 clip: None,
3281 opacity: 1.0,
3282 isolated: true,
3283 knockout: false,
3284 soft_mask: None,
3285 children: vec![],
3286 },
3287 };
3288
3289 let pattern = TilingPattern {
3291 paint_type: PaintType::Colored,
3292 tiling_type: TilingType::ConstantSpacing,
3293 bbox: [0.0, 0.0, 5.0, 5.0],
3294 x_step: 5.0,
3295 y_step: 5.0,
3296 matrix: Matrix::from_scale(2.0, 2.0),
3297 resources_id: None,
3298 stream_id: rpdfium_core::ObjectId::new(1, 0),
3299 };
3300
3301 let tree = DisplayTree {
3302 root: DisplayNode::Group {
3303 blend_mode: BlendMode::Normal,
3304 clip: None,
3305 opacity: 1.0,
3306 isolated: false,
3307 knockout: false,
3308 soft_mask: None,
3309 children: vec![DisplayNode::PatternFill {
3310 path_ops: vec![
3311 PathOp::MoveTo { x: 0.0, y: 0.0 },
3312 PathOp::LineTo { x: 20.0, y: 0.0 },
3313 PathOp::LineTo { x: 20.0, y: 20.0 },
3314 PathOp::LineTo { x: 0.0, y: 20.0 },
3315 PathOp::Close,
3316 ],
3317 fill_rule: FillRule::EvenOdd,
3318 pattern,
3319 pattern_tree: Box::new(pattern_tree),
3320 fill_color: None,
3321 matrix: Matrix::identity(),
3322 }],
3323 },
3324 };
3325 let log = run_mock_renderer(&tree);
3326 assert!(
3328 log.contains(&"push_clip".to_string()),
3329 "expected push_clip in {:?}",
3330 log
3331 );
3332 assert!(
3333 log.contains(&"pop_clip".to_string()),
3334 "expected pop_clip in {:?}",
3335 log
3336 );
3337 }
3338
3339 #[test]
3342 fn test_apply_transfer_identity_no_change() {
3343 use rpdfium_page::function::TransferFunction;
3344 let mut color = RgbaColor {
3345 r: 128,
3346 g: 64,
3347 b: 200,
3348 a: 255,
3349 };
3350 apply_transfer(&mut color, &TransferFunction::Identity);
3351 assert_eq!(color.r, 128);
3352 assert_eq!(color.g, 64);
3353 assert_eq!(color.b, 200);
3354 assert_eq!(color.a, 255);
3355 }
3356
3357 #[test]
3358 fn test_apply_transfer_single_function() {
3359 use rpdfium_page::function::{PdfFunction, TransferFunction};
3360 let func = PdfFunction::Type2 {
3362 domain: [0.0, 1.0],
3363 range: vec![],
3364 c0: vec![1.0],
3365 c1: vec![0.0],
3366 n: 1.0,
3367 };
3368 let tf = TransferFunction::Single(func);
3369 let mut color = RgbaColor {
3370 r: 0,
3371 g: 255,
3372 b: 128,
3373 a: 200,
3374 };
3375 apply_transfer(&mut color, &tf);
3376 assert_eq!(color.r, 255);
3378 assert_eq!(color.g, 0);
3380 assert!((color.b as i32 - 127).abs() <= 1);
3382 assert_eq!(color.a, 200);
3384 }
3385
3386 #[test]
3387 fn test_apply_transfer_per_component() {
3388 use rpdfium_page::function::{PdfFunction, TransferFunction};
3389 let r_fn = PdfFunction::Type2 {
3391 domain: [0.0, 1.0],
3392 range: vec![],
3393 c0: vec![0.0],
3394 c1: vec![1.0],
3395 n: 1.0,
3396 };
3397 let g_fn = PdfFunction::Type2 {
3398 domain: [0.0, 1.0],
3399 range: vec![],
3400 c0: vec![1.0],
3401 c1: vec![1.0],
3402 n: 1.0,
3403 };
3404 let b_fn = PdfFunction::Type2 {
3405 domain: [0.0, 1.0],
3406 range: vec![],
3407 c0: vec![0.0],
3408 c1: vec![0.0],
3409 n: 1.0,
3410 };
3411 let k_fn = PdfFunction::Type2 {
3412 domain: [0.0, 1.0],
3413 range: vec![],
3414 c0: vec![0.0],
3415 c1: vec![1.0],
3416 n: 1.0,
3417 };
3418 let tf = TransferFunction::PerComponent {
3419 r: Box::new(r_fn),
3420 g: Box::new(g_fn),
3421 b: Box::new(b_fn),
3422 k: Box::new(k_fn),
3423 };
3424 let mut color = RgbaColor {
3425 r: 128,
3426 g: 128,
3427 b: 128,
3428 a: 255,
3429 };
3430 apply_transfer(&mut color, &tf);
3431 assert!((color.r as i32 - 128).abs() <= 1);
3433 assert_eq!(color.g, 255);
3435 assert_eq!(color.b, 0);
3437 assert_eq!(color.a, 255);
3439 }
3440
3441 #[test]
3442 fn test_transfer_function_identity_default_graphics_state() {
3443 let gs = rpdfium_page::GraphicsState::default();
3445 assert!(gs.transfer_function.is_none());
3446 }
3447
3448 #[test]
3449 fn test_apply_transfer_identity_sampling_256() {
3450 use rpdfium_page::function::TransferFunction;
3451 for v in 0..=255u8 {
3453 let mut color = RgbaColor {
3454 r: v,
3455 g: v,
3456 b: v,
3457 a: v,
3458 };
3459 apply_transfer(&mut color, &TransferFunction::Identity);
3460 assert_eq!(color.r, v, "r mismatch at {v}");
3461 assert_eq!(color.g, v, "g mismatch at {v}");
3462 assert_eq!(color.b, v, "b mismatch at {v}");
3463 assert_eq!(color.a, v, "a should be unchanged at {v}");
3464 }
3465 }
3466
3467 #[test]
3468 fn test_apply_transfer_per_component_rgb_independent() {
3469 use rpdfium_page::function::{PdfFunction, TransferFunction};
3470 let r_fn = PdfFunction::Type2 {
3472 domain: [0.0, 1.0],
3473 range: vec![],
3474 c0: vec![0.0],
3475 c1: vec![0.0],
3476 n: 1.0,
3477 };
3478 let g_fn = PdfFunction::Type2 {
3479 domain: [0.0, 1.0],
3480 range: vec![],
3481 c0: vec![0.0],
3482 c1: vec![1.0],
3483 n: 1.0,
3484 };
3485 let b_fn = PdfFunction::Type2 {
3486 domain: [0.0, 1.0],
3487 range: vec![],
3488 c0: vec![1.0],
3489 c1: vec![1.0],
3490 n: 1.0,
3491 };
3492 let k_fn = PdfFunction::Type2 {
3493 domain: [0.0, 1.0],
3494 range: vec![],
3495 c0: vec![0.0],
3496 c1: vec![1.0],
3497 n: 1.0,
3498 };
3499 let tf = TransferFunction::PerComponent {
3500 r: Box::new(r_fn),
3501 g: Box::new(g_fn),
3502 b: Box::new(b_fn),
3503 k: Box::new(k_fn),
3504 };
3505 let mut color = RgbaColor {
3506 r: 200,
3507 g: 100,
3508 b: 50,
3509 a: 180,
3510 };
3511 apply_transfer(&mut color, &tf);
3512 assert_eq!(color.r, 0);
3514 assert!((color.g as i32 - 100).abs() <= 1);
3516 assert_eq!(color.b, 255);
3518 assert_eq!(color.a, 180);
3520 }
3521
3522 #[test]
3523 fn test_apply_transfer_zero_alpha_preserved() {
3524 use rpdfium_page::function::{PdfFunction, TransferFunction};
3525 let func = PdfFunction::Type2 {
3527 domain: [0.0, 1.0],
3528 range: vec![],
3529 c0: vec![1.0],
3530 c1: vec![0.0],
3531 n: 1.0,
3532 };
3533 let tf = TransferFunction::Single(func);
3534 let mut color = RgbaColor {
3535 r: 128,
3536 g: 128,
3537 b: 128,
3538 a: 0,
3539 };
3540 apply_transfer(&mut color, &tf);
3541 assert_eq!(color.a, 0);
3543 assert!((color.r as i32 - 127).abs() <= 1);
3545 }
3546
3547 #[test]
3548 fn test_apply_transfer_boundary_values() {
3549 use rpdfium_page::function::{PdfFunction, TransferFunction};
3550 let func = PdfFunction::Type2 {
3552 domain: [0.0, 1.0],
3553 range: vec![],
3554 c0: vec![0.0],
3555 c1: vec![1.0],
3556 n: 1.0,
3557 };
3558 let tf = TransferFunction::Single(func);
3559 let mut color = RgbaColor {
3560 r: 0,
3561 g: 255,
3562 b: 1,
3563 a: 42,
3564 };
3565 apply_transfer(&mut color, &tf);
3566 assert_eq!(color.r, 0);
3568 assert_eq!(color.g, 255);
3570 assert!((color.b as i32 - 1).abs() <= 1);
3572 assert_eq!(color.a, 42);
3574 }
3575
3576 #[test]
3581 fn test_renderer_has_raster_cache() {
3582 let mut backend = MockBackend::new();
3583 let mut surface = backend.create_surface(100, 100, &RgbaColor::WHITE);
3584 let renderer = DisplayRenderer::new(&mut backend, &mut surface, Matrix::identity(), None);
3585 assert!(renderer.raster_cache.is_empty());
3587 }
3588
3589 #[test]
3590 fn test_rasterize_glyph_alpha_simple_rect() {
3591 let ops = vec![
3593 PathOp::MoveTo { x: 0.0, y: 0.0 },
3594 PathOp::LineTo { x: 100.0, y: 0.0 },
3595 PathOp::LineTo { x: 100.0, y: 100.0 },
3596 PathOp::LineTo { x: 0.0, y: 100.0 },
3597 PathOp::Close,
3598 ];
3599 let result = super::rasterize_glyph_alpha(&ops, 0.1, 10, 256);
3600 assert!(result.is_some());
3601 let glyph = result.unwrap();
3602 assert!(glyph.width > 0);
3603 assert!(glyph.height > 0);
3604 assert_eq!(glyph.alpha.len(), (glyph.width * glyph.height) as usize);
3605 assert!(glyph.alpha.iter().any(|&a| a > 0));
3607 }
3608
3609 #[test]
3610 fn test_rasterize_glyph_alpha_empty_ops() {
3611 let result = super::rasterize_glyph_alpha(&[], 1.0, 12, 256);
3612 assert!(result.is_none());
3613 }
3614
3615 #[test]
3616 fn test_rasterize_glyph_alpha_zero_dim() {
3617 let ops = vec![
3618 PathOp::MoveTo { x: 0.0, y: 0.0 },
3619 PathOp::LineTo { x: 10.0, y: 10.0 },
3620 ];
3621 let result = super::rasterize_glyph_alpha(&ops, 1.0, 12, 0);
3622 assert!(result.is_none());
3623 }
3624
3625 struct MockImageDecoder {
3631 mask_data: Vec<u8>,
3633 mask_width: u32,
3634 mask_height: u32,
3635 }
3636
3637 impl crate::image::ImageDecoder for MockImageDecoder {
3638 fn decode_image(
3639 &self,
3640 _image_ref: &rpdfium_graphics::ImageRef,
3641 _matrix: &Matrix,
3642 ) -> Result<crate::image::DecodedImage, crate::error::RenderError> {
3643 Ok(crate::image::DecodedImage {
3644 width: self.mask_width,
3645 height: self.mask_height,
3646 data: self.mask_data.clone(),
3647 format: crate::image::DecodedImageFormat::Gray8,
3648 })
3649 }
3650
3651 fn decode_inline_image(
3652 &self,
3653 _properties: &std::collections::HashMap<rpdfium_core::Name, rpdfium_parser::Operand>,
3654 _data: &[u8],
3655 _matrix: &Matrix,
3656 ) -> Result<crate::image::DecodedImage, crate::error::RenderError> {
3657 Err(crate::error::RenderError::ImageDecode(
3658 "not supported".to_string(),
3659 ))
3660 }
3661 }
3662
3663 #[test]
3664 fn test_apply_explicit_mask_with_decoder_applies_alpha() {
3665 use crate::image::{DecodedImage, DecodedImageFormat};
3666 use rpdfium_page::display::ImageMask;
3667
3668 let img = DecodedImage {
3670 width: 2,
3671 height: 2,
3672 data: vec![
3673 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, ],
3678 format: DecodedImageFormat::Rgb24,
3679 };
3680
3681 let decoder = MockImageDecoder {
3683 mask_data: vec![0, 128, 255, 0],
3684 mask_width: 2,
3685 mask_height: 2,
3686 };
3687
3688 let mask = ImageMask::ExplicitMask {
3689 mask_object_id: rpdfium_core::ObjectId::new(99, 0),
3690 };
3691
3692 let result = apply_image_mask(img, Some(&mask), None, Some(&decoder));
3693
3694 assert_eq!(result.format, DecodedImageFormat::Rgba32);
3695 assert_eq!(result.width, 2);
3696 assert_eq!(result.height, 2);
3697 assert_eq!(result.data.len(), 16);
3699
3700 assert_eq!(result.data[3], 0);
3702 assert_eq!(result.data[7], 128);
3704 assert_eq!(result.data[11], 255);
3706 assert_eq!(result.data[15], 0);
3708 }
3709
3710 #[test]
3711 fn test_apply_explicit_mask_with_decoder_scales_to_image_size() {
3712 use crate::image::{DecodedImage, DecodedImageFormat};
3713 use rpdfium_page::display::ImageMask;
3714
3715 let img = DecodedImage {
3717 width: 4,
3718 height: 4,
3719 data: vec![200u8; 16],
3720 format: DecodedImageFormat::Gray8,
3721 };
3722
3723 let decoder = MockImageDecoder {
3726 mask_data: vec![255, 0, 0, 255],
3727 mask_width: 2,
3728 mask_height: 2,
3729 };
3730
3731 let mask = ImageMask::ExplicitMask {
3732 mask_object_id: rpdfium_core::ObjectId::new(100, 0),
3733 };
3734
3735 let result = apply_image_mask(img, Some(&mask), None, Some(&decoder));
3736
3737 assert_eq!(result.format, DecodedImageFormat::Rgba32);
3738 assert_eq!(result.data.len(), 64);
3740
3741 assert_eq!(result.data[3], 255); assert_eq!(result.data[7], 255); assert_eq!(result.data[11], 0); assert_eq!(result.data[15], 0); }
3748
3749 #[test]
3754 fn test_smask_backdrop_color_converts_to_rgba() {
3755 use rpdfium_page::display::{SoftMask, SoftMaskSubtype};
3756
3757 let sm = SoftMask {
3760 subtype: SoftMaskSubtype::Luminosity,
3761 group: DisplayTree {
3762 root: DisplayNode::Group {
3763 blend_mode: BlendMode::Normal,
3764 clip: None,
3765 opacity: 1.0,
3766 isolated: true,
3767 knockout: false,
3768 soft_mask: None,
3769 children: vec![],
3770 },
3771 },
3772 backdrop_color: Some(vec![1.0, 1.0, 1.0]),
3773 transfer_function: None,
3774 };
3775
3776 let mut backend = MockBackend::new();
3780 let surface = backend.create_surface(100, 100, &RgbaColor::WHITE);
3781 let result = render_soft_mask_alpha(&mut backend, &surface, &sm, Matrix::identity(), None);
3782
3783 assert!(result.is_some());
3785 let (alpha, w, h) = result.unwrap();
3786 assert_eq!(w, 100);
3787 assert_eq!(h, 100);
3788 assert_eq!(alpha.len(), 10_000);
3789 }
3794
3795 #[test]
3796 fn test_smask_no_backdrop_uses_transparent() {
3797 use rpdfium_page::display::{SoftMask, SoftMaskSubtype};
3798
3799 let sm = SoftMask {
3800 subtype: SoftMaskSubtype::Alpha,
3801 group: DisplayTree {
3802 root: DisplayNode::Group {
3803 blend_mode: BlendMode::Normal,
3804 clip: None,
3805 opacity: 1.0,
3806 isolated: true,
3807 knockout: false,
3808 soft_mask: None,
3809 children: vec![],
3810 },
3811 },
3812 backdrop_color: None,
3813 transfer_function: None,
3814 };
3815
3816 let mut backend = MockBackend::new();
3817 let surface = backend.create_surface(100, 100, &RgbaColor::WHITE);
3818 let result = render_soft_mask_alpha(&mut backend, &surface, &sm, Matrix::identity(), None);
3819
3820 assert!(result.is_some());
3821 let (alpha, w, h) = result.unwrap();
3822 assert_eq!(w, 100);
3823 assert_eq!(h, 100);
3824 assert!(alpha.iter().all(|&a| a == 0));
3826 }
3827
3828 #[test]
3829 fn test_smask_backdrop_single_component_grayscale() {
3830 use rpdfium_page::display::{SoftMask, SoftMaskSubtype};
3831
3832 let sm = SoftMask {
3834 subtype: SoftMaskSubtype::Alpha,
3835 group: DisplayTree {
3836 root: DisplayNode::Group {
3837 blend_mode: BlendMode::Normal,
3838 clip: None,
3839 opacity: 1.0,
3840 isolated: true,
3841 knockout: false,
3842 soft_mask: None,
3843 children: vec![],
3844 },
3845 },
3846 backdrop_color: Some(vec![0.5]),
3847 transfer_function: None,
3848 };
3849
3850 let mut backend = MockBackend::new();
3851 let surface = backend.create_surface(10, 10, &RgbaColor::WHITE);
3852 let result = render_soft_mask_alpha(&mut backend, &surface, &sm, Matrix::identity(), None);
3853
3854 assert!(result.is_some());
3855 }
3856}