#![deny(missing_docs)]
#[macro_use]
extern crate lazy_static;
use chrono::offset::Local;
use std::collections::{BTreeMap, HashMap};
use std::fmt;
use std::fs::File;
use std::io::{self, Seek, SeekFrom, Write};
mod fontsource;
pub use crate::fontsource::{BuiltinFont, FontSource};
mod fontref;
pub use crate::fontref::FontRef;
mod fontmetrics;
pub use crate::fontmetrics::FontMetrics;
mod encoding;
pub use crate::encoding::Encoding;
pub mod graphicsstate;
mod outline;
use crate::outline::OutlineItem;
mod canvas;
pub use crate::canvas::Canvas;
mod textobject;
pub use crate::textobject::TextObject;
pub struct Pdf {
output: File,
object_offsets: Vec<i64>,
page_objects_ids: Vec<usize>,
all_font_object_ids: HashMap<BuiltinFont, usize>,
outline_items: Vec<OutlineItem>,
document_info: BTreeMap<String, String>,
}
const ROOT_OBJECT_ID: usize = 1;
const PAGES_OBJECT_ID: usize = 2;
impl Pdf {
pub fn create(filename: &str) -> io::Result<Pdf> {
let file = File::create(filename)?;
Pdf::new(file)
}
pub fn new(mut output: File) -> io::Result<Pdf> {
output.write_all(b"%PDF-1.7\n%\xB5\xED\xAE\xFB\n")?;
Ok(Pdf {
output,
object_offsets: vec![-1, -1, -1],
page_objects_ids: Vec::new(),
all_font_object_ids: HashMap::new(),
outline_items: Vec::new(),
document_info: BTreeMap::new(),
})
}
pub fn set_title(&mut self, title: &str) {
self.document_info
.insert("Title".to_string(), title.to_string());
}
pub fn set_author(&mut self, author: &str) {
self.document_info
.insert("Author".to_string(), author.to_string());
}
pub fn set_subject(&mut self, subject: &str) {
self.document_info
.insert("Subject".to_string(), subject.to_string());
}
pub fn set_keywords(&mut self, keywords: &str) {
self.document_info
.insert("Keywords".to_string(), keywords.to_string());
}
pub fn set_creator(&mut self, creator: &str) {
self.document_info
.insert("Creator".to_string(), creator.to_string());
}
pub fn set_producer(&mut self, producer: &str) {
self.document_info
.insert("Producer".to_string(), producer.to_string());
}
fn tell(&mut self) -> io::Result<u64> {
self.output.seek(SeekFrom::Current(0))
}
pub fn render_page<F>(
&mut self,
width: f32,
height: f32,
render_contents: F,
) -> io::Result<()>
where
F: FnOnce(&mut Canvas) -> io::Result<()>,
{
let (contents_object_id, content_length, fonts, outline_items) = self
.write_new_object(move |contents_object_id, pdf| {
writeln!(
pdf.output,
"<< /Length {} 0 R >>\n\
stream",
contents_object_id + 1,
)?;
let start = pdf.tell()?;
writeln!(pdf.output, "/DeviceRGB cs /DeviceRGB CS")?;
let mut fonts = HashMap::new();
let mut outline_items = Vec::new();
render_contents(&mut Canvas::new(
&mut pdf.output,
&mut fonts,
&mut outline_items,
))?;
let end = pdf.tell()?;
writeln!(pdf.output, "endstream")?;
Ok((contents_object_id, end - start, fonts, outline_items))
})?;
self.write_new_object(|length_object_id, pdf| {
assert!(length_object_id == contents_object_id + 1);
writeln!(pdf.output, "{}", content_length)
})?;
let mut font_oids = NamedRefs::new(fonts.len());
for (src, r) in fonts {
if let Some(&object_id) = self.all_font_object_ids.get(&src) {
font_oids.insert(r, object_id);
} else {
let object_id = src.write_object(self)?;
font_oids.insert(r, object_id);
self.all_font_object_ids.insert(src, object_id);
}
}
let page_oid = self.write_page_dict(
contents_object_id,
width,
height,
font_oids,
)?;
for mut item in outline_items {
item.set_page(page_oid);
self.outline_items.push(item);
}
self.page_objects_ids.push(page_oid);
Ok(())
}
fn write_page_dict(
&mut self,
content_oid: usize,
width: f32,
height: f32,
font_oids: NamedRefs,
) -> io::Result<usize> {
self.write_new_object(|page_oid, pdf| {
writeln!(
pdf.output,
"<< /Type /Page\n \
/Parent {parent} 0 R\n \
/Resources << /Font << {fonts}>> >>\n \
/MediaBox [ 0 0 {width} {height} ]\n \
/Contents {c_oid} 0 R\n\
>>",
parent = PAGES_OBJECT_ID,
fonts = font_oids,
width = width,
height = height,
c_oid = content_oid,
)
.map(|_| page_oid)
})
}
fn write_new_object<F, T>(&mut self, write_content: F) -> io::Result<T>
where
F: FnOnce(usize, &mut Pdf) -> io::Result<T>,
{
let id = self.object_offsets.len();
let (result, offset) =
self.write_object(id, |pdf| write_content(id, pdf))?;
self.object_offsets.push(offset);
Ok(result)
}
fn write_object_with_id<F, T>(
&mut self,
id: usize,
write_content: F,
) -> io::Result<T>
where
F: FnOnce(&mut Pdf) -> io::Result<T>,
{
assert!(self.object_offsets[id] == -1);
let (result, offset) = self.write_object(id, write_content)?;
self.object_offsets[id] = offset;
Ok(result)
}
fn write_object<F, T>(
&mut self,
id: usize,
write_content: F,
) -> io::Result<(T, i64)>
where
F: FnOnce(&mut Pdf) -> io::Result<T>,
{
let offset = self.tell()? as i64;
writeln!(self.output, "{} 0 obj", id)?;
let result = write_content(self)?;
writeln!(self.output, "endobj")?;
Ok((result, offset))
}
pub fn finish(mut self) -> io::Result<()> {
self.write_object_with_id(PAGES_OBJECT_ID, |pdf| {
write!(
pdf.output,
"<< /Type /Pages\n \
/Count {}\n ",
pdf.page_objects_ids.len()
)?;
write!(pdf.output, "/Kids [ ")?;
for id in &pdf.page_objects_ids {
write!(pdf.output, "{} 0 R ", id)?;
}
writeln!(pdf.output, "]\n>>")
})?;
let document_info_id = if !self.document_info.is_empty() {
let info = self.document_info.clone();
self.write_new_object(|page_object_id, pdf| {
write!(pdf.output, "<<")?;
for (key, value) in info {
writeln!(pdf.output, " /{} ({})", key, value)?;
}
let now = Local::now().format("%Y%m%d%H%M%S%z").to_string();
write!(
pdf.output,
" /CreationDate (D:{now})\n \
/ModDate (D:{now})",
now = now,
)?;
writeln!(pdf.output, ">>")?;
Ok(Some(page_object_id))
})?
} else {
None
};
let outlines_id = self.write_outlines()?;
self.write_object_with_id(ROOT_OBJECT_ID, |pdf| {
writeln!(
pdf.output,
"<< /Type /Catalog\n \
/Pages {} 0 R",
PAGES_OBJECT_ID,
)?;
if let Some(outlines_id) = outlines_id {
writeln!(pdf.output, "/Outlines {} 0 R", outlines_id)?;
}
writeln!(pdf.output, ">>")
})?;
let startxref = self.tell()?;
writeln!(
self.output,
"xref\n\
0 {}\n\
0000000000 65535 f ",
self.object_offsets.len(),
)?;
for &offset in &self.object_offsets[1..] {
assert!(offset >= 0);
writeln!(self.output, "{:010} 00000 n ", offset)?;
}
writeln!(
self.output,
"trailer\n\
<< /Size {size}\n \
/Root {root} 0 R",
size = self.object_offsets.len(),
root = ROOT_OBJECT_ID,
)?;
if let Some(id) = document_info_id {
writeln!(self.output, " /Info {} 0 R", id)?;
}
writeln!(
self.output,
">>\n\
startxref\n\
{}\n\
%%EOF",
startxref,
)
}
fn write_outlines(&mut self) -> io::Result<Option<usize>> {
if self.outline_items.is_empty() {
return Ok(None);
}
let parent_id = self.object_offsets.len();
self.object_offsets.push(-1);
let count = self.outline_items.len();
let mut first_id = 0;
let mut last_id = 0;
let items = self.outline_items.clone();
for (i, item) in items.iter().enumerate() {
let (is_first, is_last) = (i == 0, i == count - 1);
let id = self.write_new_object(|object_id, pdf| {
item.write_dictionary(
&mut pdf.output,
parent_id,
if is_first { None } else { Some(object_id - 1) },
if is_last { None } else { Some(object_id + 1) },
)
.and(Ok(object_id))
})?;
if is_first {
first_id = id;
}
if is_last {
last_id = id;
}
}
self.write_object_with_id(parent_id, |pdf| {
writeln!(
pdf.output,
"<< /Type /Outlines\n \
/First {first} 0 R\n \
/Last {last} 0 R\n \
/Count {count}\n\
>>",
last = last_id,
first = first_id,
count = count,
)
})?;
Ok(Some(parent_id))
}
}
struct NamedRefs {
oids: HashMap<FontRef, usize>,
}
impl NamedRefs {
fn new(capacity: usize) -> Self {
NamedRefs {
oids: HashMap::with_capacity(capacity),
}
}
fn insert(&mut self, name: FontRef, oid: usize) -> Option<usize> {
self.oids.insert(name, oid)
}
}
impl fmt::Display for NamedRefs {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for (name, id) in &self.oids {
write!(f, "{} {} 0 R ", name, id)?;
}
Ok(())
}
}