use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::fs::File;
use std::io::{self, BufWriter, Write};
use std::path::Path;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use crate::fonts::{BuiltinFont, FontRef, TrueTypeFontId};
use crate::graphics::Color;
use crate::images::{self, ImageData, ImageFit, ImageFormat, ImageId};
use crate::objects::{ObjId, PdfObject};
use crate::tables::{Row, Table, TableCursor};
use crate::textflow::{FitResult, Rect, TextFlow, TextStyle};
use crate::truetype::TrueTypeFont;
use crate::writer::PdfWriter;
const CATALOG_OBJ: ObjId = ObjId(1, 0);
const PAGES_OBJ: ObjId = ObjId(2, 0);
const FIRST_PAGE_OBJ_NUM: u32 = 3;
struct ImageObjIds {
xobject: ObjId,
smask: Option<ObjId>,
pdf_name: String,
}
struct TrueTypeFontObjIds {
type0: ObjId,
cid_font: ObjId,
descriptor: ObjId,
font_file: ObjId,
tounicode: ObjId,
}
struct PageRecord {
obj_id: ObjId,
content_ids: Vec<ObjId>,
width: f64,
height: f64,
used_fonts: BTreeSet<BuiltinFont>,
used_truetype_fonts: BTreeSet<usize>,
used_images: BTreeSet<usize>,
}
pub struct PdfDocument<W: Write> {
writer: PdfWriter<W>,
info: Vec<(String, String)>,
page_records: Vec<PageRecord>,
current_page: Option<PageBuilder>,
next_obj_num: u32,
font_obj_ids: BTreeMap<BuiltinFont, ObjId>,
truetype_fonts: Vec<TrueTypeFont>,
truetype_font_obj_ids: BTreeMap<usize, TrueTypeFontObjIds>,
next_font_num: u32,
compress: bool,
images: Vec<ImageData>,
image_obj_ids: BTreeMap<usize, ImageObjIds>,
written_images: BTreeSet<usize>,
next_image_num: u32,
}
struct PageBuilder {
width: f64,
height: f64,
content_ops: Vec<u8>,
used_fonts: BTreeSet<BuiltinFont>,
used_truetype_fonts: BTreeSet<usize>,
used_images: BTreeSet<usize>,
overlay_for: Option<usize>,
}
impl PdfDocument<BufWriter<File>> {
pub fn create<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let file = File::create(path)?;
Self::new(BufWriter::new(file))
}
}
impl<W: Write> PdfDocument<W> {
pub fn new(writer: W) -> io::Result<Self> {
let mut pdf_writer = PdfWriter::new(writer);
pdf_writer.write_header()?;
Ok(PdfDocument {
writer: pdf_writer,
info: Vec::new(),
page_records: Vec::new(),
current_page: None,
next_obj_num: FIRST_PAGE_OBJ_NUM,
font_obj_ids: BTreeMap::new(),
truetype_fonts: Vec::new(),
truetype_font_obj_ids: BTreeMap::new(),
next_font_num: 15,
compress: false,
images: Vec::new(),
image_obj_ids: BTreeMap::new(),
written_images: BTreeSet::new(),
next_image_num: 1,
})
}
pub fn set_info(&mut self, key: &str, value: &str) -> &mut Self {
self.info.push((key.to_string(), value.to_string()));
self
}
pub fn set_compression(&mut self, enabled: bool) -> &mut Self {
self.compress = enabled;
self
}
pub fn load_font_file<P: AsRef<Path>>(&mut self, path: P) -> Result<FontRef, String> {
let data =
std::fs::read(path.as_ref()).map_err(|e| format!("Failed to read font file: {}", e))?;
self.load_font_bytes(data)
}
pub fn load_font_bytes(&mut self, data: Vec<u8>) -> Result<FontRef, String> {
let font_num = self.next_font_num;
self.next_font_num += 1;
let font = TrueTypeFont::from_bytes(data, font_num)?;
let idx = self.truetype_fonts.len();
self.truetype_fonts.push(font);
Ok(FontRef::TrueType(TrueTypeFontId(idx)))
}
pub fn page_count(&self) -> usize {
self.page_records.len()
}
pub fn begin_page(&mut self, width: f64, height: f64) -> &mut Self {
if self.current_page.is_some() {
let _ = self.end_page();
}
self.current_page = Some(PageBuilder {
width,
height,
content_ops: Vec::new(),
used_fonts: BTreeSet::new(),
used_truetype_fonts: BTreeSet::new(),
used_images: BTreeSet::new(),
overlay_for: None,
});
self
}
pub fn open_page(&mut self, page_num: usize) -> io::Result<()> {
if page_num == 0 || page_num > self.page_records.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"open_page: page_num {} out of range (1..={})",
page_num,
self.page_records.len()
),
));
}
if self.current_page.is_some() {
self.end_page()?;
}
let idx = page_num - 1;
let width = self.page_records[idx].width;
let height = self.page_records[idx].height;
self.current_page = Some(PageBuilder {
width,
height,
content_ops: Vec::new(),
used_fonts: BTreeSet::new(),
used_truetype_fonts: BTreeSet::new(),
used_images: BTreeSet::new(),
overlay_for: Some(idx),
});
Ok(())
}
pub fn place_text(&mut self, text: &str, x: f64, y: f64) -> &mut Self {
let page = self
.current_page
.as_mut()
.expect("place_text called with no open page");
page.used_fonts.insert(BuiltinFont::Helvetica);
let escaped = crate::writer::escape_pdf_string(text);
let ops = format!(
"BT\n/F1 12 Tf\n{} {} Td\n({}) Tj\nET\n",
format_coord(x),
format_coord(y),
escaped,
);
page.content_ops.extend_from_slice(ops.as_bytes());
self
}
pub fn place_text_styled(
&mut self,
text: &str,
x: f64,
y: f64,
style: &TextStyle,
) -> &mut Self {
let (font_name, text_op) = match style.font {
FontRef::Builtin(b) => {
let escaped = crate::writer::escape_pdf_string(text);
(b.pdf_name().to_string(), format!("({}) Tj", escaped))
}
FontRef::TrueType(id) => {
let font = &mut self.truetype_fonts[id.0];
let hex = font.encode_text_hex(text);
(font.pdf_name.clone(), format!("{} Tj", hex))
}
};
let page = self
.current_page
.as_mut()
.expect("place_text_styled called with no open page");
match style.font {
FontRef::Builtin(b) => {
page.used_fonts.insert(b);
}
FontRef::TrueType(id) => {
page.used_truetype_fonts.insert(id.0);
}
}
let ops = format!(
"BT\n/{} {} Tf\n{} {} Td\n{}\nET\n",
font_name,
format_coord(style.font_size),
format_coord(x),
format_coord(y),
text_op,
);
page.content_ops.extend_from_slice(ops.as_bytes());
self
}
pub fn fit_textflow(&mut self, flow: &mut TextFlow, rect: &Rect) -> io::Result<FitResult> {
let (ops, result, used_fonts) = flow.generate_content_ops(rect, &mut self.truetype_fonts);
let page = self
.current_page
.as_mut()
.expect("fit_textflow called with no open page");
page.content_ops.extend_from_slice(&ops);
page.used_fonts.extend(used_fonts.builtin);
page.used_truetype_fonts.extend(used_fonts.truetype);
Ok(result)
}
pub fn fit_row(
&mut self,
table: &Table,
row: &Row,
cursor: &mut TableCursor,
) -> io::Result<FitResult> {
let total_span: usize = row.cells.iter().map(|c| c.col_span.max(1)).sum();
if total_span != table.columns.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"row col_span sum ({}) must equal table column count ({})",
total_span,
table.columns.len()
),
));
}
let (ops, result, used_fonts) =
table.generate_row_ops(row, cursor, &mut self.truetype_fonts);
let page = self
.current_page
.as_mut()
.expect("fit_row called with no open page");
page.content_ops.extend_from_slice(&ops);
page.used_fonts.extend(used_fonts.builtin);
page.used_truetype_fonts.extend(used_fonts.truetype);
Ok(result)
}
pub fn load_image_file<P: AsRef<Path>>(&mut self, path: P) -> Result<ImageId, String> {
let data = std::fs::read(path.as_ref())
.map_err(|e| format!("Failed to read image file: {}", e))?;
self.load_image_bytes(data)
}
pub fn load_image_bytes(&mut self, data: Vec<u8>) -> Result<ImageId, String> {
let image_data = images::load_image(data)?;
let idx = self.images.len();
self.images.push(image_data);
Ok(ImageId(idx))
}
pub fn place_image(&mut self, image: &ImageId, rect: &Rect, fit: ImageFit) -> &mut Self {
let idx = image.0;
let img = &self.images[idx];
let page_height = self
.current_page
.as_ref()
.expect("place_image called with no open page")
.height;
let placement = images::calculate_placement(img.width, img.height, rect, fit, page_height);
self.ensure_image_obj_ids(idx);
let pdf_name = self.image_obj_ids[&idx].pdf_name.clone();
let page = self
.current_page
.as_mut()
.expect("place_image called with no open page");
page.used_images.insert(idx);
let mut ops = String::new();
ops.push_str("q\n");
if let Some(clip) = &placement.clip {
ops.push_str(&format!(
"{} {} {} {} re W n\n",
format_coord(clip.x),
format_coord(clip.y),
format_coord(clip.width),
format_coord(clip.height),
));
}
ops.push_str(&format!(
"{} 0 0 {} {} {} cm\n",
format_coord(placement.width),
format_coord(placement.height),
format_coord(placement.x),
format_coord(placement.y),
));
ops.push_str(&format!("/{} Do\n", pdf_name));
ops.push_str("Q\n");
page.content_ops.extend_from_slice(ops.as_bytes());
self
}
fn ensure_image_obj_ids(&mut self, idx: usize) {
if self.image_obj_ids.contains_key(&idx) {
return;
}
let xobject = ObjId(self.next_obj_num, 0);
self.next_obj_num += 1;
let smask = if self.images[idx].smask_data.is_some() {
let id = ObjId(self.next_obj_num, 0);
self.next_obj_num += 1;
Some(id)
} else {
None
};
let pdf_name = format!("Im{}", self.next_image_num);
self.next_image_num += 1;
self.image_obj_ids.insert(
idx,
ImageObjIds {
xobject,
smask,
pdf_name,
},
);
}
fn write_image_xobject(&mut self, idx: usize) -> io::Result<()> {
if self.written_images.contains(&idx) {
return Ok(());
}
let img = &self.images[idx];
let obj_ids = &self.image_obj_ids[&idx];
let xobject_id = obj_ids.xobject;
let smask_id = obj_ids.smask;
if let (Some(smask_obj_id), Some(smask_data)) = (smask_id, img.smask_data.as_ref()) {
let smask_stream = self.make_stream(
vec![
("Type", PdfObject::name("XObject")),
("Subtype", PdfObject::name("Image")),
("Width", PdfObject::Integer(img.width as i64)),
("Height", PdfObject::Integer(img.height as i64)),
("ColorSpace", PdfObject::name("DeviceGray")),
("BitsPerComponent", PdfObject::Integer(8)),
],
smask_data.clone(),
);
self.writer.write_object(smask_obj_id, &smask_stream)?;
}
let mut entries: Vec<(&str, PdfObject)> = vec![
("Type", PdfObject::name("XObject")),
("Subtype", PdfObject::name("Image")),
("Width", PdfObject::Integer(img.width as i64)),
("Height", PdfObject::Integer(img.height as i64)),
("ColorSpace", PdfObject::name(img.color_space.pdf_name())),
(
"BitsPerComponent",
PdfObject::Integer(img.bits_per_component as i64),
),
];
if let Some(smask_obj_id) = smask_id {
entries.push(("SMask", PdfObject::Reference(smask_obj_id)));
}
let image_obj = match img.format {
ImageFormat::Jpeg => {
entries.push(("Filter", PdfObject::name("DCTDecode")));
PdfObject::stream(entries, img.data.clone())
}
ImageFormat::Png => self.make_stream(entries, img.data.clone()),
};
self.writer.write_object(xobject_id, &image_obj)?;
self.written_images.insert(idx);
Ok(())
}
pub fn set_stroke_color(&mut self, color: Color) -> &mut Self {
let page = self
.current_page
.as_mut()
.expect("set_stroke_color called with no open page");
let ops = format!(
"{} {} {} RG\n",
format_coord(color.r),
format_coord(color.g),
format_coord(color.b),
);
page.content_ops.extend_from_slice(ops.as_bytes());
self
}
pub fn set_fill_color(&mut self, color: Color) -> &mut Self {
let page = self
.current_page
.as_mut()
.expect("set_fill_color called with no open page");
let ops = format!(
"{} {} {} rg\n",
format_coord(color.r),
format_coord(color.g),
format_coord(color.b),
);
page.content_ops.extend_from_slice(ops.as_bytes());
self
}
pub fn set_line_width(&mut self, width: f64) -> &mut Self {
let page = self
.current_page
.as_mut()
.expect("set_line_width called with no open page");
let ops = format!("{} w\n", format_coord(width));
page.content_ops.extend_from_slice(ops.as_bytes());
self
}
pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
let page = self
.current_page
.as_mut()
.expect("move_to called with no open page");
let ops = format!("{} {} m\n", format_coord(x), format_coord(y));
page.content_ops.extend_from_slice(ops.as_bytes());
self
}
pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
let page = self
.current_page
.as_mut()
.expect("line_to called with no open page");
let ops = format!("{} {} l\n", format_coord(x), format_coord(y));
page.content_ops.extend_from_slice(ops.as_bytes());
self
}
pub fn rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
let page = self
.current_page
.as_mut()
.expect("rect called with no open page");
let ops = format!(
"{} {} {} {} re\n",
format_coord(x),
format_coord(y),
format_coord(width),
format_coord(height),
);
page.content_ops.extend_from_slice(ops.as_bytes());
self
}
pub fn close_path(&mut self) -> &mut Self {
let page = self
.current_page
.as_mut()
.expect("close_path called with no open page");
page.content_ops.extend_from_slice(b"h\n");
self
}
pub fn stroke(&mut self) -> &mut Self {
let page = self
.current_page
.as_mut()
.expect("stroke called with no open page");
page.content_ops.extend_from_slice(b"S\n");
self
}
pub fn fill(&mut self) -> &mut Self {
let page = self
.current_page
.as_mut()
.expect("fill called with no open page");
page.content_ops.extend_from_slice(b"f\n");
self
}
pub fn fill_stroke(&mut self) -> &mut Self {
let page = self
.current_page
.as_mut()
.expect("fill_stroke called with no open page");
page.content_ops.extend_from_slice(b"B\n");
self
}
pub fn save_state(&mut self) -> &mut Self {
let page = self
.current_page
.as_mut()
.expect("save_state called with no open page");
page.content_ops.extend_from_slice(b"q\n");
self
}
pub fn restore_state(&mut self) -> &mut Self {
let page = self
.current_page
.as_mut()
.expect("restore_state called with no open page");
page.content_ops.extend_from_slice(b"Q\n");
self
}
fn make_stream(&self, mut dict_entries: Vec<(&str, PdfObject)>, data: Vec<u8>) -> PdfObject {
if self.compress {
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&data).expect("flate2 in-memory write");
let compressed = encoder.finish().expect("flate2 finish");
dict_entries.push(("Filter", PdfObject::name("FlateDecode")));
PdfObject::stream(dict_entries, compressed)
} else {
PdfObject::stream(dict_entries, data)
}
}
fn ensure_font_written(&mut self, font: BuiltinFont) -> io::Result<ObjId> {
if let Some(&id) = self.font_obj_ids.get(&font) {
return Ok(id);
}
let id = ObjId(self.next_obj_num, 0);
self.next_obj_num += 1;
let obj = PdfObject::dict(vec![
("Type", PdfObject::name("Font")),
("Subtype", PdfObject::name("Type1")),
("BaseFont", PdfObject::name(font.pdf_base_name())),
]);
self.writer.write_object(id, &obj)?;
self.font_obj_ids.insert(font, id);
Ok(id)
}
fn ensure_tt_font_obj_ids(&mut self, idx: usize) -> &TrueTypeFontObjIds {
if !self.truetype_font_obj_ids.contains_key(&idx) {
let type0 = ObjId(self.next_obj_num, 0);
self.next_obj_num += 1;
let cid_font = ObjId(self.next_obj_num, 0);
self.next_obj_num += 1;
let descriptor = ObjId(self.next_obj_num, 0);
self.next_obj_num += 1;
let font_file = ObjId(self.next_obj_num, 0);
self.next_obj_num += 1;
let tounicode = ObjId(self.next_obj_num, 0);
self.next_obj_num += 1;
self.truetype_font_obj_ids.insert(
idx,
TrueTypeFontObjIds {
type0,
cid_font,
descriptor,
font_file,
tounicode,
},
);
}
&self.truetype_font_obj_ids[&idx]
}
pub fn end_page(&mut self) -> io::Result<()> {
let page = self
.current_page
.take()
.expect("end_page called with no open page");
for &font in &page.used_fonts {
self.ensure_font_written(font)?;
}
for &idx in &page.used_truetype_fonts {
self.ensure_tt_font_obj_ids(idx);
}
let used_images: Vec<usize> = page.used_images.iter().copied().collect();
for idx in &used_images {
self.write_image_xobject(*idx)?;
}
let content_id = ObjId(self.next_obj_num, 0);
self.next_obj_num += 1;
let content_stream = self.make_stream(vec![], page.content_ops);
self.writer.write_object(content_id, &content_stream)?;
match page.overlay_for {
None => {
let page_id = ObjId(self.next_obj_num, 0);
self.next_obj_num += 1;
self.page_records.push(PageRecord {
obj_id: page_id,
content_ids: vec![content_id],
width: page.width,
height: page.height,
used_fonts: page.used_fonts,
used_truetype_fonts: page.used_truetype_fonts,
used_images: page.used_images,
});
}
Some(idx) => {
let record = &mut self.page_records[idx];
record.content_ids.push(content_id);
record.used_fonts.extend(page.used_fonts);
record.used_truetype_fonts.extend(page.used_truetype_fonts);
record.used_images.extend(page.used_images);
}
}
Ok(())
}
fn build_font_dict(&self, used_fonts: &[BuiltinFont], used_truetype: &[usize]) -> PdfObject {
let mut entries: Vec<(String, PdfObject)> = used_fonts
.iter()
.map(|f| {
(
f.pdf_name().to_string(),
PdfObject::Reference(self.font_obj_ids[f]),
)
})
.collect();
for &idx in used_truetype {
let name = self.truetype_fonts[idx].pdf_name.clone();
let type0_id = self.truetype_font_obj_ids[&idx].type0;
entries.push((name, PdfObject::Reference(type0_id)));
}
PdfObject::Dictionary(entries)
}
fn build_resource_dict(
&self,
used_fonts: &[BuiltinFont],
used_truetype: &[usize],
used_images: &[usize],
) -> PdfObject {
let font_dict = self.build_font_dict(used_fonts, used_truetype);
let xobject_entries: Vec<(String, PdfObject)> = used_images
.iter()
.filter_map(|idx| {
self.image_obj_ids
.get(idx)
.map(|ids| (ids.pdf_name.clone(), PdfObject::Reference(ids.xobject)))
})
.collect();
let mut resource_entries: Vec<(String, PdfObject)> = vec![("Font".to_string(), font_dict)];
if !xobject_entries.is_empty() {
resource_entries.push((
"XObject".to_string(),
PdfObject::Dictionary(xobject_entries),
));
}
PdfObject::Dictionary(resource_entries)
}
fn build_contents(content_ids: &[ObjId]) -> PdfObject {
if content_ids.len() == 1 {
PdfObject::Reference(content_ids[0])
} else {
PdfObject::array(
content_ids
.iter()
.map(|id| PdfObject::Reference(*id))
.collect(),
)
}
}
fn write_page_dicts(&mut self) -> io::Result<()> {
for i in 0..self.page_records.len() {
let obj_id = self.page_records[i].obj_id;
let content_ids: Vec<ObjId> =
self.page_records[i].content_ids.iter().copied().collect();
let width = self.page_records[i].width;
let height = self.page_records[i].height;
let used_fonts: Vec<BuiltinFont> =
self.page_records[i].used_fonts.iter().copied().collect();
let used_truetype: Vec<usize> = self.page_records[i]
.used_truetype_fonts
.iter()
.copied()
.collect();
let used_images: Vec<usize> =
self.page_records[i].used_images.iter().copied().collect();
let resources = self.build_resource_dict(&used_fonts, &used_truetype, &used_images);
let contents = Self::build_contents(&content_ids);
let page_dict = PdfObject::dict(vec![
("Type", PdfObject::name("Page")),
("Parent", PdfObject::Reference(PAGES_OBJ)),
(
"MediaBox",
PdfObject::array(vec![
PdfObject::Integer(0),
PdfObject::Integer(0),
PdfObject::Real(width),
PdfObject::Real(height),
]),
),
("Contents", contents),
("Resources", resources),
]);
self.writer.write_object(obj_id, &page_dict)?;
}
Ok(())
}
fn write_truetype_fonts(&mut self) -> io::Result<()> {
let indices: Vec<usize> = self.truetype_font_obj_ids.keys().copied().collect();
for idx in indices {
let obj_ids_type0 = self.truetype_font_obj_ids[&idx].type0;
let obj_ids_cid = self.truetype_font_obj_ids[&idx].cid_font;
let obj_ids_desc = self.truetype_font_obj_ids[&idx].descriptor;
let obj_ids_file = self.truetype_font_obj_ids[&idx].font_file;
let obj_ids_tounicode = self.truetype_font_obj_ids[&idx].tounicode;
let font = &self.truetype_fonts[idx];
let original_len = font.font_data.len() as i64;
let font_file_stream = self.make_stream(
vec![("Length1", PdfObject::Integer(original_len))],
font.font_data.clone(),
);
self.writer.write_object(obj_ids_file, &font_file_stream)?;
let descriptor = PdfObject::dict(vec![
("Type", PdfObject::name("FontDescriptor")),
("FontName", PdfObject::name(&font.postscript_name)),
("Flags", PdfObject::Integer(font.flags as i64)),
(
"FontBBox",
PdfObject::array(vec![
PdfObject::Integer(font.scale_to_pdf(font.bbox[0])),
PdfObject::Integer(font.scale_to_pdf(font.bbox[1])),
PdfObject::Integer(font.scale_to_pdf(font.bbox[2])),
PdfObject::Integer(font.scale_to_pdf(font.bbox[3])),
]),
),
("ItalicAngle", PdfObject::Real(font.italic_angle)),
("Ascent", PdfObject::Integer(font.scale_to_pdf(font.ascent))),
(
"Descent",
PdfObject::Integer(font.scale_to_pdf(font.descent)),
),
(
"CapHeight",
PdfObject::Integer(font.scale_to_pdf(font.cap_height)),
),
("StemV", PdfObject::Integer(font.scale_to_pdf(font.stem_v))),
("FontFile2", PdfObject::Reference(obj_ids_file)),
]);
self.writer.write_object(obj_ids_desc, &descriptor)?;
let w_array = font.build_w_array();
let cid_font = PdfObject::dict(vec![
("Type", PdfObject::name("Font")),
("Subtype", PdfObject::name("CIDFontType2")),
("BaseFont", PdfObject::name(&font.postscript_name)),
(
"CIDSystemInfo",
PdfObject::dict(vec![
("Registry", PdfObject::literal_string("Adobe")),
("Ordering", PdfObject::literal_string("Identity")),
("Supplement", PdfObject::Integer(0)),
]),
),
("FontDescriptor", PdfObject::Reference(obj_ids_desc)),
("DW", PdfObject::Integer(font.default_width_pdf())),
("W", PdfObject::Array(w_array)),
]);
self.writer.write_object(obj_ids_cid, &cid_font)?;
let tounicode_data = font.build_tounicode_cmap();
let tounicode = self.make_stream(vec![], tounicode_data);
self.writer.write_object(obj_ids_tounicode, &tounicode)?;
let type0 = PdfObject::dict(vec![
("Type", PdfObject::name("Font")),
("Subtype", PdfObject::name("Type0")),
("BaseFont", PdfObject::name(&font.postscript_name)),
("Encoding", PdfObject::name("Identity-H")),
(
"DescendantFonts",
PdfObject::array(vec![PdfObject::Reference(obj_ids_cid)]),
),
("ToUnicode", PdfObject::Reference(obj_ids_tounicode)),
]);
self.writer.write_object(obj_ids_type0, &type0)?;
}
Ok(())
}
pub fn end_document(mut self) -> io::Result<W> {
if self.current_page.is_some() {
self.end_page()?;
}
self.write_page_dicts()?;
self.write_truetype_fonts()?;
let info_id = if !self.info.is_empty() {
let id = ObjId(self.next_obj_num, 0);
self.next_obj_num += 1;
let entries: Vec<(&str, PdfObject)> = self
.info
.iter()
.map(|(k, v)| (k.as_str(), PdfObject::literal_string(v)))
.collect();
let info_obj = PdfObject::dict(entries);
self.writer.write_object(id, &info_obj)?;
Some(id)
} else {
None
};
let kids: Vec<PdfObject> = self
.page_records
.iter()
.map(|r| PdfObject::Reference(r.obj_id))
.collect();
let page_count = self.page_records.len() as i64;
let pages = PdfObject::dict(vec![
("Type", PdfObject::name("Pages")),
("Kids", PdfObject::Array(kids)),
("Count", PdfObject::Integer(page_count)),
]);
self.writer.write_object(PAGES_OBJ, &pages)?;
let catalog = PdfObject::dict(vec![
("Type", PdfObject::name("Catalog")),
("Pages", PdfObject::Reference(PAGES_OBJ)),
]);
self.writer.write_object(CATALOG_OBJ, &catalog)?;
self.writer.write_xref_and_trailer(CATALOG_OBJ, info_id)?;
Ok(self.writer.into_inner())
}
}
pub(crate) fn format_coord(v: f64) -> String {
if v == v.floor() && v.abs() < 1e15 {
format!("{}", v as i64)
} else {
let s = format!("{:.4}", v);
let s = s.trim_end_matches('0');
let s = s.trim_end_matches('.');
s.to_string()
}
}