Skip to main content

rust_pdf_box/cos/
writer.rs

1use crate::cos::document::COSDocument;
2use crate::cos::object::{
3    COSArray, COSDictionary, COSName, COSObject, COSStream, COSString, ObjectId, StringFormat,
4};
5use crate::error::PdfResult;
6use std::collections::BTreeMap;
7use std::io::Write;
8
9pub struct Writer<W: Write> {
10    writer: W,
11    written_bytes: usize,
12}
13
14impl<W: Write> Writer<W> {
15    pub fn new(writer: W) -> Self {
16        Self {
17            writer,
18            written_bytes: 0,
19        }
20    }
21
22    pub fn with_offset(writer: W, offset: usize) -> Self {
23        Self {
24            writer,
25            written_bytes: offset,
26        }
27    }
28
29    pub fn written_bytes(&self) -> usize {
30        self.written_bytes
31    }
32
33    pub fn into_inner(self) -> W {
34        self.writer
35    }
36
37    pub fn write_bytes(&mut self, bytes: &[u8]) -> PdfResult<()> {
38        self.writer.write_all(bytes)?;
39        self.written_bytes += bytes.len();
40        Ok(())
41    }
42
43    pub fn write_str(&mut self, s: &str) -> PdfResult<()> {
44        self.write_bytes(s.as_bytes())
45    }
46
47    pub fn write_line(&mut self, s: &str) -> PdfResult<()> {
48        self.write_str(s)?;
49        self.write_bytes(b"\n")
50    }
51
52    /// Serializes an entire COSDocument into standard PDF format.
53    pub fn write_document(&mut self, doc: &COSDocument) -> PdfResult<()> {
54        // 1. Header
55        self.write_line(&format!("%PDF-{}", doc.version))?;
56        // 4 binary bytes > 127 to indicate binary file
57        self.write_bytes(b"%\xE2\xE3\xCF\xD3\n")?;
58
59        let mut offsets: BTreeMap<ObjectId, usize> = BTreeMap::new();
60
61        // 2. Body: write all indirect objects in order
62        for (&id, obj) in &doc.objects {
63            offsets.insert(id, self.written_bytes);
64            self.write_indirect_object(id, obj)?;
65        }
66
67        // 3. Cross-reference table
68        let xref_offset = self.written_bytes;
69        let size = (doc.max_object_number + 1) as usize;
70
71        self.write_line("xref")?;
72        self.write_line(&format!("0 {}", size))?;
73
74        // Object 0 (head of free list)
75        self.write_str("0000000000 65535 f \n")?;
76
77        for num in 1..=doc.max_object_number {
78            let id = ObjectId::new(num, 0);
79            if let Some(&off) = offsets.get(&id) {
80                self.write_str(&format!("{:010} {:05} n \n", off, id.generation))?;
81            } else {
82                self.write_str("0000000000 00000 f \n")?;
83            }
84        }
85
86        // 4. Trailer
87        let mut trailer = doc.trailer.clone();
88        trailer.set("Size", size as i64);
89
90        self.write_line("trailer")?;
91        self.write_object(&COSObject::Dictionary(trailer))?;
92        self.write_line("")?;
93
94        // 5. Startxref & EOF
95        self.write_line("startxref")?;
96        self.write_line(&format!("{}", xref_offset))?;
97        self.write_line("%%EOF")?;
98
99        Ok(())
100    }
101
102    /// Appends an incremental update to an existing PDF buffer.
103    pub fn write_incremental_update(
104        &mut self,
105        doc: &COSDocument,
106        prev_startxref: usize,
107    ) -> PdfResult<()> {
108        let mut updated_offsets: BTreeMap<ObjectId, usize> = BTreeMap::new();
109
110        // Write only dirty objects
111        for &id in &doc.dirty_objects {
112            if let Some(obj) = doc.get_object(id) {
113                updated_offsets.insert(id, self.written_bytes);
114                self.write_indirect_object(id, obj)?;
115            }
116        }
117
118        let xref_offset = self.written_bytes;
119        self.write_line("xref")?;
120
121        // Group updated objects into contiguous sections or single entries
122        for (&id, &off) in &updated_offsets {
123            self.write_line(&format!("{} 1", id.num))?;
124            self.write_str(&format!("{:010} {:05} n \n", off, id.generation))?;
125        }
126
127        // Trailer pointing to previous xref
128        let mut trailer = doc.trailer.clone();
129        let size = (doc.max_object_number + 1) as i64;
130        trailer.set("Size", size);
131        trailer.set("Prev", prev_startxref as i64);
132
133        self.write_line("trailer")?;
134        self.write_object(&COSObject::Dictionary(trailer))?;
135        self.write_line("")?;
136
137        // Startxref & EOF
138        self.write_line("startxref")?;
139        self.write_line(&format!("{}", xref_offset))?;
140        self.write_line("%%EOF")?;
141
142        Ok(())
143    }
144
145    pub fn write_indirect_object(&mut self, id: ObjectId, obj: &COSObject) -> PdfResult<()> {
146        self.write_line(&format!("{} {} obj", id.num, id.generation))?;
147        self.write_object(obj)?;
148        self.write_line("\nendobj")?;
149        Ok(())
150    }
151
152    pub fn write_object(&mut self, obj: &COSObject) -> PdfResult<()> {
153        match obj {
154            COSObject::Null => self.write_str("null"),
155            COSObject::Boolean(b) => self.write_str(if *b { "true" } else { "false" }),
156            COSObject::Integer(i) => self.write_str(&format!("{}", i)),
157            COSObject::Real(f) => {
158                if f.fract() == 0.0 {
159                    self.write_str(&format!("{:.1}", f))
160                } else {
161                    let mut s = format!("{:.5}", f);
162                    while s.ends_with('0') && s.contains('.') {
163                        s.pop();
164                    }
165                    if s.ends_with('.') {
166                        s.pop();
167                    }
168                    self.write_str(&s)
169                }
170            }
171            COSObject::String(s) => self.write_string(s),
172            COSObject::Name(n) => self.write_name(n),
173            COSObject::Array(a) => self.write_array(a),
174            COSObject::Dictionary(d) => self.write_dictionary(d),
175            COSObject::Stream(s) => self.write_stream(s),
176            COSObject::Reference(id) => self.write_str(&format!("{} {} R", id.num, id.generation)),
177        }
178    }
179
180    pub fn write_name(&mut self, name: &COSName) -> PdfResult<()> {
181        self.write_str("/")?;
182        for &b in name.as_str().as_bytes() {
183            if matches!(b, 33..=126)
184                && !matches!(
185                    b,
186                    b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%' | b'#'
187                )
188            {
189                self.write_bytes(&[b])?;
190            } else {
191                self.write_str(&format!("#{:02X}", b))?;
192            }
193        }
194        Ok(())
195    }
196
197    pub fn write_string(&mut self, s: &COSString) -> PdfResult<()> {
198        match s.format {
199            StringFormat::Literal => {
200                self.write_str("(")?;
201                for &b in &s.data {
202                    match b {
203                        b'(' => self.write_str(r"\(")?,
204                        b')' => self.write_str(r"\)")?,
205                        b'\\' => self.write_str(r"\\")?,
206                        b'\r' => self.write_str(r"\r")?,
207                        b'\n' => self.write_str(r"\n")?,
208                        _ => self.write_bytes(&[b])?,
209                    }
210                }
211                self.write_str(")")
212            }
213            StringFormat::Hex => {
214                self.write_str("<")?;
215                for &b in &s.data {
216                    self.write_str(&format!("{:02X}", b))?;
217                }
218                self.write_str(">")
219            }
220        }
221    }
222
223    pub fn write_array(&mut self, arr: &COSArray) -> PdfResult<()> {
224        self.write_str("[")?;
225        for (i, item) in arr.iter().enumerate() {
226            if i > 0 {
227                self.write_str(" ")?;
228            }
229            self.write_object(item)?;
230        }
231        self.write_str("]")
232    }
233
234    pub fn write_dictionary(&mut self, dict: &COSDictionary) -> PdfResult<()> {
235        self.write_line("<<")?;
236        for (key, val) in &dict.entries {
237            self.write_str("  ")?;
238            self.write_name(key)?;
239            self.write_str(" ")?;
240            self.write_object(val)?;
241            self.write_line("")?;
242        }
243        self.write_str(">>")
244    }
245
246    pub fn write_stream(&mut self, stream: &COSStream) -> PdfResult<()> {
247        let mut dict = stream.dict.clone();
248        dict.set("Length", stream.data.len() as i64);
249
250        self.write_dictionary(&dict)?;
251        self.write_line("")?;
252        self.write_line("stream")?;
253        self.write_bytes(&stream.data)?;
254        self.write_line("")?;
255        self.write_str("endstream")
256    }
257}