Skip to main content

hayro_syntax/
page.rs

1//! Reading the pages of a PDF document.
2
3use crate::content::{TypedIter, UntypedIter};
4use crate::object::Array;
5use crate::object::Dict;
6use crate::object::Name;
7use crate::object::Rect;
8use crate::object::Stream;
9use crate::object::array::ArrayIter;
10use crate::object::dict::keys::*;
11use crate::object::{MaybeRef, Object};
12use crate::reader::ReaderContext;
13use crate::sync::{FxHashSet, OnceLock};
14use crate::transform::Transform;
15use crate::util::FloatExt;
16use crate::xref::XRef;
17use alloc::vec;
18use alloc::vec::Vec;
19use core::ops::Deref;
20
21/// Attributes that can be inherited.
22#[derive(Debug, Clone)]
23struct PagesContext<'a> {
24    media_box: Option<Rect>,
25    crop_box: Option<Rect>,
26    rotate: Option<i32>,
27    resources: Dict<'a>,
28}
29
30impl PagesContext<'_> {
31    fn new() -> Self {
32        Self {
33            media_box: None,
34            crop_box: None,
35            rotate: None,
36            resources: Dict::empty(),
37        }
38    }
39}
40
41/// A structure holding the pages of a PDF document.
42pub struct Pages<'a> {
43    pages: Vec<Page<'a>>,
44    xref: &'a XRef,
45}
46
47impl<'a> Pages<'a> {
48    /// Create a new `Pages` object.
49    pub(crate) fn new(
50        pages_dict: &Dict<'a>,
51        ctx: &ReaderContext<'a>,
52        xref: &'a XRef,
53    ) -> Option<Self> {
54        let mut pages = vec![];
55        let pages_ctx = PagesContext::new();
56        resolve_pages(pages_dict, &mut pages, pages_ctx, ctx)?;
57
58        Some(Self { pages, xref })
59    }
60
61    /// Create a new `Pages` object by bruteforce-searching.
62    ///
63    /// Of course this could result in the order of pages being messed up, but
64    /// this is still better than nothing.
65    pub(crate) fn new_brute_force(ctx: &ReaderContext<'a>, xref: &'a XRef) -> Option<Self> {
66        let mut pages = vec![];
67
68        for object in xref.objects() {
69            if let Some(dict) = object.into_dict()
70                && let Some(page) = Page::new(&dict, &PagesContext::new(), ctx, true)
71            {
72                pages.push(page);
73            }
74        }
75
76        if pages.is_empty() {
77            return None;
78        }
79
80        Some(Self { pages, xref })
81    }
82
83    /// Return the xref table (of the document the pages belong to).   
84    pub fn xref(&self) -> &'a XRef {
85        self.xref
86    }
87}
88
89impl<'a> Deref for Pages<'a> {
90    type Target = [Page<'a>];
91
92    fn deref(&self) -> &Self::Target {
93        &self.pages
94    }
95}
96
97struct PageTreeFrame<'a> {
98    kids: ArrayIter<'a>,
99    ctx: PagesContext<'a>,
100}
101
102impl<'a> PageTreeFrame<'a> {
103    fn new(dict: &Dict<'a>, mut ctx: PagesContext<'a>) -> Option<Self> {
104        ctx.media_box = dict.get::<Rect>(MEDIA_BOX).or(ctx.media_box);
105        ctx.crop_box = dict.get::<Rect>(CROP_BOX).or(ctx.crop_box);
106        ctx.rotate = dict.get::<i32>(ROTATE).or(ctx.rotate);
107        if let Some(resources) = dict.get::<Dict<'a>>(RESOURCES) {
108            ctx.resources = resources;
109        }
110
111        Some(Self {
112            kids: dict.get::<Array<'a>>(KIDS)?.raw_iter(),
113            ctx,
114        })
115    }
116}
117
118fn resolve_pages<'a>(
119    pages_dict: &Dict<'a>,
120    entries: &mut Vec<Page<'a>>,
121    ctx: PagesContext<'a>,
122    reader_ctx: &ReaderContext<'a>,
123) -> Option<()> {
124    let mut visited = FxHashSet::default();
125
126    if let Some(id) = pages_dict.obj_id() {
127        visited.insert(id);
128    }
129
130    let mut stack = vec![PageTreeFrame::new(pages_dict, ctx)?];
131
132    while let Some(frame) = stack.last_mut() {
133        let Some(kid) = frame.kids.next() else {
134            stack.pop();
135
136            continue;
137        };
138
139        let dict = match kid {
140            MaybeRef::Ref(id) => {
141                if !visited.insert(id.into()) {
142                    continue;
143                }
144
145                reader_ctx
146                    .xref()
147                    .get_with::<Dict<'a>>(id.into(), reader_ctx)
148            }
149            MaybeRef::NotRef(object) => object.into_dict(),
150        };
151
152        let Some(dict) = dict else {
153            continue;
154        };
155
156        match dict.get::<Name<'_>>(TYPE).as_deref() {
157            Some(PAGES) => {
158                if let Some(child) = PageTreeFrame::new(&dict, frame.ctx.clone()) {
159                    stack.push(child);
160                }
161            }
162            // Let's be lenient and assume it's a `Page` in case it's `None` or something else
163            // (see corpus test case 0083781).
164            _ => {
165                if let Some(page) = Page::new(&dict, &frame.ctx, reader_ctx, false) {
166                    entries.push(page);
167                }
168            }
169        }
170    }
171
172    Some(())
173}
174
175/// The rotation of the page.
176#[derive(Debug, Copy, Clone)]
177pub enum Rotation {
178    /// No rotation.
179    None,
180    /// A rotation of 90 degrees.
181    Horizontal,
182    /// A rotation of 180 degrees.
183    Flipped,
184    /// A rotation of 270 degrees.
185    FlippedHorizontal,
186}
187
188/// A PDF page.
189pub struct Page<'a> {
190    inner: Dict<'a>,
191    media_box: Rect,
192    crop_box: Rect,
193    rotation: Rotation,
194    page_streams: OnceLock<Option<Vec<u8>>>,
195    resources: Resources<'a>,
196    ctx: ReaderContext<'a>,
197}
198
199impl<'a> Page<'a> {
200    fn new(
201        dict: &Dict<'a>,
202        ctx: &PagesContext<'a>,
203        reader_ctx: &ReaderContext<'a>,
204        brute_force: bool,
205    ) -> Option<Self> {
206        // In general, pages without content are allowed, but in case we are brute-forcing
207        // we ignore them.
208        if brute_force && !dict.contains_key(CONTENTS) {
209            return None;
210        }
211
212        let media_box = dict.get::<Rect>(MEDIA_BOX).or(ctx.media_box).unwrap_or(A4);
213
214        let crop_box = dict
215            .get::<Rect>(CROP_BOX)
216            .or(ctx.crop_box)
217            .unwrap_or(media_box);
218
219        let rotation = match dict
220            .get::<i32>(ROTATE)
221            .or(ctx.rotate)
222            .unwrap_or(0)
223            .rem_euclid(360)
224        {
225            0 => Rotation::None,
226            90 => Rotation::Horizontal,
227            180 => Rotation::Flipped,
228            270 => Rotation::FlippedHorizontal,
229            _ => Rotation::None,
230        };
231
232        let resources = Resources::new(
233            dict.get::<Dict<'a>>(RESOURCES)
234                .unwrap_or_else(|| ctx.resources.clone()),
235        );
236
237        Some(Self {
238            inner: dict.clone(),
239            media_box,
240            crop_box,
241            rotation,
242            page_streams: OnceLock::new(),
243            resources,
244            ctx: reader_ctx.clone(),
245        })
246    }
247
248    fn operations_impl(&self) -> Option<UntypedIter<'_>> {
249        let stream = self.page_stream()?;
250        let iter = UntypedIter::new(stream);
251
252        Some(iter)
253    }
254
255    /// Return the decoded content stream of the page.
256    pub fn page_stream(&self) -> Option<&[u8]> {
257        let convert_single = |s: Stream<'_>| {
258            let data = s.decoded_data().ok()?;
259            Some(data.into_owned())
260        };
261
262        self.page_streams
263            .get_or_init(|| {
264                if let Some(stream) = self.inner.get::<Stream<'_>>(CONTENTS) {
265                    convert_single(stream)
266                } else if let Some(array) = self.inner.get::<Array<'_>>(CONTENTS) {
267                    let streams = array
268                        .raw_iter()
269                        .filter_map(|item| item.resolve(&self.ctx)?.into_stream())
270                        .flat_map(convert_single);
271
272                    let mut collected = vec![];
273
274                    for stream in streams {
275                        collected.extend(stream);
276                        // Streams must have at least one whitespace in-between.
277                        collected.push(b' ');
278                    }
279
280                    Some(collected)
281                } else {
282                    None
283                }
284            })
285            .as_ref()
286            .map(|d| d.as_slice())
287    }
288
289    /// Get the resources of the page.
290    pub fn resources(&self) -> &Resources<'a> {
291        &self.resources
292    }
293
294    /// Get the media box of the page.
295    pub fn media_box(&self) -> Rect {
296        self.media_box
297    }
298
299    /// Get the rotation of the page.
300    pub fn rotation(&self) -> Rotation {
301        self.rotation
302    }
303
304    /// Get the crop box of the page.
305    pub fn crop_box(&self) -> Rect {
306        self.crop_box
307    }
308
309    /// Return the intersection of crop box and media box.
310    pub fn intersected_crop_box(&self) -> Rect {
311        self.crop_box().intersect(self.media_box())
312    }
313
314    /// Return the base dimensions of the page (same as `intersected_crop_box`, but with special
315    /// handling applied for zero-area pages).
316    pub fn base_dimensions(&self) -> (f32, f32) {
317        let crop_box = self.intersected_crop_box();
318
319        if (crop_box.width() as f32).is_nearly_zero() || (crop_box.height() as f32).is_nearly_zero()
320        {
321            (A4.width() as f32, A4.height() as f32)
322        } else {
323            (
324                crop_box.width().max(1.0) as f32,
325                crop_box.height().max(1.0) as f32,
326            )
327        }
328    }
329
330    /// Return the with and height of the page that should be assumed when rendering the page.
331    ///
332    /// Depending on the document, it is either based on the media box or the crop box
333    /// of the page. In addition to that, it also takes the rotation of the page into account.
334    pub fn render_dimensions(&self) -> (f32, f32) {
335        let (mut base_width, mut base_height) = self.base_dimensions();
336
337        if matches!(
338            self.rotation(),
339            Rotation::Horizontal | Rotation::FlippedHorizontal
340        ) {
341            core::mem::swap(&mut base_width, &mut base_height);
342        }
343
344        (base_width, base_height)
345    }
346
347    /// Return an untyped iterator over the operators of the page's content stream.
348    pub fn operations(&self) -> UntypedIter<'_> {
349        self.operations_impl().unwrap_or(UntypedIter::empty())
350    }
351
352    /// Get the raw dictionary of the page.
353    pub fn raw(&self) -> &Dict<'a> {
354        &self.inner
355    }
356
357    /// Get the xref table (of the document the page belongs to).
358    pub fn xref(&self) -> &'a XRef {
359        self.ctx.xref()
360    }
361
362    /// Return a typed iterator over the operators of the page's content stream.
363    pub fn typed_operations(&self) -> TypedIter<'_> {
364        TypedIter::from_untyped(self.operations())
365    }
366
367    /// Return the initial transform that should be applied when rendering.
368    ///
369    /// This accounts for the mismatch between PDF's y-up and most renderers'
370    /// y-down coordinate system, the rotation of the page and the offset of
371    /// the crop box.
372    pub fn initial_transform(&self, invert_y: bool) -> Transform {
373        let crop_box = self.intersected_crop_box();
374        let (_, base_height) = self.base_dimensions();
375        let (width, height) = self.render_dimensions();
376
377        let horizontal_t = Transform::ROTATE_CW_90 * Transform::translate((0.0, -width as f64));
378        let flipped_horizontal_t =
379            Transform::translate((0.0, height as f64)) * Transform::ROTATE_CCW_90;
380
381        let rotation_transform = match self.rotation() {
382            Rotation::None => Transform::IDENTITY,
383            Rotation::Horizontal => {
384                if invert_y {
385                    horizontal_t
386                } else {
387                    flipped_horizontal_t
388                }
389            }
390            Rotation::Flipped => {
391                Transform::scale(-1.0) * Transform::translate((-width as f64, -height as f64))
392            }
393            Rotation::FlippedHorizontal => {
394                if invert_y {
395                    flipped_horizontal_t
396                } else {
397                    horizontal_t
398                }
399            }
400        };
401
402        let inversion_transform = if invert_y {
403            Transform::new([1.0, 0.0, 0.0, -1.0, 0.0, base_height as f64])
404        } else {
405            Transform::IDENTITY
406        };
407
408        rotation_transform
409            * inversion_transform
410            * Transform::translate((-crop_box.x0, -crop_box.y0))
411    }
412}
413
414/// A structure keeping track of the resources of a page.
415#[derive(Clone, Debug)]
416pub struct Resources<'a> {
417    /// The raw dictionary of external graphics states.
418    pub ext_g_states: Dict<'a>,
419    /// The raw dictionary of fonts.
420    pub fonts: Dict<'a>,
421    /// The raw dictionary of properties.
422    pub properties: Dict<'a>,
423    /// The raw dictionary of color spaces.
424    pub color_spaces: Dict<'a>,
425    /// The raw dictionary of x objects.
426    pub x_objects: Dict<'a>,
427    /// The raw dictionary of patterns.
428    pub patterns: Dict<'a>,
429    /// The raw dictionary of shadings.
430    pub shadings: Dict<'a>,
431}
432
433impl<'a> Resources<'a> {
434    /// Create a new `Resources` object.
435    pub fn new(resources: Dict<'a>) -> Self {
436        let ext_g_states = resources.get::<Dict<'_>>(EXT_G_STATE).unwrap_or_default();
437        let fonts = resources.get::<Dict<'_>>(FONT).unwrap_or_default();
438        let color_spaces = resources.get::<Dict<'_>>(COLORSPACE).unwrap_or_default();
439        let x_objects = resources.get::<Dict<'_>>(XOBJECT).unwrap_or_default();
440        let patterns = resources.get::<Dict<'_>>(PATTERN).unwrap_or_default();
441        let shadings = resources.get::<Dict<'_>>(SHADING).unwrap_or_default();
442        let properties = resources.get::<Dict<'_>>(PROPERTIES).unwrap_or_default();
443
444        Self {
445            ext_g_states,
446            fonts,
447            color_spaces,
448            properties,
449            x_objects,
450            patterns,
451            shadings,
452        }
453    }
454
455    /// Get an external graphics state by name.
456    pub fn get_ext_g_state(&self, name: &Name<'_>) -> Option<Dict<'a>> {
457        self.ext_g_states.get(name)
458    }
459
460    /// Get a color space by name.
461    pub fn get_color_space(&self, name: &Name<'_>) -> Option<Object<'a>> {
462        self.color_spaces.get(name)
463    }
464
465    /// Get a font by name.
466    pub fn get_font(&self, name: &Name<'_>) -> Option<Dict<'a>> {
467        self.fonts.get(name)
468    }
469
470    /// Get a pattern by name.
471    pub fn get_pattern(&self, name: &Name<'_>) -> Option<Object<'a>> {
472        self.patterns.get(name)
473    }
474
475    /// Get an x object by name.
476    pub fn get_x_object(&self, name: &Name<'_>) -> Option<Stream<'a>> {
477        self.x_objects.get(name)
478    }
479
480    /// Get a shading by name.
481    pub fn get_shading(&self, name: &Name<'_>) -> Option<Object<'a>> {
482        self.shadings.get(name)
483    }
484}
485
486// <https://github.com/apache/pdfbox/blob/a53a70db16ea3133994120bcf1e216b9e760c05b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/PDRectangle.java#L38>
487const POINTS_PER_INCH: f64 = 72.0;
488const POINTS_PER_MM: f64 = 1.0 / (10.0 * 2.54) * POINTS_PER_INCH;
489
490/// The dimension of an A4 page.
491pub const A4: Rect = Rect {
492    x0: 0.0,
493    y0: 0.0,
494    x1: 210.0 * POINTS_PER_MM,
495    y1: 297.0 * POINTS_PER_MM,
496};
497
498pub(crate) mod cached {
499    use crate::page::Pages;
500    use crate::reader::ReaderContext;
501    use crate::xref::XRef;
502    use core::ops::Deref;
503
504    // Keep in sync with the implementation in `sync`. We duplicate it here
505    // to make it more visible since we have unsafe code here.
506    #[cfg(feature = "std")]
507    pub(crate) use std::sync::Arc;
508
509    #[cfg(not(feature = "std"))]
510    pub(crate) use alloc::rc::Rc as Arc;
511
512    pub(crate) struct CachedPages {
513        pages: Pages<'static>,
514        // NOTE: `pages` references the data in `xref`, so it's important that `xref`
515        // appears after `pages` in the struct definition to ensure correct drop order.
516        _xref: Arc<XRef>,
517    }
518
519    impl CachedPages {
520        pub(crate) fn new(xref: Arc<XRef>) -> Option<Self> {
521            // SAFETY:
522            // - The XRef's location is stable in memory:
523            //   - We wrapped it in a `Arc` (or `Rc` in `no_std`), which implements `StableDeref`.
524            //   - The struct owns the `Arc`, ensuring that the inner value is not dropped during the whole
525            //     duration.
526            // - The internal 'static lifetime is not leaked because its rewritten
527            //   to the self-lifetime in `pages()`.
528            let xref_reference: &'static XRef = unsafe { core::mem::transmute(xref.deref()) };
529
530            let ctx = ReaderContext::new(xref_reference, false);
531            let pages = xref_reference
532                .get_with(xref.trailer_data().pages_ref, &ctx)
533                .and_then(|p| Pages::new(&p, &ctx, xref_reference))
534                .or_else(|| Pages::new_brute_force(&ctx, xref_reference))?;
535
536            Some(Self { pages, _xref: xref })
537        }
538
539        pub(crate) fn get(&self) -> &Pages<'_> {
540            &self.pages
541        }
542    }
543}