Skip to main content

hayro_write/
lib.rs

1/*!
2A crate for converting PDF pages into either `XObjects` or a new page via [`pdf-writer`](https://docs.rs/pdf-writer/).
3
4This is an internal crate and not meant for external use. Therefore, it's not very
5well-documented.
6*/
7
8#![forbid(unsafe_code)]
9#![deny(missing_docs)]
10
11#[macro_use]
12mod log;
13
14mod primitive;
15
16use crate::primitive::{WriteDirect, WriteIndirect};
17use flate2::Compression;
18use flate2::write::ZlibEncoder;
19use hayro_syntax::object::ObjRef;
20use hayro_syntax::object::Object;
21use hayro_syntax::object::dict::keys::{
22    COLORSPACE, EXT_G_STATE, FONT, GROUP, PATTERN, PROPERTIES, SHADING, XOBJECT,
23};
24use hayro_syntax::page::{Page, Resources, Rotation};
25use pdf_writer::{Chunk, Content, Filter, Finish, Name, Rect, Ref};
26use rustc_hash::FxHashMap;
27use std::collections::HashSet;
28use std::ops::Deref;
29use std::ops::DerefMut;
30
31pub use hayro_syntax;
32use hayro_syntax::Pdf;
33pub use pdf_writer;
34pub use pdf_writer::Settings as ChunkSettings;
35
36/// Apply the extraction queries to the given PDF and return the results.
37pub fn extract<'a>(
38    pdf: &Pdf,
39    new_ref: Box<dyn FnMut() -> Ref + 'a>,
40    chunk_settings: ChunkSettings,
41    queries: &[ExtractionQuery],
42) -> Result<ExtractionResult, ExtractionError> {
43    let pages = pdf.pages();
44    let mut ctx = ExtractionContext::new(new_ref, pdf, chunk_settings);
45
46    for query in queries {
47        let page = pages
48            .get(query.page_index)
49            .ok_or(ExtractionError::InvalidPageIndex(query.page_index))?;
50
51        let root_ref = ctx.new_ref();
52
53        let res = match query.query_type {
54            ExtractionQueryType::XObject => write_xobject(page, root_ref, &mut ctx),
55            ExtractionQueryType::Page => write_page(page, root_ref, query.page_index, &mut ctx),
56        };
57
58        ctx.root_refs.push(res.map(|_| root_ref));
59    }
60
61    // Now we have shallowly extracted all pages, now go through all dependencies until there aren't
62    // any anymore.
63    write_dependencies(pdf, &mut ctx);
64
65    let mut global_chunk = Chunk::with_settings(chunk_settings);
66
67    for chunk in &ctx.chunks {
68        global_chunk.extend(chunk);
69    }
70
71    Ok(ExtractionResult {
72        chunk: global_chunk,
73        root_refs: ctx.root_refs,
74        page_tree_parent_ref: ctx.page_tree_parent_ref,
75    })
76}
77
78/// A type of extraction query, indicating as what kind of
79/// object you want to extract the page.
80#[derive(Copy, Clone, Debug)]
81pub enum ExtractionQueryType {
82    /// Extract the page as an `XObject`.
83    XObject,
84    /// Extract the page as a new page.
85    Page,
86}
87
88/// An extraction query.
89#[derive(Copy, Clone, Debug)]
90pub struct ExtractionQuery {
91    query_type: ExtractionQueryType,
92    page_index: usize,
93}
94
95impl ExtractionQuery {
96    /// Create a new page extraction query with the given page index.
97    pub fn new_page(page_index: usize) -> Self {
98        Self {
99            query_type: ExtractionQueryType::Page,
100            page_index,
101        }
102    }
103
104    /// Create a new `XObject` extraction query with the given page index.
105    pub fn new_xobject(page_index: usize) -> Self {
106        Self {
107            query_type: ExtractionQueryType::XObject,
108            page_index,
109        }
110    }
111}
112
113/// An error that occurred during page extraction.
114#[derive(Debug, Copy, Clone)]
115pub enum ExtractionError {
116    /// An invalid page index was given.
117    InvalidPageIndex(usize),
118}
119
120/// The result of an extraction.
121#[derive(Debug)]
122pub struct ExtractionResult {
123    /// The chunk containing all objects as well as their dependencies.
124    pub chunk: Chunk,
125    /// The root references of the pages/XObject, one for each extraction query.
126    pub root_refs: Vec<Result<Ref, ExtractionError>>,
127    /// The reference to the page tree parent that was generated.
128    pub page_tree_parent_ref: Ref,
129}
130
131struct ExtractionContext<'a> {
132    chunks: Vec<Chunk>,
133    visited_objects: HashSet<ObjRef>,
134    to_visit_refs: Vec<ObjRef>,
135    valid_ref_cache: FxHashMap<ObjRef, bool>,
136    root_refs: Vec<Result<Ref, ExtractionError>>,
137    pdf: &'a Pdf,
138    new_ref: Box<dyn FnMut() -> Ref + 'a>,
139    ref_map: FxHashMap<ObjRef, Ref>,
140    cached_content_streams: FxHashMap<usize, Ref>,
141    page_tree_parent_ref: Ref,
142    chunk_settings: ChunkSettings,
143}
144
145impl<'a> ExtractionContext<'a> {
146    fn new(
147        mut new_ref: Box<dyn FnMut() -> Ref + 'a>,
148        pdf: &'a Pdf,
149        chunk_settings: ChunkSettings,
150    ) -> Self {
151        let page_tree_parent_ref = new_ref();
152        Self {
153            chunks: vec![],
154            visited_objects: HashSet::new(),
155            to_visit_refs: Vec::new(),
156            valid_ref_cache: FxHashMap::default(),
157            pdf,
158            new_ref,
159            ref_map: FxHashMap::default(),
160            cached_content_streams: FxHashMap::default(),
161            root_refs: Vec::new(),
162            page_tree_parent_ref,
163            chunk_settings,
164        }
165    }
166
167    pub(crate) fn map_ref(&mut self, ref_: ObjRef) -> Ref {
168        if let Some(ref_) = self.ref_map.get(&ref_) {
169            *ref_
170        } else {
171            let new_ref = self.new_ref();
172            self.ref_map.insert(ref_, new_ref);
173
174            new_ref
175        }
176    }
177
178    pub(crate) fn new_ref(&mut self) -> Ref {
179        (self.new_ref)()
180    }
181}
182
183fn write_dependencies(pdf: &Pdf, ctx: &mut ExtractionContext<'_>) {
184    while let Some(ref_) = ctx.to_visit_refs.pop() {
185        // Don't visit objects twice!
186        if ctx.visited_objects.contains(&ref_) {
187            continue;
188        }
189
190        let mut chunk = Chunk::with_settings(ctx.chunk_settings);
191        if let Some(object) = pdf.xref().get::<Object<'_>>(ref_.into()) {
192            let new_ref = ctx.map_ref(ref_);
193            object.write_indirect(&mut chunk, new_ref, ctx);
194            ctx.chunks.push(chunk);
195
196            ctx.visited_objects.insert(ref_);
197        } else {
198            warn!("failed to extract object with ref: {ref_:?}");
199        }
200    }
201}
202
203/// Extract the given pages from the PDF and resave them as a new PDF. This function shouldn't be
204/// used directly and only exists for test purposes.
205#[doc(hidden)]
206pub fn extract_pages_to_pdf(hayro_pdf: &Pdf, page_indices: &[usize]) -> Vec<u8> {
207    let mut pdf = pdf_writer::Pdf::new();
208    let mut next_ref = Ref::new(1);
209    let requests = page_indices
210        .iter()
211        .map(|i| ExtractionQuery {
212            query_type: ExtractionQueryType::Page,
213            page_index: *i,
214        })
215        .collect::<Vec<_>>();
216
217    let catalog_id = next_ref.bump();
218
219    let extracted = extract(
220        hayro_pdf,
221        Box::new(|| next_ref.bump()),
222        ChunkSettings::default(),
223        &requests,
224    )
225    .unwrap();
226    pdf.catalog(catalog_id)
227        .pages(extracted.page_tree_parent_ref);
228    let count = extracted.root_refs.len();
229    pdf.pages(extracted.page_tree_parent_ref)
230        .kids(extracted.root_refs.iter().map(|r| r.unwrap()))
231        .count(count as i32);
232    pdf.extend(&extracted.chunk);
233
234    pdf.finish()
235}
236
237/// Extract the given pages as XObjects from the PDF and resave them as a new PDF.
238/// This function shouldn't be used directly and only exists for test purposes.
239#[doc(hidden)]
240pub fn extract_pages_as_xobject_to_pdf(hayro_pdf: &Pdf, page_indices: &[usize]) -> Vec<u8> {
241    let hayro_pages = hayro_pdf.pages();
242    let page_list = hayro_pages.as_ref();
243
244    let mut pdf = pdf_writer::Pdf::new();
245    let mut next_ref = Ref::new(1);
246
247    let catalog_id = next_ref.bump();
248    let requests = page_indices
249        .iter()
250        .map(|i| ExtractionQuery {
251            query_type: ExtractionQueryType::XObject,
252            page_index: *i,
253        })
254        .collect::<Vec<_>>();
255
256    let extracted = extract(
257        hayro_pdf,
258        Box::new(|| next_ref.bump()),
259        ChunkSettings::default(),
260        &requests,
261    )
262    .unwrap();
263
264    pdf.catalog(catalog_id)
265        .pages(extracted.page_tree_parent_ref);
266    let mut page_refs = vec![];
267
268    for (x_object_ref, page_idx) in extracted.root_refs.iter().zip(page_indices) {
269        let page = &page_list[*page_idx];
270        let render_dimensions = page.render_dimensions();
271
272        let mut content = Content::new();
273        content.x_object(Name(b"O1"));
274
275        let finished = content.finish();
276
277        let page_id = next_ref.bump();
278        let stream_id = next_ref.bump();
279        page_refs.push(page_id);
280
281        let mut page = pdf.page(page_id);
282        page.resources()
283            .x_objects()
284            .pair(Name(b"O1"), x_object_ref.unwrap());
285        page.media_box(Rect::new(
286            0.0,
287            0.0,
288            render_dimensions.0,
289            render_dimensions.1,
290        ));
291        page.parent(extracted.page_tree_parent_ref);
292        page.contents(stream_id);
293        page.finish();
294
295        pdf.stream(stream_id, finished.as_slice());
296    }
297
298    let count = extracted.root_refs.len();
299    pdf.pages(extracted.page_tree_parent_ref)
300        .kids(page_refs)
301        .count(count as i32);
302    pdf.extend(&extracted.chunk);
303
304    pdf.finish()
305}
306
307fn write_page(
308    page: &Page<'_>,
309    page_ref: Ref,
310    page_idx: usize,
311    ctx: &mut ExtractionContext<'_>,
312) -> Result<(), ExtractionError> {
313    let mut chunk = Chunk::with_settings(ctx.chunk_settings);
314    // Note: We can cache content stream references, but _not_ the page references themselves.
315    // Acrobat for some reason doesn't like duplicate page references in the page tree.
316    let stream_ref = if let Some(cached) = ctx.cached_content_streams.get(&page_idx) {
317        *cached
318    } else {
319        let stream_ref = ctx.new_ref();
320
321        chunk
322            .stream(
323                stream_ref,
324                &deflate_encode(page.page_stream().unwrap_or(b"")),
325            )
326            .filter(Filter::FlateDecode);
327        ctx.cached_content_streams.insert(page_idx, stream_ref);
328
329        stream_ref
330    };
331
332    let mut pdf_page = chunk.page(page_ref);
333
334    pdf_page
335        .media_box(convert_rect(&page.media_box()))
336        .crop_box(convert_rect(&page.crop_box()))
337        .rotate(match page.rotation() {
338            Rotation::None => 0,
339            Rotation::Horizontal => 90,
340            Rotation::Flipped => 180,
341            Rotation::FlippedHorizontal => 270,
342        })
343        .parent(ctx.page_tree_parent_ref)
344        .contents(stream_ref);
345
346    let raw_dict = page.raw();
347
348    if let Some(group) = raw_dict.get_raw::<Object<'_>>(GROUP) {
349        group.write_direct(pdf_page.insert(Name(GROUP)), ctx);
350    }
351
352    serialize_resources(page.resources(), ctx, &mut pdf_page);
353
354    pdf_page.finish();
355
356    ctx.chunks.push(chunk);
357
358    Ok(())
359}
360
361fn write_xobject(
362    page: &Page<'_>,
363    xobj_ref: Ref,
364    ctx: &mut ExtractionContext<'_>,
365) -> Result<(), ExtractionError> {
366    let mut chunk = Chunk::with_settings(ctx.chunk_settings);
367    let encoded_stream = deflate_encode(page.page_stream().unwrap_or(b""));
368    let mut x_object = chunk.form_xobject(xobj_ref, &encoded_stream);
369    x_object.deref_mut().filter(Filter::FlateDecode);
370
371    let bbox = page.crop_box();
372    let initial_transform = page.initial_transform(false);
373
374    x_object.bbox(Rect::new(
375        bbox.x0 as f32,
376        bbox.y0 as f32,
377        bbox.x1 as f32,
378        bbox.y1 as f32,
379    ));
380
381    let i = initial_transform.as_coeffs();
382    x_object.matrix([
383        i[0] as f32,
384        i[1] as f32,
385        i[2] as f32,
386        i[3] as f32,
387        i[4] as f32,
388        i[5] as f32,
389    ]);
390
391    serialize_resources(page.resources(), ctx, &mut x_object);
392
393    let raw_dict = page.raw();
394
395    if let Some(group) = raw_dict.get_raw::<Object<'_>>(GROUP) {
396        group.write_direct(x_object.insert(Name(GROUP)), ctx);
397    }
398
399    x_object.finish();
400    ctx.chunks.push(chunk);
401
402    Ok(())
403}
404
405fn serialize_resources(
406    resources: &Resources<'_>,
407    ctx: &mut ExtractionContext<'_>,
408    writer: &mut impl ResourcesExt,
409) {
410    // Resource dictionary is always required (unless it can be inherited), so
411    // let's just be safe and always write it.
412    let mut output = writer.resources();
413
414    macro_rules! write {
415        ($name:ident, $key:expr) => {
416            if !resources.$name.is_empty() {
417                let mut dict = output.insert(Name($key)).dict();
418
419                for (name, obj) in resources.$name.entries() {
420                    obj.write_direct(dict.insert(Name(name.deref())), ctx);
421                }
422            }
423        };
424    }
425
426    write!(ext_g_states, EXT_G_STATE);
427    write!(shadings, SHADING);
428    write!(patterns, PATTERN);
429    write!(x_objects, XOBJECT);
430    write!(color_spaces, COLORSPACE);
431    write!(fonts, FONT);
432    write!(properties, PROPERTIES);
433}
434
435pub(crate) fn deflate_encode(data: &[u8]) -> Vec<u8> {
436    use std::io::Write;
437
438    const COMPRESSION_LEVEL: u8 = 6;
439    let mut e = ZlibEncoder::new(Vec::new(), Compression::new(COMPRESSION_LEVEL as u32));
440    e.write_all(data).unwrap();
441    e.finish().unwrap()
442}
443
444fn convert_rect(hy_rect: &hayro_syntax::object::Rect) -> Rect {
445    Rect::new(
446        hy_rect.x0 as f32,
447        hy_rect.y0 as f32,
448        hy_rect.x1 as f32,
449        hy_rect.y1 as f32,
450    )
451}
452
453trait ResourcesExt {
454    fn resources(&mut self) -> pdf_writer::writers::Resources<'_>;
455}
456
457impl ResourcesExt for pdf_writer::writers::Page<'_> {
458    fn resources(&mut self) -> pdf_writer::writers::Resources<'_> {
459        Self::resources(self)
460    }
461}
462
463impl ResourcesExt for pdf_writer::writers::FormXObject<'_> {
464    fn resources(&mut self) -> pdf_writer::writers::Resources<'_> {
465        Self::resources(self)
466    }
467}