Skip to main content

paperforge_pdf/
serializer.rs

1use std::collections::BTreeMap;
2use std::io::{Seek, Write};
3
4use crate::error::PdfResult;
5use crate::object::*;
6use crate::parser::Document;
7
8pub struct Serializer {
9    deterministic: bool,
10}
11
12impl Serializer {
13    pub fn new() -> Self {
14        Self {
15            deterministic: false,
16        }
17    }
18
19    /// When enabled, dictionary entries are emitted in sorted key order and the
20    /// output is byte-for-byte reproducible across runs and platforms.
21    pub fn with_deterministic(deterministic: bool) -> Self {
22        Self { deterministic }
23    }
24
25    pub fn serialize(&self, doc: &Document, writer: &mut (impl Write + Seek)) -> PdfResult<()> {
26        writeln!(writer, "%PDF-1.7")?;
27        writer.write_all(b"%\xe2\xcf\xd3\xe2\n")?;
28
29        let mut offsets: BTreeMap<u32, u64> = BTreeMap::new();
30        for (id, obj) in doc.objects() {
31            offsets.insert(id.number, writer.stream_position()?);
32            writeln!(writer, "{} {} obj", id.number, id.generation)?;
33            self.serialize_object(obj, writer)?;
34            // A token separator is required between the object body and the
35            // `endobj` keyword: scalar bodies (`null`, `42`, `[...]`) would
36            // otherwise run into it and corrupt the object.
37            writeln!(writer)?;
38            writeln!(writer, "endobj")?;
39            writeln!(writer)?;
40        }
41
42        let xref_offset = writer.stream_position()?;
43        let max_number = doc.objects().keys().map(|id| id.number).max().unwrap_or(0);
44        let size = max_number + 1;
45
46        writeln!(writer, "xref")?;
47        writeln!(writer, "0 {}", size)?;
48        for n in 0..=max_number {
49            // PDF spec (ISO 32000-1 §7.5.4): each xref entry must be exactly
50            // 20 bytes, including the end-of-line marker. The trailing space
51            // pads the entry so a single `\n` keeps it at 20 bytes.
52            if let Some(offset) = offsets.get(&n) {
53                writeln!(writer, "{:010} 00000 n ", offset)?;
54            } else {
55                writeln!(writer, "0000000000 65535 f ")?;
56            }
57        }
58
59        writeln!(writer, "trailer")?;
60        write!(
61            writer,
62            "<< /Size {} /Root {}",
63            size,
64            doc.catalog().unwrap_or(ObjectId::new(1, 0))
65        )?;
66        if let Some(info) = doc.info() {
67            write!(writer, " /Info {}", info)?;
68        }
69        writeln!(writer, " >>")?;
70        writeln!(writer, "startxref")?;
71        writeln!(writer, "{}", xref_offset)?;
72        writeln!(writer, "%%EOF")?;
73        Ok(())
74    }
75
76    fn serialize_object(&self, obj: &PdfObject, writer: &mut impl Write) -> PdfResult<()> {
77        match obj {
78            PdfObject::Null => write!(writer, "null")?,
79            PdfObject::Boolean(b) => write!(writer, "{}", b)?,
80            PdfObject::Integer(i) => write!(writer, "{}", i)?,
81            PdfObject::Real(r) => {
82                // Integral reals must keep a decimal point (`2.0`, not `2`):
83                // Rust's Display drops it, and a reader would round-trip the
84                // object back as an Integer, losing the type. Appending `.0`
85                // is exact for any f64 whose value is an integer, so this is
86                // safe at any magnitude.
87                if r.is_finite() && r.fract() == 0.0 {
88                    write!(writer, "{r:.1}")?;
89                } else {
90                    write!(writer, "{r}")?;
91                }
92            }
93            PdfObject::Name(n) => write!(writer, "{}", n)?,
94            PdfObject::String(s) => write!(writer, "{}", s)?,
95            PdfObject::Array(a) => {
96                write!(writer, "[")?;
97                for (i, item) in a.0.iter().enumerate() {
98                    if i > 0 {
99                        write!(writer, " ")?;
100                    }
101                    self.serialize_object(item, writer)?;
102                }
103                write!(writer, "]")?;
104            }
105            PdfObject::Dictionary(d) => {
106                writeln!(writer, "<<")?;
107                if self.deterministic {
108                    // Sorted key order keeps output byte-for-byte reproducible.
109                    let mut entries: Vec<(&PdfName, &PdfObject)> = d.iter().collect();
110                    entries.sort_by(|(a, _), (b, _)| a.as_str().cmp(b.as_str()));
111                    for (key, value) in entries {
112                        write!(writer, "{} ", key)?;
113                        self.serialize_object(value, writer)?;
114                        writeln!(writer)?;
115                    }
116                } else {
117                    for (key, value) in d.iter() {
118                        write!(writer, "{} ", key)?;
119                        self.serialize_object(value, writer)?;
120                        writeln!(writer)?;
121                    }
122                }
123                write!(writer, ">>")?;
124            }
125            PdfObject::Stream(s) => {
126                // ISO 32000-1 §7.3.8: /Length is required. If the stream was
127                // built without one, emit the true byte length so the output
128                // stays parseable by strict readers.
129                let mut dict = s.dictionary.clone();
130                dict.insert("Length", PdfObject::Integer(s.data.len() as i64));
131                self.serialize_object(&PdfObject::Dictionary(dict), writer)?;
132                writeln!(writer)?;
133                writeln!(writer, "stream")?;
134                writer.write_all(&s.data)?;
135                writeln!(writer)?;
136                write!(writer, "endstream")?;
137            }
138            PdfObject::Reference(id) => write!(writer, "{}", id)?,
139        }
140        Ok(())
141    }
142}
143
144impl Default for Serializer {
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use std::io::Cursor;
153
154    use super::*;
155    use crate::{ObjectId, PdfDictionary, PdfDocument, PdfName, PdfObject};
156
157    #[test]
158    fn xref_entries_are_exactly_20_bytes() {
159        // PDF spec (ISO 32000-1 §7.5.4) requires every xref entry to be exactly
160        // 20 bytes including the EOL marker. Strict parsers (e.g. lopdf) read
161        // fixed-width records, so under-padded entries break interop.
162        let mut doc = PdfDocument::new();
163        doc.set_catalog(ObjectId::new(1, 0));
164        let mut catalog = PdfDictionary::new();
165        catalog.insert("Type", PdfObject::Name(PdfName::new("Catalog")));
166        doc.add_object(ObjectId::new(1, 0), PdfObject::Dictionary(catalog));
167
168        let mut buf = Cursor::new(Vec::new());
169        Serializer::new()
170            .serialize(&doc, &mut buf)
171            .expect("serialize");
172        let bytes = buf.into_inner();
173
174        // The header's binary comment line is not UTF-8, so scan raw bytes.
175        let start = bytes
176            .windows(b"startxref".len())
177            .position(|w| w == b"startxref")
178            .expect("startxref present");
179        let xref = &bytes[..start];
180
181        let mut entry_count = 0;
182        for line in xref.split(|&b| b == b'\n') {
183            // Each entry is 19 content bytes plus a 1-byte `\n` EOL = exactly
184            // 20 bytes, per ISO 32000-1 §7.5.4: `nnnnnnnnnn ggggg n `.
185            let is_entry = line.len() == 19
186                && line[..10].iter().all(u8::is_ascii_digit)
187                && (line.ends_with(b"n ") || line.ends_with(b"f "));
188            if is_entry {
189                entry_count += 1;
190            }
191        }
192        assert!(
193            entry_count >= 2,
194            "expected at least two 20-byte xref entries, found {entry_count}"
195        );
196    }
197}