Skip to main content

xberg_native_pdf/document/
rect_api.rs

1//! Rectangle-scoped and rectangle-excluding extraction variants.
2//!
3//! Split out of the parent's single 18,544-line `impl PdfDocument`, which made
4//! `document.rs` 1.2 MiB and tripped the 500 KiB file-safety limit. A child module's
5//! `impl` is the same inherent impl and sees the parent's private items unchanged. ~keep
6
7use super::*;
8
9impl PdfDocument {
10    /// Extract text from a specific rectangular region of a page.
11    ///
12    /// Only spans whose bounding boxes match `region` under `mode` are kept;
13    /// the retained spans are assembled through the full text pipeline
14    /// (reading order, tables, line breaks) so the output matches the
15    /// quality of [`Self::extract_text`]. Calling this with a region that covers
16    /// the whole page is equivalent to [`Self::extract_text`].
17    pub fn extract_text_in_rect(
18        &self,
19        page_index: usize,
20        region: crate::geometry::Rect,
21        mode: crate::layout::RectFilterMode,
22    ) -> Result<String> {
23        let options = crate::converters::ConversionOptions {
24            extract_tables: true,
25            include_region: Some((region, mode)),
26            ..Default::default()
27        };
28        self.extract_text_with_options(page_index, &options)
29    }
30
31    /// Extract words from a specific rectangular region of a page.
32    pub fn extract_words_in_rect(
33        &self,
34        page_index: usize,
35        region: crate::geometry::Rect,
36        mode: crate::layout::RectFilterMode,
37    ) -> Result<Vec<crate::layout::Word>> {
38        use crate::layout::SpatialCollectionFiltering;
39        let words = self.extract_words(page_index)?;
40        Ok(words.filter_by_rect(&region, mode))
41    }
42
43    /// Extract text lines from a specific rectangular region of a page.
44    pub fn extract_text_lines_in_rect(
45        &self,
46        page_index: usize,
47        region: crate::geometry::Rect,
48        mode: crate::layout::RectFilterMode,
49    ) -> Result<Vec<crate::layout::TextLine>> {
50        use crate::layout::SpatialCollectionFiltering;
51        let lines = self.extract_text_lines(page_index)?;
52        Ok(lines.filter_by_rect(&region, mode))
53    }
54
55    /// Extract text spans from a specific rectangular region of a page.
56    pub fn extract_spans_in_rect(
57        &self,
58        page_index: usize,
59        region: crate::geometry::Rect,
60        mode: crate::layout::RectFilterMode,
61    ) -> Result<Vec<crate::layout::TextSpan>> {
62        use crate::layout::SpatialCollectionFiltering;
63        let spans = self.extract_spans(page_index)?;
64        Ok(spans.filter_by_rect(&region, mode))
65    }
66
67    /// Extract text from a page excluding specific rectangular regions.
68    ///
69    /// The excluded spans are removed before the full text-assembly pipeline
70    /// runs, so the output has the same structure — line breaks, tables,
71    /// reading order — as [`Self::extract_text`]. Calling this with an empty
72    /// `exclude` slice is equivalent to [`Self::extract_text`].
73    ///
74    /// `mode` controls the overlap rule:
75    /// - [`crate::layout::RectFilterMode::Intersects`] (default): drop any span with *any* overlap
76    /// - [`crate::layout::RectFilterMode::FullyContained`]: drop only spans lying entirely inside
77    /// - `RectFilterMode::MinOverlap(t)`: drop spans where at least fraction `t`
78    ///   of the *span's* area overlaps an excluded region
79    ///
80    /// For Tagged PDFs the extractor already honours `/Artifact` marked-content
81    /// (PDF spec §14.8.2.2). This method provides the same capability for
82    /// untagged PDFs where spatial coordinates are the only available signal.
83    /// Exclusion is unconditional: spans inside a region are dropped regardless
84    /// of their structure-tree role.
85    pub fn extract_text_excluding_rects(
86        &self,
87        page_index: usize,
88        exclude: &[crate::geometry::Rect],
89        mode: crate::layout::RectFilterMode,
90    ) -> Result<String> {
91        let options = crate::converters::ConversionOptions {
92            extract_tables: true,
93            exclude_regions: exclude.to_vec(),
94            exclude_regions_mode: mode,
95            ..Default::default()
96        };
97        self.extract_text_with_options(page_index, &options)
98    }
99
100    /// Extract words from a page excluding specific rectangular regions.
101    ///
102    /// See [`Self::extract_text_excluding_rects`] for a description of `exclude` and `mode`.
103    /// Returns the low-level [`crate::layout::Word`] stream; use [`Self::extract_text_excluding_rects`]
104    /// for fully-assembled text with line breaks and tables.
105    pub fn extract_words_excluding_rects(
106        &self,
107        page_index: usize,
108        exclude: &[crate::geometry::Rect],
109        mode: crate::layout::RectFilterMode,
110    ) -> Result<Vec<crate::layout::Word>> {
111        use crate::layout::SpatialCollectionFiltering;
112        let words = self.extract_words(page_index)?;
113        Ok(words.exclude_rects(exclude, mode))
114    }
115
116    /// Extract text spans from a page excluding specific rectangular regions.
117    ///
118    /// See [`Self::extract_text_excluding_rects`] for a description of `exclude` and `mode`.
119    /// Returns raw [`crate::layout::TextSpan`] objects with bounding boxes and font metadata;
120    /// use [`Self::extract_text_excluding_rects`] for fully-assembled text output.
121    pub fn extract_spans_excluding_rects(
122        &self,
123        page_index: usize,
124        exclude: &[crate::geometry::Rect],
125        mode: crate::layout::RectFilterMode,
126    ) -> Result<Vec<crate::layout::TextSpan>> {
127        use crate::layout::SpatialCollectionFiltering;
128        let spans = self.extract_spans(page_index)?;
129        Ok(spans.exclude_rects(exclude, mode))
130    }
131
132    /// Extract rectangles from a specific rectangular region of a page.
133    pub fn extract_rects_in_rect(
134        &self,
135        page_index: usize,
136        region: crate::geometry::Rect,
137    ) -> Result<Vec<crate::elements::PathContent>> {
138        let rects = self.extract_rects(page_index)?;
139        // Rendered extents, matching `extract_paths_in_rect`. ~keep
140        Ok(rects
141            .into_iter()
142            .filter(|p| p.rendered_bbox().intersects(&region))
143            .collect())
144    }
145
146    /// Extract straight lines from a specific rectangular region of a page.
147    pub fn extract_lines_in_rect(
148        &self,
149        page_index: usize,
150        region: crate::geometry::Rect,
151    ) -> Result<Vec<crate::elements::PathContent>> {
152        let lines = self.extract_lines(page_index)?;
153        // Rendered extents, matching `extract_paths_in_rect`: a
154        // stroke-width-encoded rule must match region queries over its drawn
155        // bar, not only over its geometric speck. ~keep
156        Ok(lines
157            .into_iter()
158            .filter(|p| p.rendered_bbox().intersects(&region))
159            .collect())
160    }
161
162    /// Extract individual characters from a specific rectangular region of a page.
163    pub fn extract_chars_in_rect(
164        &self,
165        page_index: usize,
166        region: crate::geometry::Rect,
167        mode: crate::layout::RectFilterMode,
168    ) -> Result<Vec<crate::layout::TextChar>> {
169        use crate::layout::SpatialCollectionFiltering;
170        let chars = self.extract_chars(page_index)?;
171        Ok(chars.filter_by_rect(&region, mode))
172    }
173
174    /// Extract images from a specific rectangular region of a page.
175    pub fn extract_images_in_rect(
176        &self,
177        page_index: usize,
178        region: crate::geometry::Rect,
179    ) -> Result<Vec<crate::extractors::PdfImage>> {
180        let images = self.extract_images(page_index)?;
181        Ok(images
182            .into_iter()
183            .filter(|img| {
184                if let Some(bbox) = img.bbox() {
185                    bbox.intersects(&region)
186                } else {
187                    false
188                }
189            })
190            .collect())
191    }
192
193    /// Get information about a page, including its dimensions.
194    ///
195    /// This is useful for rendering and layout calculations.
196    pub fn get_page_info(&self, page_index: usize) -> Result<PageInfo> {
197        let page = self.get_page(page_index)?;
198        let page_dict = page.as_dict().ok_or_else(|| Error::ParseError {
199            offset: 0,
200            reason: "Page is not a dictionary".to_string(),
201        })?;
202
203        fn obj_to_f32(obj: &Object) -> Option<f32> {
204            match obj {
205                Object::Integer(i) => Some(*i as f32),
206                Object::Real(r) => Some(*r as f32),
207                _ => None,
208            }
209        }
210
211        // Get MediaBox (required, may be inherited).
212        // PDF spec §7.3.10: any value may be a direct or indirect reference —
213        // including each individual array element (pdf.js issue7872 stores
214        // `/MediaBox [4 0 R 5 0 R 6 0 R 7 0 R]`). Resolve every element,
215        // otherwise an unresolved Reference reads as None and silently
216        // falls back to the Letter-size default instead of the true bounds. ~keep
217        let media_box = page_dict
218            .get("MediaBox")
219            .map(|o| self.resolve_obj_ref(o))
220            .as_ref()
221            .and_then(|o| o.as_array().map(|a| a.to_owned()))
222            .map(|arr| {
223                let r: Vec<Object> = arr.iter().map(|o| self.resolve_obj_ref(o)).collect();
224                let x0 = r.first().and_then(obj_to_f32).unwrap_or(0.0);
225                let y0 = r.get(1).and_then(obj_to_f32).unwrap_or(0.0);
226                let x1 = r.get(2).and_then(obj_to_f32).unwrap_or(612.0);
227                let y1 = r.get(3).and_then(obj_to_f32).unwrap_or(792.0);
228                crate::geometry::Rect::from_points(x0, y0, x1, y1)
229            })
230            .unwrap_or(crate::geometry::Rect::from_points(0.0, 0.0, 612.0, 792.0));
231
232        let crop_box = page_dict
233            .get("CropBox")
234            .map(|o| self.resolve_obj_ref(o))
235            .as_ref()
236            .and_then(|o| o.as_array().map(|a| a.to_owned()))
237            .map(|arr| {
238                let r: Vec<Object> = arr.iter().map(|o| self.resolve_obj_ref(o)).collect();
239                let x0 = r.first().and_then(obj_to_f32).unwrap_or(0.0);
240                let y0 = r.get(1).and_then(obj_to_f32).unwrap_or(0.0);
241                let x1 = r.get(2).and_then(obj_to_f32).unwrap_or(612.0);
242                let y1 = r.get(3).and_then(obj_to_f32).unwrap_or(792.0);
243                crate::geometry::Rect::from_points(x0, y0, x1, y1)
244            });
245
246        let rotation = page_dict
247            .get("Rotate")
248            .map(|o| self.resolve_obj_ref(o))
249            .as_ref()
250            .and_then(|o| match o {
251                Object::Integer(i) => Some(*i as i32),
252                Object::Real(r) => Some(*r as i32),
253                _ => None,
254            })
255            .unwrap_or(0);
256
257        Ok(PageInfo {
258            media_box,
259            crop_box,
260            rotation,
261        })
262    }
263
264    /// Get the resources dictionary for a page.
265    ///
266    /// Resources contain fonts, images, patterns, and other objects
267    /// used when rendering the page.
268    pub fn get_page_resources(&self, page_index: usize) -> Result<Object> {
269        let page = self.get_page(page_index)?;
270        let page_dict = page.as_dict().ok_or_else(|| Error::ParseError {
271            offset: 0,
272            reason: "Page is not a dictionary".to_string(),
273        })?;
274
275        let resources = page_dict
276            .get("Resources")
277            .cloned()
278            .unwrap_or(Object::Dictionary(std::collections::HashMap::new()));
279
280        if let Some(ref_val) = resources.as_reference() {
281            self.load_object(ref_val)
282        } else {
283            Ok(resources)
284        }
285    }
286}