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