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().ok()?;
259            Some(data.to_vec())
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.iter::<Stream<'_>>().flat_map(convert_single);
268
269                    let mut collected = vec![];
270
271                    for stream in streams {
272                        collected.extend(stream);
273                        // Streams must have at least one whitespace in-between.
274                        collected.push(b' ');
275                    }
276
277                    Some(collected)
278                } else {
279                    warn!("contents entry of page was neither stream nor array of streams");
280
281                    None
282                }
283            })
284            .as_ref()
285            .map(|d| d.as_slice())
286    }
287
288    /// Get the resources of the page.
289    pub fn resources(&self) -> &Resources<'a> {
290        &self.resources
291    }
292
293    /// Get the media box of the page.
294    pub fn media_box(&self) -> Rect {
295        self.media_box
296    }
297
298    /// Get the rotation of the page.
299    pub fn rotation(&self) -> Rotation {
300        self.rotation
301    }
302
303    /// Get the crop box of the page.
304    pub fn crop_box(&self) -> Rect {
305        self.crop_box
306    }
307
308    /// Return the intersection of crop box and media box.
309    pub fn intersected_crop_box(&self) -> Rect {
310        self.crop_box().intersect(self.media_box())
311    }
312
313    /// Return the base dimensions of the page (same as `intersected_crop_box`, but with special
314    /// handling applied for zero-area pages).
315    pub fn base_dimensions(&self) -> (f32, f32) {
316        let crop_box = self.intersected_crop_box();
317
318        if (crop_box.width() as f32).is_nearly_zero() || (crop_box.height() as f32).is_nearly_zero()
319        {
320            (A4.width() as f32, A4.height() as f32)
321        } else {
322            (
323                crop_box.width().max(1.0) as f32,
324                crop_box.height().max(1.0) as f32,
325            )
326        }
327    }
328
329    /// Return the with and height of the page that should be assumed when rendering the page.
330    ///
331    /// Depending on the document, it is either based on the media box or the crop box
332    /// of the page. In addition to that, it also takes the rotation of the page into account.
333    pub fn render_dimensions(&self) -> (f32, f32) {
334        let (mut base_width, mut base_height) = self.base_dimensions();
335
336        if matches!(
337            self.rotation(),
338            Rotation::Horizontal | Rotation::FlippedHorizontal
339        ) {
340            core::mem::swap(&mut base_width, &mut base_height);
341        }
342
343        (base_width, base_height)
344    }
345
346    /// Return an untyped iterator over the operators of the page's content stream.
347    pub fn operations(&self) -> UntypedIter<'_> {
348        self.operations_impl().unwrap_or(UntypedIter::empty())
349    }
350
351    /// Get the raw dictionary of the page.
352    pub fn raw(&self) -> &Dict<'a> {
353        &self.inner
354    }
355
356    /// Get the xref table (of the document the page belongs to).
357    pub fn xref(&self) -> &'a XRef {
358        self.ctx.xref()
359    }
360
361    /// Return a typed iterator over the operators of the page's content stream.
362    pub fn typed_operations(&self) -> TypedIter<'_> {
363        TypedIter::from_untyped(self.operations())
364    }
365
366    /// Return the initial transform that should be applied when rendering.
367    ///
368    /// This accounts for the mismatch between PDF's y-up and most renderers'
369    /// y-down coordinate system, the rotation of the page and the offset of
370    /// the crop box.
371    pub fn initial_transform(&self, invert_y: bool) -> Transform {
372        let crop_box = self.intersected_crop_box();
373        let (_, base_height) = self.base_dimensions();
374        let (width, height) = self.render_dimensions();
375
376        let horizontal_t = Transform::ROTATE_CW_90 * Transform::translate((0.0, -width as f64));
377        let flipped_horizontal_t =
378            Transform::translate((0.0, height as f64)) * Transform::ROTATE_CCW_90;
379
380        let rotation_transform = match self.rotation() {
381            Rotation::None => Transform::IDENTITY,
382            Rotation::Horizontal => {
383                if invert_y {
384                    horizontal_t
385                } else {
386                    flipped_horizontal_t
387                }
388            }
389            Rotation::Flipped => {
390                Transform::scale(-1.0) * Transform::translate((-width as f64, -height as f64))
391            }
392            Rotation::FlippedHorizontal => {
393                if invert_y {
394                    flipped_horizontal_t
395                } else {
396                    horizontal_t
397                }
398            }
399        };
400
401        let inversion_transform = if invert_y {
402            Transform::new([1.0, 0.0, 0.0, -1.0, 0.0, base_height as f64])
403        } else {
404            Transform::IDENTITY
405        };
406
407        rotation_transform
408            * inversion_transform
409            * Transform::translate((-crop_box.x0, -crop_box.y0))
410    }
411}
412
413/// A structure keeping track of the resources of a page.
414#[derive(Clone, Debug)]
415pub struct Resources<'a> {
416    /// The raw dictionary of external graphics states.
417    pub ext_g_states: Dict<'a>,
418    /// The raw dictionary of fonts.
419    pub fonts: Dict<'a>,
420    /// The raw dictionary of properties.
421    pub properties: Dict<'a>,
422    /// The raw dictionary of color spaces.
423    pub color_spaces: Dict<'a>,
424    /// The raw dictionary of x objects.
425    pub x_objects: Dict<'a>,
426    /// The raw dictionary of patterns.
427    pub patterns: Dict<'a>,
428    /// The raw dictionary of shadings.
429    pub shadings: Dict<'a>,
430}
431
432impl<'a> Resources<'a> {
433    /// Create a new `Resources` object.
434    pub fn new(resources: Dict<'a>) -> Self {
435        let ext_g_states = resources.get::<Dict<'_>>(EXT_G_STATE).unwrap_or_default();
436        let fonts = resources.get::<Dict<'_>>(FONT).unwrap_or_default();
437        let color_spaces = resources.get::<Dict<'_>>(COLORSPACE).unwrap_or_default();
438        let x_objects = resources.get::<Dict<'_>>(XOBJECT).unwrap_or_default();
439        let patterns = resources.get::<Dict<'_>>(PATTERN).unwrap_or_default();
440        let shadings = resources.get::<Dict<'_>>(SHADING).unwrap_or_default();
441        let properties = resources.get::<Dict<'_>>(PROPERTIES).unwrap_or_default();
442
443        Self {
444            ext_g_states,
445            fonts,
446            color_spaces,
447            properties,
448            x_objects,
449            patterns,
450            shadings,
451        }
452    }
453
454    /// Get an external graphics state by name.
455    pub fn get_ext_g_state(&self, name: &Name<'_>) -> Option<Dict<'a>> {
456        self.ext_g_states.get(name)
457    }
458
459    /// Get a color space by name.
460    pub fn get_color_space(&self, name: &Name<'_>) -> Option<Object<'a>> {
461        self.color_spaces.get(name)
462    }
463
464    /// Get a font by name.
465    pub fn get_font(&self, name: &Name<'_>) -> Option<Dict<'a>> {
466        self.fonts.get(name)
467    }
468
469    /// Get a pattern by name.
470    pub fn get_pattern(&self, name: &Name<'_>) -> Option<Object<'a>> {
471        self.patterns.get(name)
472    }
473
474    /// Get an x object by name.
475    pub fn get_x_object(&self, name: &Name<'_>) -> Option<Stream<'a>> {
476        self.x_objects.get(name)
477    }
478
479    /// Get a shading by name.
480    pub fn get_shading(&self, name: &Name<'_>) -> Option<Object<'a>> {
481        self.shadings.get(name)
482    }
483}
484
485// <https://github.com/apache/pdfbox/blob/a53a70db16ea3133994120bcf1e216b9e760c05b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/PDRectangle.java#L38>
486const POINTS_PER_INCH: f64 = 72.0;
487const POINTS_PER_MM: f64 = 1.0 / (10.0 * 2.54) * POINTS_PER_INCH;
488
489/// The dimension of an A4 page.
490pub const A4: Rect = Rect {
491    x0: 0.0,
492    y0: 0.0,
493    x1: 210.0 * POINTS_PER_MM,
494    y1: 297.0 * POINTS_PER_MM,
495};
496
497pub(crate) mod cached {
498    use crate::page::Pages;
499    use crate::reader::ReaderContext;
500    use crate::xref::XRef;
501    use core::ops::Deref;
502
503    // Keep in sync with the implementation in `sync`. We duplicate it here
504    // to make it more visible since we have unsafe code here.
505    #[cfg(feature = "std")]
506    pub(crate) use std::sync::Arc;
507
508    #[cfg(not(feature = "std"))]
509    pub(crate) use alloc::rc::Rc as Arc;
510
511    pub(crate) struct CachedPages {
512        pages: Pages<'static>,
513        // NOTE: `pages` references the data in `xref`, so it's important that `xref`
514        // appears after `pages` in the struct definition to ensure correct drop order.
515        _xref: Arc<XRef>,
516    }
517
518    impl CachedPages {
519        pub(crate) fn new(xref: Arc<XRef>) -> Option<Self> {
520            // SAFETY:
521            // - The XRef's location is stable in memory:
522            //   - We wrapped it in a `Arc` (or `Rc` in `no_std`), which implements `StableDeref`.
523            //   - The struct owns the `Arc`, ensuring that the inner value is not dropped during the whole
524            //     duration.
525            // - The internal 'static lifetime is not leaked because its rewritten
526            //   to the self-lifetime in `pages()`.
527            let xref_reference: &'static XRef = unsafe { core::mem::transmute(xref.deref()) };
528
529            let ctx = ReaderContext::new(xref_reference, false);
530            let pages = xref_reference
531                .get_with(xref.trailer_data().pages_ref, &ctx)
532                .and_then(|p| Pages::new(&p, &ctx, xref_reference))
533                .or_else(|| Pages::new_brute_force(&ctx, xref_reference))?;
534
535            Some(Self { pages, _xref: xref })
536        }
537
538        pub(crate) fn get(&self) -> &Pages<'_> {
539            &self.pages
540        }
541    }
542}