printpdf 0.5.3

Rust library for writing PDF files
Documentation
//! A `PDFDocument` represents the whole content of the file

use std::cell::RefCell;
use std::collections::HashMap;
use std::io::BufWriter;
use std::io::Write;
use std::rc::Rc;
use utils::random_character_string_32;

use crate::OffsetDateTime;
use lopdf;

use indices::*;
use {
    ExternalFont, Font, PdfPage, FontList, IccProfileList, PdfMetadata, PdfConformance, IndirectFontRef,
    DirectFontRef, BuiltinFont, PdfPageReference, Error, Mm, FontData
};

/// PDF document
#[derive(Debug, Clone)]
pub struct PdfDocument {
    /// Pages of the document
    pub(super) pages: Vec<PdfPage>,
    /// Fonts used in this document
    pub fonts: FontList,
    /// ICC profiles used in the document
    pub(super) icc_profiles: IccProfileList,
    /// Inner PDF document
    pub(super) inner_doc: lopdf::Document,
    /// Document ID. Must be changed if the document is loaded / parsed from a file
    pub document_id: String,
    /// Metadata for this document
    pub metadata: PdfMetadata,
    /// The bookmarks in the document. A HashMap<Page Number, Bookmark Name>
    pub bookmarks: HashMap<usize, String>,
}

/// Marker struct for a document. Used to make the API a bit nicer.
/// It simply calls `PdfDocument` functions.
pub struct PdfDocumentReference {
    /// A wrapper for a document, so actions from outside this library
    /// are restricted to functions inside this crate (only functions in `lopdf`
    /// can directly manipulate the document)
    pub(crate) document: Rc<RefCell<PdfDocument>>,
}

impl PdfDocument {

    /// Creates a new PDF document
    #[inline]
    #[cfg_attr(feature = "cargo-clippy", allow(new_ret_no_self))]
    pub fn new<S1, S2>(
        document_title: S1,
        initial_page_width: Mm,
        initial_page_height: Mm,
        initial_layer_name: S2,
    ) -> (PdfDocumentReference, PdfPageIndex, PdfLayerIndex)
    where S1: Into<String>, S2: Into<String>
    {
        let doc = Self {
            pages: Vec::new(),
            document_id: random_character_string_32(),
            fonts: FontList::new(),
            icc_profiles: IccProfileList::new(),
            inner_doc: lopdf::Document::with_version("1.3"),
            metadata: PdfMetadata::new(document_title, 1, false, PdfConformance::default()),
            bookmarks: HashMap::new(),
        };

        let doc_ref = Rc::new(RefCell::new(doc));

        let (initial_page, layer_index) = PdfPage::new(
            initial_page_width,
            initial_page_height,
            initial_layer_name,
            0);

        { doc_ref.borrow_mut().pages.push(initial_page); }

        (PdfDocumentReference { document: doc_ref }, PdfPageIndex(0), layer_index)
    }

    pub fn empty<S: Into<String>>(document_title: S) -> PdfDocumentReference {
        let doc = Self {
            pages: Vec::new(),
            document_id: random_character_string_32(),
            fonts: FontList::new(),
            icc_profiles: IccProfileList::new(),
            inner_doc: lopdf::Document::with_version("1.3"),
            metadata: PdfMetadata::new(document_title, 1, false, PdfConformance::X3_2002_PDF_1_3),
            bookmarks: HashMap::new(),
        };

        let doc_ref = Rc::new(RefCell::new(doc));
        PdfDocumentReference { document: doc_ref }
    }
}

macro_rules! implement_adding_fonts {
    ($self:expr, $font_name:expr, $font:expr) => {{
        let font_ref;

        let possible_ref = {
            let doc = $self.document.borrow();
            font_ref = IndirectFontRef::new($font_name);
            match doc.fonts.get_font(&font_ref) {
                Some(f) => Some(f.clone()),
                None => None,
            }
        };

        if possible_ref.is_some() {
            Ok(font_ref)
        } else {
            let mut doc = $self.document.borrow_mut();
            let direct_ref = DirectFontRef {
                inner_obj: doc.inner_doc.new_object_id(),
                data: $font
            };

            doc.fonts.add_font(font_ref.clone(), direct_ref);
            Ok(font_ref)
        }
    }}
}

impl PdfDocumentReference {

    // ----- BUILDER FUNCTIONS

    /// Changes the title on both the document info dictionary as well as the metadata
    #[inline]
    pub fn with_title<S>(self, new_title: S)
    -> Self where S: Into<String>
    {
        self.document.borrow_mut().metadata.document_title = new_title.into();
        self
    }
    
    /// Changes the author metadata property on both the document info dictionary as well as the metadata
    #[inline]
    pub fn with_author<S>(self, author: S)
    -> Self where S: Into<String>
    {
        self.document.borrow_mut().metadata.author = author.into();
        self
    }
    
    /// Changes the creator metadata property on both the document info dictionary as well as the metadata
    #[inline]
    pub fn with_creator<S>(self, creator: S)
    -> Self where S: Into<String>
    {
        self.document.borrow_mut().metadata.creator = creator.into();
        self
    }
     
    /// Changes the producer/publisher metadata property on both the document info dictionary as well as the metadata
    #[inline]
    pub fn with_producer<S>(self, producer: S)
    -> Self where S: Into<String>
    {
        self.document.borrow_mut().metadata.producer = producer.into();
        self
    }
    
    /// Changes the keywords metadata property on both the document info dictionary as well as the metadata
    #[inline]
    pub fn with_keywords<S>(self, keywords: Vec<S>)
    -> Self where S: Into<String>
    {
        self.document.borrow_mut().metadata.keywords = keywords.into_iter().map(|s| s.into()).collect();
        self
    }

    /// Changes the subject metadata property on both the document info dictionary as well as the metadata
    #[inline]
    pub fn with_subject<S>(self, subject: S)
    -> Self where S: Into<String>
    {
        self.document.borrow_mut().metadata.subject = subject.into();
        self
    }
    
    /// Changes the subject metadata property on both the document info dictionary as well as the metadata
    #[inline]
    pub fn with_identifier<S>(self, identifier: S)
    -> Self where S: Into<String>
    {
        self.document.borrow_mut().metadata.identifier = identifier.into();
        self
    }

    /// Set the trapping of the document
    #[inline]
    pub fn with_trapping(self, trapping: bool)
    -> Self
    {
        self.document.borrow_mut().metadata.trapping = trapping;
        self
    }

    /// Sets the document ID (for comparing two PDF documents for equality)
    #[inline]
    pub fn with_document_id(self, id: String)
    -> Self
    {
        self.document.borrow_mut().metadata.xmp_metadata.document_id = id;
        self
    }

    /// Set the version of the document
    #[inline]
    pub fn with_document_version(self, version: u32)
    -> Self
    {
        self.document.borrow_mut().metadata.document_version = version;
        self
    }

    /// Changes the conformance of this document. It is recommended to call
    /// `check_for_errors()` after changing it.
    #[inline]
    pub fn with_conformance(self, conformance: PdfConformance)
    -> Self
    {
        self.document.borrow_mut().metadata.conformance = conformance;
        self
    }

    /// Sets the creation date on the document.
    ///
    /// Per default, the creation date is set to the current time.
    #[inline]
    pub fn with_creation_date(self, creation_date: OffsetDateTime)
    -> Self
    {
        self.document.borrow_mut().metadata.creation_date = creation_date;
        self
    }

    /// Sets the metadata date on the document.
    ///
    /// By default, the metadata date is set to the current time.
    #[inline]
    pub fn with_metadata_date(self, metadata_date: OffsetDateTime)
    -> Self
    {
        self.document.borrow_mut().metadata.metadata_date = metadata_date;
        self
    }

    /// Sets the modification date on the document. Intended to be used when
    /// reading documents that already have a modification date.
    #[inline]
    pub fn with_mod_date(self, mod_date: OffsetDateTime)
    -> Self
    {
        self.document.borrow_mut().metadata.modification_date = mod_date;
        self
    }

    // ----- ADD FUNCTIONS

    /// Create a new pdf page and returns the index of the page
    #[inline]
    pub fn add_page<S>(
        &self,
        x_mm: Mm,
        y_mm: Mm,
        inital_layer_name: S,
    ) -> (PdfPageIndex, PdfLayerIndex)
    where
        S: Into<String>,
    {
        let mut doc = self.document.borrow_mut();
        let (pdf_page, pdf_layer_index) =
            PdfPage::new(x_mm, y_mm, inital_layer_name, doc.pages.len());
        doc.pages.push(pdf_page);
        let page_index = PdfPageIndex(doc.pages.len() - 1);
        (page_index, pdf_layer_index)
    }
    /// Create a new pdf page and returns the index of the page.
    /// If the page already has a bookmark, overwrites it.
    #[inline]
    pub fn add_bookmark<S>(&self, name: S, page: PdfPageIndex)
    where
        S: Into<String>,
    {
        let mut doc = self.document.borrow_mut();
        doc.bookmarks.insert(page.0, name.into());
    }

    /// Add a font from a font stream
    pub fn add_external_font<R>(
        &self,
        font_stream: R,
    ) -> ::std::result::Result<IndirectFontRef, Error>
    where
        R: ::std::io::Read,
    {
        let last_font_index = {
            let doc = self.document.borrow();
            doc.fonts.len()
        };
        let external_font = ExternalFont::new(font_stream, last_font_index)?;
        let external_font_name = external_font.face_name.clone();
        let font = Font::ExternalFont(external_font);
        implement_adding_fonts!(&self, external_font_name, font)
    }

    /// Add a font from a custom font backend
    pub fn add_external_font_data<F>(&self, bytes: Vec<u8>, data: F)
    -> ::std::result::Result<IndirectFontRef, Error>
    where
        F: FontData + 'static,
    {
        let last_font_index = { let doc = self.document.borrow(); doc.fonts.len() };
        let external_font = ExternalFont::with_font_data(bytes, last_font_index, Box::new(data));
        let external_font_name = external_font.face_name.clone();
        let font = Font::ExternalFont(external_font);
        implement_adding_fonts!(&self, external_font_name, font)
    }

    /// Add a built-in font to the document
    ///
    /// Built-in fonts can only be used to print characters that are supported by the
    /// [Windows-1252][] encoding.  All other characters will be ignored.
    ///
    /// [Windows-1252]: https://en.wikipedia.org/wiki/Windows-1252
    pub fn add_builtin_font(&self, builtin_font: BuiltinFont)
    -> ::std::result::Result<IndirectFontRef, Error>
    {
        let builtin_font_name: &'static str = builtin_font.clone().into();
        implement_adding_fonts!(&self, builtin_font_name, Font::BuiltinFont(builtin_font))
    }

    // ----- GET FUNCTIONS

    /// Returns the page (for inserting content)
    #[inline]
    #[cfg_attr(feature = "cargo-clippy", allow(unnecessary_operation))]
    pub fn get_page(&self, page: PdfPageIndex)
    -> PdfPageReference
    {
        &self.document.borrow_mut().pages[page.0];
        PdfPageReference { document: Rc::downgrade(&self.document).clone(), page }
    }

    /// Returns a direct reference (object ID) to the font from an
    /// indirect reference (postscript name)
    #[inline]
    pub fn get_font(&self, font: &IndirectFontRef)
    -> Option<DirectFontRef>
    {
        let doc = self.document.borrow();
        doc.fonts.get_font(font)
    }

    /// Drops the PDFDocument, returning the inner `lopdf::Document`.
    /// Document may be only half-written, use only in extreme cases
    #[inline]
    pub unsafe fn get_inner(self)
    -> lopdf::Document
    {
        let doc = Rc::try_unwrap(self.document).unwrap().into_inner();
        doc.inner_doc
    }

    // --- MISC FUNCTIONS

    /// Checks for invalid settings in the document
    pub fn check_for_errors(&self)
    -> ::std::result::Result<(), Error>
    {
        // TODO
        #[cfg(feature = "logging")] {
            warn!("Checking PDFs for errors is currently not supported!");
        }

        Ok(())
    }

    /// Tries to match the document to the given conformance.
    /// Errors only on an unrecoverable error.
    pub fn repair_errors(&self, _conformance: PdfConformance)
    -> ::std::result::Result<(), Error>
    {
        // TODO
        #[cfg(feature = "logging")] {
            warn!("Reparing PDFs is currently not supported!");
        }

        Ok(())
    }

    /// Save PDF document to bytes
    #[allow(unused_qualifications)]
    pub fn save_to_bytes(self) -> Result<Vec<u8>, Error> {

        use lopdf::Object::*;
        use lopdf::StringFormat::Literal;
        use lopdf::{Dictionary as LoDictionary, Object as LoObject};
        use std::iter::FromIterator;
        use std::mem;

        // todo: remove unwrap, handle error
        let mut doc = Rc::try_unwrap(self.document).unwrap().into_inner();
        let pages_id = doc.inner_doc.new_object_id();
        let bookmarks_id = doc.inner_doc.new_object_id();
        let mut bookmarks_list = LoDictionary::from_iter(vec![
            ("Type", "Outlines".into()),
            ("Count", Integer(doc.bookmarks.len() as i64)),
            /* First and Last will be filled in once they are created from the pages */
        ]);

        // extra pdf infos
        let (xmp_metadata, document_info, icc_profile) = doc.metadata.clone().into_obj();

        let xmp_metadata_id = match xmp_metadata {
            Some(metadata) => Some(doc.inner_doc.add_object(metadata)),
            None => None,
        };

        let document_info_id = doc.inner_doc.add_object(document_info);

        // add catalog
        let icc_profile_descr = "Commercial and special offset print acccording to ISO \
                                 12647-2:2004 / Amd 1, paper type 1 or 2 (matte or gloss-coated \
                                 offset paper, 115 g/m2), screen ruling 60/cm";
        let icc_profile_str = "Coated FOGRA39 (ISO 12647-2:2004)";
        let icc_profile_short = "FOGRA39";

        let mut output_intents = LoDictionary::from_iter(vec![
            ("S", Name("GTS_PDFX".into())),
            ("OutputCondition", String(icc_profile_descr.into(), Literal)),
            ("Type", Name("OutputIntent".into())),
            (
                "OutputConditionIdentifier",
                String(icc_profile_short.into(), Literal),
            ),
            (
                "RegistryName",
                String("http://www.color.org".into(), Literal),
            ),
            ("Info", String(icc_profile_str.into(), Literal)),
        ]);

        let mut catalog = LoDictionary::from_iter(vec![
            ("Type", "Catalog".into()),
            ("PageLayout", "OneColumn".into()),
            (
                "PageMode",
                if doc.bookmarks.len() > 0 {
                    "UseOutlines"
                } else {
                    "UseNone"
                }
                .into(),
            ),
            ("Outlines", Reference(bookmarks_id)),
            ("Pages", Reference(pages_id)),
        ]);

        if let Some(profile) = icc_profile {
            let icc_profile: lopdf::Stream = profile.into();
            let icc_profile_id = doc.inner_doc.add_object(Stream(icc_profile));
            output_intents.set("DestinationOutputProfile", Reference(icc_profile_id));
            catalog.set("OutputIntents", Array(vec![Dictionary(output_intents)]));
        }

        if let Some(metadata_id) = xmp_metadata_id {
            catalog.set("Metadata", Reference(metadata_id));
        }

        let mut pages = LoDictionary::from_iter(vec![
                      ("Type", "Pages".into()),
                      ("Count", Integer(doc.pages.len() as i64)),
                      /* Kids and Resources missing */
                      ]);

        // add all pages with contents
        let mut page_ids = Vec::<LoObject>::new();

        // ----- OCG CONTENT

        // page index + page names to add the OCG to the /Catalog
        let page_layer_names: Vec<(usize, Vec<::std::string::String>)> =
            doc.pages.iter().map(|page|
                (page.index, page.layers.iter().map(|layer|
                    layer.name.clone()).collect()
            )).collect();

        // add optional content groups (layers) to the /Catalog
        let usage_ocg_dict = LoDictionary::from_iter(vec![
            ("Type", Name("OCG".into())),
            ("CreatorInfo", Dictionary(LoDictionary::from_iter(vec![
                ("Creator", String("Adobe Illustrator 14.0".into(), Literal)),
                ("Subtype", Name("Artwork".into()))
            ]))),
        ]);

        let usage_ocg_dict_ref = doc.inner_doc.add_object(Dictionary(usage_ocg_dict));

        let intent_arr = Array(vec![
            Name("View".into()),
            Name("Design".into()),
        ]);

        let intent_arr_ref = doc.inner_doc.add_object(intent_arr);

        // page index, layer index, reference to OCG dictionary
        let ocg_list: Vec<(usize, Vec<(usize, lopdf::Object)>)> =

        page_layer_names.into_iter().map(|(page_idx, layer_names)|
            (page_idx,
            layer_names.into_iter().enumerate().map(|(layer_idx, layer_name)|
                (layer_idx,
                Reference(doc.inner_doc.add_object(
                    Dictionary(LoDictionary::from_iter(vec![
                        ("Type", Name("OCG".into())),
                        ("Name", String(layer_name.into(), Literal)),
                        ("Intent", Reference(intent_arr_ref)),
                        ("Usage", Reference(usage_ocg_dict_ref))
                    ]))
                )))
            ).collect()))
        .collect();

        let flattened_ocg_list: Vec<lopdf::Object> =
            ocg_list.iter().flat_map(|&(_, ref layers)|
                layers.iter().map(|&(_, ref obj)| obj.clone())
            ).collect();

        catalog.set("OCProperties", Dictionary(LoDictionary::from_iter(vec![
            ("OCGs", Array(flattened_ocg_list.clone())),
            // optional content configuration dictionary, page 376
            ("D", Dictionary(LoDictionary::from_iter(vec![
                ("Order", Array(flattened_ocg_list.clone())),
                // "radio button groups"
                ("RBGroups", Array(vec![])),
                // initially visible OCG
                ("ON", Array(flattened_ocg_list)),
            ])))
        ])));

        // ----- END OCG CONTENT (on document level)

        // ----- PAGE CONTENT

        // add fonts (shared resources)
        let mut font_dict_id = None;

        // add all fonts / other resources shared in the whole document
        let fonts_dict: lopdf::Dictionary = doc.fonts.into_with_document(&mut doc.inner_doc);

        if fonts_dict.len() > 0 {
            font_dict_id = Some(doc.inner_doc.add_object(Dictionary(fonts_dict)));
        }

        let mut page_id_to_obj: HashMap<usize, (u32, u16)> = HashMap::new();

        for (idx, page) in doc.pages.into_iter().enumerate() {
            let mut p = LoDictionary::from_iter(vec![
                ("Type", "Page".into()),
                ("Rotate", Integer(0)),
                (
                    "MediaBox",
                    vec![0.into(), 0.into(), page.width.into(), page.height.into()].into(),
                ),
                (
                    "TrimBox",
                    vec![0.into(), 0.into(), page.width.into(), page.height.into()].into(),
                ),
                (
                    "CropBox",
                    vec![0.into(), 0.into(), page.width.into(), page.height.into()].into(),
                ),
                ("Parent", Reference(pages_id)),
            ]);

            // this will collect the resources needed for rendering this page
            let layers_temp = ocg_list.iter().find(|e| e.0 == idx).unwrap();
            let (mut resources_page, layer_streams) =
                page.collect_resources_and_streams(&mut doc.inner_doc, &layers_temp.1);

            if let Some(f) = font_dict_id {
                resources_page.set("Font", Reference(f));
            }

            if resources_page.len() > 0 {
                let resources_page_id = doc.inner_doc.add_object(Dictionary(resources_page));
                p.set("Resources", Reference(resources_page_id));
            }

            // merge all streams of the individual layers into one big stream
            let mut layer_streams_merged_vec = Vec::<u8>::new();
            for mut stream in layer_streams {
                layer_streams_merged_vec.append(&mut stream.content);
            }

            let merged_layer_stream = lopdf::Stream::new(lopdf::Dictionary::new(), layer_streams_merged_vec);
            let page_content_id = doc.inner_doc.add_object(merged_layer_stream);

            p.set("Contents", Reference(page_content_id));
            let page_obj = doc.inner_doc.add_object(p);
            if doc.bookmarks.len() > 0 {
                if let Some(_) = doc.bookmarks.get(&idx) {
                    page_id_to_obj.insert(idx, page_obj);
                }
            }
            page_ids.push(Reference(page_obj))
        }

        if doc.bookmarks.len() > 0 {
            let len = doc.bookmarks.len();
            if len == 1 {
                let page_index = doc.bookmarks.iter().next().unwrap().0.to_owned();
                let title = doc.bookmarks.iter().next().unwrap().1.to_owned();
                let obj_ref = doc
                    .inner_doc
                    .add_object(Dictionary(LoDictionary::from_iter(vec![
                        ("Parent", Reference(bookmarks_id)),
                        ("Title", String(title.into(), Literal)),
                        (
                            "Dest",
                            Array(vec![
                                Reference(page_id_to_obj.get(&page_index).unwrap().to_owned()),
                                "XYZ".into(),
                                Null,
                                Null,
                                Null,
                            ]),
                        ),
                    ])));
                bookmarks_list.set("First", Reference(obj_ref));
                bookmarks_list.set("Last", Reference(obj_ref));
            } else {
                let mut sorted_bmarks: Vec<(&usize, &std::string::String)> = doc.bookmarks.iter().collect();
                sorted_bmarks.sort();
                for (i, (page_index, b_name)) in sorted_bmarks.iter().enumerate() {
                    let dest = (
                        "Dest",
                        Array(vec![
                            Reference(page_id_to_obj.get(page_index).unwrap().to_owned()),
                                "XYZ".into(),
                                Null,
                                Null,
                                Null,
                        ]),
                    );
                    doc.inner_doc
                        .add_object(Dictionary(LoDictionary::from_iter(if i == 0 {
                            bookmarks_list.set("First", Reference((doc.inner_doc.max_id + 1, 0)));
                            vec![
                                ("Parent", Reference(bookmarks_id)),
                                ("Title", String(b_name.to_owned().to_owned().into(), Literal)),
                                ("Next", Reference((doc.inner_doc.max_id + 2, 0))),
                                dest,
                            ]
                        } else if i == len - 1 {
                            bookmarks_list.set("Last", Reference((doc.inner_doc.max_id + 1, 0)));
                            vec![
                                ("Parent", Reference(bookmarks_id)),
                                ("Title", String(b_name.to_owned().to_owned().into(), Literal)),
                                ("Prev", Reference((doc.inner_doc.max_id, 0))),
                                dest,
                            ]
                        } else {
                            vec![
                                ("Parent", Reference(bookmarks_id)),
                                ("Title", String(b_name.to_owned().to_owned().into(), Literal)),
                                ("Prev", Reference((doc.inner_doc.max_id, 0))),
                                ("Next", Reference((doc.inner_doc.max_id + 2, 0))),
                                dest,
                            ]
                        })));
                }
            }
        }

        pages.set::<_, LoObject>("Kids".to_string(), page_ids.into());

        // ----- END PAGE CONTENT

        doc.inner_doc.objects.insert(pages_id, Dictionary(pages));
        doc.inner_doc
            .objects
            .insert(bookmarks_id, Dictionary(bookmarks_list));

        // save inner document
        let catalog_id = doc.inner_doc.add_object(catalog);
        let instance_id = random_character_string_32();

        doc.inner_doc.trailer.set("Root", Reference(catalog_id));
        doc.inner_doc
            .trailer
            .set("Info", Reference(document_info_id));
        doc.inner_doc.trailer.set(
            "ID",
            Array(vec![
                String(doc.document_id.as_bytes().to_vec(), Literal),
                String(instance_id.as_bytes().to_vec(), Literal),
            ]),
        );

        // does nothing in debug mode, optimized in release mode
        Self::optimize(&mut doc.inner_doc);
        let mut bytes = Vec::new();
        let mut writer = BufWriter::new(&mut bytes);
        doc.inner_doc.save_to(&mut writer)?;
        mem::drop(writer);

        Ok(bytes)
    }


    /// Save PDF Document, writing the contents to the target
    pub fn save<W: Write>(self, target: &mut BufWriter<W>) -> Result<(), Error> {
        let pdf_as_bytes = self.save_to_bytes()?;
        target.write(&pdf_as_bytes)?;
        Ok(())
    }

    #[cfg(any(debug_assertions, feature="less-optimization"))]
    #[inline]
    fn optimize(_: &mut lopdf::Document) { }

    #[cfg(all(not(debug_assertions), not(feature="less-optimization")))]
    #[inline]
    fn optimize(doc: &mut lopdf::Document)
    {
        doc.prune_objects();
        doc.delete_zero_length_streams();
        doc.compress();
    }
}