Skip to main content

pivot_pdf/
writer.rs

1use std::io::{self, Write};
2
3use crate::objects::{ObjId, PdfObject};
4
5/// Low-level PDF binary writer. Serializes PDF objects to any
6/// `Write` target while tracking byte offsets for the xref table.
7pub struct PdfWriter<W: Write> {
8    writer: W,
9    offset: usize,
10    xref_entries: Vec<(u32, usize)>,
11}
12
13impl<W: Write> PdfWriter<W> {
14    /// Create a new `PdfWriter` wrapping the given writer.
15    pub fn new(writer: W) -> Self {
16        PdfWriter {
17            writer,
18            offset: 0,
19            xref_entries: Vec::new(),
20        }
21    }
22
23    /// Write raw bytes, tracking the byte offset.
24    fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> {
25        self.writer.write_all(data)?;
26        self.offset += data.len();
27        Ok(())
28    }
29
30    /// Write a formatted string, tracking the byte offset.
31    fn write_str(&mut self, s: &str) -> io::Result<()> {
32        self.write_bytes(s.as_bytes())
33    }
34
35    /// Write the PDF 1.7 header and binary comment.
36    pub fn write_header(&mut self) -> io::Result<()> {
37        self.write_str("%PDF-1.7\n")?;
38        // Binary comment: 4 bytes >= 128 for binary detection.
39        self.write_bytes(b"%\xe2\xe3\xcf\xd3\n")?;
40        Ok(())
41    }
42
43    /// Write an indirect object, recording its byte offset for xref.
44    pub fn write_object(&mut self, id: ObjId, obj: &PdfObject) -> io::Result<()> {
45        self.xref_entries.push((id.0, self.offset));
46        self.write_str(&format!("{} {} obj\n", id.0, id.1))?;
47        self.write_pdf_object(obj)?;
48        self.write_str("\nendobj\n")?;
49        Ok(())
50    }
51
52    /// Serialize a PdfObject to its PDF text representation.
53    fn write_pdf_object(&mut self, obj: &PdfObject) -> io::Result<()> {
54        match obj {
55            PdfObject::Null => self.write_str("null"),
56            PdfObject::Boolean(b) => {
57                if *b {
58                    self.write_str("true")
59                } else {
60                    self.write_str("false")
61                }
62            }
63            PdfObject::Integer(n) => self.write_str(&n.to_string()),
64            PdfObject::Real(f) => {
65                let s = format_real(*f);
66                self.write_str(&s)
67            }
68            PdfObject::Name(name) => {
69                self.write_str("/")?;
70                self.write_str(name)
71            }
72            PdfObject::LiteralString(s) => {
73                self.write_str("(")?;
74                self.write_str(&escape_pdf_string(s))?;
75                self.write_str(")")
76            }
77            PdfObject::Array(items) => {
78                self.write_str("[")?;
79                for (i, item) in items.iter().enumerate() {
80                    if i > 0 {
81                        self.write_str(" ")?;
82                    }
83                    self.write_pdf_object(item)?;
84                }
85                self.write_str("]")
86            }
87            PdfObject::Dictionary(entries) => {
88                self.write_str("<<")?;
89                for (key, val) in entries {
90                    self.write_str(" /")?;
91                    self.write_str(key)?;
92                    self.write_str(" ")?;
93                    self.write_pdf_object(val)?;
94                }
95                self.write_str(" >>")
96            }
97            PdfObject::Stream { dict, data } => {
98                self.write_str("<<")?;
99                for (key, val) in dict {
100                    self.write_str(" /")?;
101                    self.write_str(key)?;
102                    self.write_str(" ")?;
103                    self.write_pdf_object(val)?;
104                }
105                self.write_str(" /Length ")?;
106                self.write_str(&data.len().to_string())?;
107                self.write_str(" >>\nstream\n")?;
108                self.write_bytes(data)?;
109                self.write_str("\nendstream")
110            }
111            PdfObject::Reference(id) => self.write_str(&format!("{} {} R", id.0, id.1)),
112        }
113    }
114
115    /// Current byte offset in the output.
116    pub fn current_offset(&self) -> usize {
117        self.offset
118    }
119
120    /// Write xref table, trailer, startxref, and %%EOF.
121    pub fn write_xref_and_trailer(
122        &mut self,
123        root_id: ObjId,
124        info_id: Option<ObjId>,
125    ) -> io::Result<()> {
126        let xref_offset = self.offset;
127
128        // Sort xref entries by object number.
129        self.xref_entries.sort_by_key(|&(num, _)| num);
130
131        let max_obj = self.xref_entries.last().map(|&(num, _)| num).unwrap_or(0);
132        let size = max_obj + 1;
133
134        self.write_str("xref\n")?;
135        self.write_str(&format!("0 {}\n", size))?;
136
137        // Object 0: free entry head (exactly 20 bytes).
138        self.write_bytes(b"0000000000 65535 f\r\n")?;
139
140        // Build a map for quick lookup.
141        let mut offset_map = std::collections::HashMap::new();
142        for &(num, off) in &self.xref_entries {
143            offset_map.insert(num, off);
144        }
145
146        // Write entries for objects 1..max_obj.
147        for obj_num in 1..size {
148            if let Some(&off) = offset_map.get(&obj_num) {
149                let entry = format!("{:010} {:05} n\r\n", off, 0);
150                self.write_bytes(entry.as_bytes())?;
151            } else {
152                // Free entry for gaps.
153                self.write_bytes(b"0000000000 00000 f\r\n")?;
154            }
155        }
156
157        // Trailer.
158        self.write_str("trailer\n")?;
159        self.write_str(&format!(
160            "<< /Size {} /Root {} {} R",
161            size, root_id.0, root_id.1,
162        ))?;
163        if let Some(info) = info_id {
164            self.write_str(&format!(" /Info {} {} R", info.0, info.1,))?;
165        }
166        self.write_str(" >>\n")?;
167
168        self.write_str("startxref\n")?;
169        self.write_str(&format!("{}\n", xref_offset))?;
170        self.write_str("%%EOF\n")?;
171
172        Ok(())
173    }
174
175    /// Return the inner writer, consuming this PdfWriter.
176    pub fn into_inner(self) -> W {
177        self.writer
178    }
179}
180
181/// Escape special characters in a PDF literal string.
182pub fn escape_pdf_string(s: &str) -> String {
183    let mut result = String::with_capacity(s.len());
184    for c in s.chars() {
185        match c {
186            '\\' => result.push_str("\\\\"),
187            '(' => result.push_str("\\("),
188            ')' => result.push_str("\\)"),
189            _ => result.push(c),
190        }
191    }
192    result
193}
194
195/// Format a float for PDF output: no trailing zeros,
196/// no scientific notation.
197fn format_real(f: f64) -> String {
198    if f == f.floor() && f.abs() < 1e15 {
199        format!("{:.1}", f)
200    } else {
201        let s = format!("{:.6}", f);
202        let s = s.trim_end_matches('0');
203        let s = s.trim_end_matches('.');
204        s.to_string()
205    }
206}