use std::collections::BTreeMap;
use std::io::{Seek, Write};
use crate::error::PdfResult;
use crate::object::*;
use crate::parser::Document;
pub struct Serializer {
deterministic: bool,
}
impl Serializer {
pub fn new() -> Self {
Self {
deterministic: false,
}
}
pub fn with_deterministic(deterministic: bool) -> Self {
Self { deterministic }
}
pub fn serialize(&self, doc: &Document, writer: &mut (impl Write + Seek)) -> PdfResult<()> {
writeln!(writer, "%PDF-1.7")?;
writer.write_all(b"%\xe2\xcf\xd3\xe2\n")?;
let mut offsets: BTreeMap<u32, u64> = BTreeMap::new();
for (id, obj) in doc.objects() {
offsets.insert(id.number, writer.stream_position()?);
writeln!(writer, "{} {} obj", id.number, id.generation)?;
self.serialize_object(obj, writer)?;
writeln!(writer)?;
writeln!(writer, "endobj")?;
writeln!(writer)?;
}
let xref_offset = writer.stream_position()?;
let max_number = doc.objects().keys().map(|id| id.number).max().unwrap_or(0);
let size = max_number + 1;
writeln!(writer, "xref")?;
writeln!(writer, "0 {}", size)?;
for n in 0..=max_number {
if let Some(offset) = offsets.get(&n) {
writeln!(writer, "{:010} 00000 n ", offset)?;
} else {
writeln!(writer, "0000000000 65535 f ")?;
}
}
writeln!(writer, "trailer")?;
write!(
writer,
"<< /Size {} /Root {}",
size,
doc.catalog().unwrap_or(ObjectId::new(1, 0))
)?;
if let Some(info) = doc.info() {
write!(writer, " /Info {}", info)?;
}
writeln!(writer, " >>")?;
writeln!(writer, "startxref")?;
writeln!(writer, "{}", xref_offset)?;
writeln!(writer, "%%EOF")?;
Ok(())
}
fn serialize_object(&self, obj: &PdfObject, writer: &mut impl Write) -> PdfResult<()> {
match obj {
PdfObject::Null => write!(writer, "null")?,
PdfObject::Boolean(b) => write!(writer, "{}", b)?,
PdfObject::Integer(i) => write!(writer, "{}", i)?,
PdfObject::Real(r) => {
if r.is_finite() && r.fract() == 0.0 {
write!(writer, "{r:.1}")?;
} else {
write!(writer, "{r}")?;
}
}
PdfObject::Name(n) => write!(writer, "{}", n)?,
PdfObject::String(s) => write!(writer, "{}", s)?,
PdfObject::Array(a) => {
write!(writer, "[")?;
for (i, item) in a.0.iter().enumerate() {
if i > 0 {
write!(writer, " ")?;
}
self.serialize_object(item, writer)?;
}
write!(writer, "]")?;
}
PdfObject::Dictionary(d) => {
writeln!(writer, "<<")?;
if self.deterministic {
let mut entries: Vec<(&PdfName, &PdfObject)> = d.iter().collect();
entries.sort_by(|(a, _), (b, _)| a.as_str().cmp(b.as_str()));
for (key, value) in entries {
write!(writer, "{} ", key)?;
self.serialize_object(value, writer)?;
writeln!(writer)?;
}
} else {
for (key, value) in d.iter() {
write!(writer, "{} ", key)?;
self.serialize_object(value, writer)?;
writeln!(writer)?;
}
}
write!(writer, ">>")?;
}
PdfObject::Stream(s) => {
let mut dict = s.dictionary.clone();
dict.insert("Length", PdfObject::Integer(s.data.len() as i64));
self.serialize_object(&PdfObject::Dictionary(dict), writer)?;
writeln!(writer)?;
writeln!(writer, "stream")?;
writer.write_all(&s.data)?;
writeln!(writer)?;
write!(writer, "endstream")?;
}
PdfObject::Reference(id) => write!(writer, "{}", id)?,
}
Ok(())
}
}
impl Default for Serializer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use super::*;
use crate::{ObjectId, PdfDictionary, PdfDocument, PdfName, PdfObject};
#[test]
fn xref_entries_are_exactly_20_bytes() {
let mut doc = PdfDocument::new();
doc.set_catalog(ObjectId::new(1, 0));
let mut catalog = PdfDictionary::new();
catalog.insert("Type", PdfObject::Name(PdfName::new("Catalog")));
doc.add_object(ObjectId::new(1, 0), PdfObject::Dictionary(catalog));
let mut buf = Cursor::new(Vec::new());
Serializer::new()
.serialize(&doc, &mut buf)
.expect("serialize");
let bytes = buf.into_inner();
let start = bytes
.windows(b"startxref".len())
.position(|w| w == b"startxref")
.expect("startxref present");
let xref = &bytes[..start];
let mut entry_count = 0;
for line in xref.split(|&b| b == b'\n') {
let is_entry = line.len() == 19
&& line[..10].iter().all(u8::is_ascii_digit)
&& (line.ends_with(b"n ") || line.ends_with(b"f "));
if is_entry {
entry_count += 1;
}
}
assert!(
entry_count >= 2,
"expected at least two 20-byte xref entries, found {entry_count}"
);
}
}