use crate::utils::random_character_string_32;
use std::cell::RefCell;
use std::collections::HashMap;
use std::io::BufWriter;
use std::io::Write;
use std::rc::Rc;
use crate::OffsetDateTime;
use lopdf;
use crate::indices::*;
use crate::{
BuiltinFont, DirectFontRef, Error, ExternalFont, Font, FontData, FontList, IccProfileList,
IndirectFontRef, Mm, PdfConformance, PdfMetadata, PdfPage, PdfPageReference,
};
#[derive(Debug, Clone)]
pub struct PdfDocument {
pub(super) pages: Vec<PdfPage>,
pub fonts: FontList,
pub(super) _icc_profiles: IccProfileList,
pub(super) inner_doc: lopdf::Document,
pub document_id: String,
pub metadata: PdfMetadata,
pub bookmarks: HashMap<usize, String>,
}
pub struct PdfDocumentReference {
pub(crate) document: Rc<RefCell<PdfDocument>>,
}
impl PdfDocument {
#[allow(clippy::new_ret_no_self)]
#[inline]
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 {
#[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
}
#[inline]
pub fn with_author<S>(self, author: S) -> Self
where
S: Into<String>,
{
self.document.borrow_mut().metadata.author = author.into();
self
}
#[inline]
pub fn with_creator<S>(self, creator: S) -> Self
where
S: Into<String>,
{
self.document.borrow_mut().metadata.creator = creator.into();
self
}
#[inline]
pub fn with_producer<S>(self, producer: S) -> Self
where
S: Into<String>,
{
self.document.borrow_mut().metadata.producer = producer.into();
self
}
#[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
}
#[inline]
pub fn with_subject<S>(self, subject: S) -> Self
where
S: Into<String>,
{
self.document.borrow_mut().metadata.subject = subject.into();
self
}
#[inline]
pub fn with_identifier<S>(self, identifier: S) -> Self
where
S: Into<String>,
{
self.document.borrow_mut().metadata.identifier = identifier.into();
self
}
#[inline]
pub fn with_trapping(self, trapping: bool) -> Self {
self.document.borrow_mut().metadata.trapping = trapping;
self
}
#[inline]
pub fn with_document_id(self, id: String) -> Self {
self.document.borrow_mut().metadata.xmp_metadata.document_id = id;
self
}
#[inline]
pub fn with_document_version(self, version: u32) -> Self {
self.document.borrow_mut().metadata.document_version = version;
self
}
#[inline]
pub fn with_conformance(self, conformance: PdfConformance) -> Self {
self.document.borrow_mut().metadata.conformance = conformance;
self
}
#[inline]
pub fn with_creation_date(self, creation_date: OffsetDateTime) -> Self {
self.document.borrow_mut().metadata.creation_date = creation_date;
self
}
#[inline]
pub fn with_metadata_date(self, metadata_date: OffsetDateTime) -> Self {
self.document.borrow_mut().metadata.metadata_date = metadata_date;
self
}
#[inline]
pub fn with_mod_date(self, mod_date: OffsetDateTime) -> Self {
self.document.borrow_mut().metadata.modification_date = mod_date;
self
}
#[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)
}
#[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());
}
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)
}
#[cfg(feature = "font_subsetting")]
pub fn add_external_font_with_subsetting<R>(
&self,
font_stream: R,
allow_subsetting: bool,
) -> ::std::result::Result<IndirectFontRef, Error>
where
R: ::std::io::Read,
{
let font = self.add_external_font(font_stream);
if let Ok(font) = &font {
match self.document.borrow().fonts.get_font(&font).unwrap().data {
Font::ExternalFont(ex_font) => {
*ex_font.allow_subsetting.borrow_mut() = allow_subsetting
}
Font::BuiltinFont(_) => unreachable!(),
}
}
font
}
pub fn add_external_font_data<F>(
&self,
bytes: Vec<u8>,
data: F,
) -> 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)
}
#[cfg(feature = "font_subsetting")]
pub fn add_external_font_data_with_subsetting<F>(
&self,
bytes: Vec<u8>,
data: F,
allow_subsetting: bool,
) -> Result<IndirectFontRef, Error>
where
F: FontData + 'static,
{
let font = self.add_external_font_data(bytes, data);
if let Ok(font) = &font {
match self.document.borrow().fonts.get_font(&font).unwrap().data {
Font::ExternalFont(ex_font) => {
*ex_font.allow_subsetting.borrow_mut() = allow_subsetting
}
Font::BuiltinFont(_) => unreachable!(),
}
}
font
}
pub fn add_builtin_font(
&self,
builtin_font: BuiltinFont,
) -> ::std::result::Result<IndirectFontRef, Error> {
let builtin_font_name: &'static str = builtin_font.into();
implement_adding_fonts!(self, builtin_font_name, Font::BuiltinFont(builtin_font))
}
#[inline]
pub fn get_page(&self, page: PdfPageIndex) -> PdfPageReference {
let _ = &self.document.borrow_mut().pages[page.0];
PdfPageReference {
document: Rc::downgrade(&self.document),
page,
}
}
#[inline]
pub fn get_font(&self, font: &IndirectFontRef) -> Option<DirectFontRef> {
let doc = self.document.borrow();
doc.fonts.get_font(font)
}
#[inline]
pub unsafe fn get_inner(self) -> lopdf::Document {
let doc = Rc::try_unwrap(self.document).unwrap().into_inner();
doc.inner_doc
}
pub fn check_for_errors(&self) -> ::std::result::Result<(), Error> {
#[cfg(feature = "logging")]
{
warn!("Checking PDFs for errors is currently not supported!");
}
Ok(())
}
pub fn repair_errors(&self, _conformance: PdfConformance) -> ::std::result::Result<(), Error> {
#[cfg(feature = "logging")]
{
warn!("Reparing PDFs is currently not supported!");
}
Ok(())
}
#[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::mem;
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)),
]);
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);
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.is_empty() {
"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)),
]);
let mut page_ids = Vec::<LoObject>::new();
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();
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);
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(|(_, layers)| layers.iter().map(|(_, obj)| obj.clone()))
.collect();
catalog.set(
"OCProperties",
Dictionary(LoDictionary::from_iter(vec![
("OCGs", Array(flattened_ocg_list.clone())),
(
"D",
Dictionary(LoDictionary::from_iter(vec![
("Order", Array(flattened_ocg_list.clone())),
("RBGroups", Array(vec![])),
("ON", Array(flattened_ocg_list)),
])),
),
])),
);
let mut font_dict_id = None;
let fonts_dict: lopdf::Dictionary = doc
.fonts
.into_with_document(&mut doc.inner_doc, &mut doc.pages);
if !fonts_dict.is_empty() {
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 annotation_ids = page
.resources
.link_annotations
.clone()
.into_iter()
.map(|(_, annotation)| doc.inner_doc.add_object(annotation))
.collect::<Vec<_>>();
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(),
),
(
"Annots",
annotation_ids
.iter()
.map(|id| Reference(*id))
.collect::<Vec<LoObject>>()
.into(),
),
("Parent", Reference(pages_id)),
]);
if let Some(extension) = &page.extend_with {
for (key, value) in extension.iter() {
p.set(key.to_vec(), value.clone())
}
}
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.is_empty() {
let resources_page_id = doc.inner_doc.add_object(Dictionary(resources_page));
p.set("Resources", Reference(resources_page_id));
}
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.contains_key(&idx) {
page_id_to_obj.insert(idx, page_obj);
}
page_ids.push(Reference(page_obj))
}
if !doc.bookmarks.is_empty() {
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());
doc.inner_doc.objects.insert(pages_id, Dictionary(pages));
doc.inner_doc
.objects
.insert(bookmarks_id, Dictionary(bookmarks_list));
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),
]),
);
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)
}
pub fn save<W: Write>(self, target: &mut BufWriter<W>) -> Result<(), Error> {
Ok(target.write_all(&self.save_to_bytes()?)?)
}
#[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();
}
}