Skip to main content

rpdfium_render/
annotation_render.rs

1//! Annotation rendering support.
2//!
3//! Renders annotation appearance streams as overlays on top of the page content.
4//! Annotations without appearance streams (`/AP`) are skipped.
5
6use rpdfium_core::Matrix;
7use rpdfium_doc::Annotation;
8use rpdfium_graphics::Bitmap;
9use rpdfium_page::display::{DisplayTree, walk};
10
11use crate::cfx_defaultrenderdevice::TinySkiaBackend;
12use crate::error::RenderError;
13use crate::image::ImageDecoder;
14use crate::page_transform::compute_page_transform;
15use crate::render_defines::RenderConfig;
16use crate::renderdevicedriver_iface::RenderBackend;
17use crate::renderer::DisplayRenderer;
18
19/// An annotation paired with its pre-interpreted appearance stream.
20///
21/// The `display_tree` is the result of interpreting the annotation's
22/// appearance stream (a Form XObject) as a `DisplayTree`. The annotation's
23/// `/Rect` and visibility flags are used to position and filter it.
24#[derive(Debug, Clone)]
25pub struct AnnotationOverlay {
26    /// The parsed annotation metadata (subtype, rect, flags, etc.).
27    pub annotation: Annotation,
28    /// The display tree from the annotation's normal appearance stream.
29    pub display_tree: DisplayTree,
30}
31
32/// Render a display tree with annotation overlays.
33///
34/// After rendering the page content, each visible annotation's appearance
35/// stream display tree is rendered on top, positioned at the annotation's
36/// `/Rect`.
37pub fn render_with_annotations(
38    tree: &DisplayTree,
39    annotations: &[AnnotationOverlay],
40    config: &RenderConfig,
41    decoder: Option<&dyn ImageDecoder>,
42) -> Result<Bitmap, RenderError> {
43    let mut backend = TinySkiaBackend::new();
44
45    // Use forced color scheme background when set.
46    let effective_bg = config
47        .forced_color_scheme
48        .as_ref()
49        .map_or(&config.background, |s| &s.background_color);
50
51    let mut surface = backend.create_surface(config.width, config.height, effective_bg);
52
53    let page_transform = match config.media_box {
54        Some(ref mb) => compute_page_transform(mb, config.width, config.height, config.rotation),
55        None => Matrix::identity(),
56    };
57
58    // Render main page content
59    {
60        let mut renderer =
61            DisplayRenderer::new(&mut backend, &mut surface, page_transform, decoder)
62                .with_per_feature_aa(
63                    config.text_antialiasing,
64                    config.path_antialiasing,
65                    config.image_antialiasing,
66                );
67        if let Some(ref scheme) = config.forced_color_scheme {
68            renderer = renderer.with_forced_color_scheme(scheme.clone());
69        }
70        walk(tree, &mut renderer);
71    }
72
73    // Overlay each visible annotation
74    for overlay in annotations {
75        if !should_render_annotation(&overlay.annotation) {
76            continue;
77        }
78
79        // Apply annotation position transform from /Rect
80        let rect = &overlay.annotation.rect;
81        let annot_transform = Matrix::from_translation(rect[0] as f64, rect[1] as f64);
82        let combined = page_transform.pre_concat(&annot_transform);
83
84        // The annotation's appearance stream is already a Form XObject display tree.
85        // Render with the combined transform so coordinates map to the annotation's /Rect.
86        let mut renderer = DisplayRenderer::new(&mut backend, &mut surface, combined, decoder)
87            .with_per_feature_aa(
88                config.text_antialiasing,
89                config.path_antialiasing,
90                config.image_antialiasing,
91            );
92        if let Some(ref scheme) = config.forced_color_scheme {
93            renderer = renderer.with_forced_color_scheme(scheme.clone());
94        }
95        walk(&overlay.display_tree, &mut renderer);
96    }
97
98    Ok(backend.finish(surface))
99}
100
101/// Determine if an annotation should be rendered for screen display.
102///
103/// An annotation is rendered if:
104/// - It is not hidden (flag bit 2)
105/// - It is not set to no-view (flag bit 6)
106/// - It has an appearance stream (checked by the caller via `AnnotationOverlay`)
107fn should_render_annotation(annotation: &Annotation) -> bool {
108    annotation.flags.is_visible()
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use rpdfium_doc::{AnnotationFlags, AnnotationSubtypeData, AnnotationType};
115    use rpdfium_graphics::BlendMode;
116    use rpdfium_page::display::DisplayNode;
117
118    use crate::color_convert::RgbaColor;
119
120    fn make_empty_tree() -> DisplayTree {
121        DisplayTree {
122            root: DisplayNode::Group {
123                blend_mode: BlendMode::Normal,
124                clip: None,
125                opacity: 1.0,
126                isolated: false,
127                knockout: false,
128                soft_mask: None,
129                children: Vec::new(),
130            },
131        }
132    }
133
134    fn make_test_annotation(flags: u32) -> Annotation {
135        Annotation {
136            subtype: AnnotationType::Text,
137            rect: [10.0, 10.0, 50.0, 50.0],
138            contents: None,
139            flags: AnnotationFlags::from_bits(flags),
140            name: None,
141            appearance: None,
142            color: None,
143            border: None,
144            action: None,
145            destination: None,
146            subtype_data: AnnotationSubtypeData::default(),
147            mk: None,
148            file_spec: None,
149            parent_ref: None,
150            object_id: None,
151            open: None,
152            ap_n_bytes: None,
153            ap_r_bytes: None,
154            ap_d_bytes: None,
155            irt_ref: None,
156            field_name: None,
157            alternate_name: None,
158            field_value: None,
159            form_field_flags: None,
160            additional_actions: None,
161            form_field_type: None,
162            options: None,
163        }
164    }
165
166    #[test]
167    fn test_render_annotations_empty_list() {
168        let tree = make_empty_tree();
169        let config = RenderConfig {
170            width: 100,
171            height: 100,
172            background: RgbaColor::WHITE,
173            ..RenderConfig::default()
174        };
175        let bitmap = render_with_annotations(&tree, &[], &config, None).unwrap();
176        assert_eq!(bitmap.width, 100);
177        assert_eq!(bitmap.height, 100);
178    }
179
180    #[test]
181    fn test_annotation_visibility_filtering() {
182        // Visible annotation (no flags)
183        assert!(should_render_annotation(&make_test_annotation(0)));
184
185        // Hidden annotation (bit 2)
186        assert!(!should_render_annotation(&make_test_annotation(2)));
187
188        // No-view annotation (bit 6)
189        assert!(!should_render_annotation(&make_test_annotation(32)));
190
191        // Print-only (bit 3, but not hidden)
192        assert!(should_render_annotation(&make_test_annotation(4)));
193
194        // Hidden + Print (bits 2+3)
195        assert!(!should_render_annotation(&make_test_annotation(6)));
196    }
197
198    #[test]
199    fn test_render_with_visible_annotation_overlay() {
200        let tree = make_empty_tree();
201        let overlay = AnnotationOverlay {
202            annotation: make_test_annotation(0), // visible
203            display_tree: make_empty_tree(),
204        };
205        let config = RenderConfig {
206            width: 50,
207            height: 50,
208            background: RgbaColor::WHITE,
209            ..RenderConfig::default()
210        };
211        let bitmap = render_with_annotations(&tree, &[overlay], &config, None).unwrap();
212        assert_eq!(bitmap.width, 50);
213        assert_eq!(bitmap.height, 50);
214    }
215}