Skip to main content

pdfluent_lopdf/
writer.rs

1use std::fs::File;
2use std::io::{BufWriter, Result, Write};
3use std::path::Path;
4use std::vec;
5
6use super::Object::*;
7use super::{Dictionary, Document, Object, Stream, StringFormat};
8use crate::{IncrementalDocument, xref::*};
9
10impl Document {
11    /// Save PDF document to specified file path.
12    #[inline]
13    pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<File> {
14        let mut file = BufWriter::new(File::create(path)?);
15        self.save_internal(&mut file)?;
16        Ok(file.into_inner()?)
17    }
18
19    /// Save PDF to arbitrary target
20    #[inline]
21    pub fn save_to<W: Write>(&mut self, target: &mut W) -> Result<()> {
22        self.save_internal(target)
23    }
24
25    /// Save PDF with custom options
26    pub fn save_with_options<W: Write>(
27        &mut self,
28        target: &mut W,
29        options: crate::SaveOptions,
30    ) -> Result<()> {
31        if options.use_object_streams {
32            self.save_with_object_streams(target, options)
33        } else {
34            self.save_internal(target)
35        }
36    }
37
38    /// Save PDF with modern features (object streams and cross-reference streams)
39    pub fn save_modern<W: Write>(&mut self, target: &mut W) -> Result<()> {
40        let options = crate::SaveOptions {
41            use_object_streams: true,
42            use_xref_streams: true,
43            ..Default::default()
44        };
45        self.save_with_options(target, options)
46    }
47
48    fn save_internal<W: Write>(&mut self, target: &mut W) -> Result<()> {
49        let mut target = CountingWrite {
50            inner: target,
51            bytes_written: 0,
52        };
53
54        let mut xref = Xref::new(self.max_id + 1, self.reference_table.cross_reference_type);
55        writeln!(target, "%PDF-{}", self.version)?;
56
57        Writer::write_binary_mark(&mut target, &self.binary_mark)?;
58
59        for (&(id, generation), object) in &self.objects {
60            if object
61                .type_name()
62                .map(|name| {
63                    [
64                        b"ObjStm".as_slice(),
65                        b"XRef".as_slice(),
66                        b"Linearized".as_slice(),
67                    ]
68                    .contains(&name)
69                })
70                .ok()
71                != Some(true)
72            {
73                Writer::write_indirect_object(&mut target, id, generation, object, &mut xref)?;
74            }
75        }
76
77        let xref_start = target.bytes_written;
78
79        // Pick right cross reference stream.
80        match xref.cross_reference_type {
81            XrefType::CrossReferenceTable => {
82                Writer::write_xref(&mut target, &xref)?;
83                self.write_trailer(&mut target)?;
84            }
85            XrefType::CrossReferenceStream => {
86                // Cross Reference Stream instead of XRef and Trailer
87                self.write_cross_reference_stream(&mut target, &mut xref, xref_start as u32)?;
88            }
89        }
90        // Write `startxref` part of trailer
91        write!(target, "\nstartxref\n{xref_start}\n%%EOF")?;
92
93        Ok(())
94    }
95
96    /// Save PDF with object streams enabled
97    fn save_with_object_streams<W: Write>(
98        &mut self,
99        target: &mut W,
100        options: crate::SaveOptions,
101    ) -> Result<()> {
102        use crate::ObjectStream;
103        use std::collections::HashMap;
104
105        let mut target = CountingWrite {
106            inner: target,
107            bytes_written: 0,
108        };
109
110        // Ensure PDF version is at least 1.5 (required for object streams)
111        if self.version.as_str() < "1.5" {
112            self.version = "1.5".to_string();
113        }
114
115        // Update cross-reference type if requested
116        if options.use_xref_streams {
117            self.reference_table.cross_reference_type = XrefType::CrossReferenceStream;
118        }
119
120        let mut xref = Xref::new(self.max_id + 1, self.reference_table.cross_reference_type);
121        writeln!(target, "%PDF-{}", self.version)?;
122        Writer::write_binary_mark(&mut target, &self.binary_mark)?;
123
124        // Organize objects into streams
125        let mut object_streams: Vec<crate::ObjectStream> = Vec::new();
126        let mut objects_to_write_directly = Vec::new();
127        let mut object_to_stream_map = HashMap::new();
128
129        // Categorize objects
130        for (&(id, generation), object) in &self.objects {
131            // Skip existing object streams - we'll create new ones
132            if let Object::Stream(stream) = object {
133                if let Ok(type_obj) = stream.dict.get(b"Type") {
134                    if let Ok(type_name) = type_obj.as_name() {
135                        if type_name == b"ObjStm" {
136                            continue; // Skip existing object streams
137                        }
138                    }
139                }
140            }
141
142            if generation == 0 && ObjectStream::can_be_compressed((id, generation), object, self) {
143                // Object can be compressed
144                // Find or create an object stream for it
145                let stream_index = object_streams.len().saturating_sub(1);
146
147                if object_streams.is_empty()
148                    || object_streams[stream_index].object_count()
149                        >= options.object_stream_config.max_objects_per_stream
150                {
151                    // Create new object stream
152                    let new_stream = ObjectStream::builder()
153                        .max_objects(options.object_stream_config.max_objects_per_stream)
154                        .compression_level(options.object_stream_config.compression_level)
155                        .build();
156                    object_streams.push(new_stream);
157                }
158
159                let stream_index = object_streams.len() - 1;
160                object_streams[stream_index]
161                    .add_object((id, generation), object.clone())
162                    .ok();
163                object_to_stream_map.insert((id, generation), stream_index);
164            } else {
165                // Object must be written directly
166                objects_to_write_directly.push(((id, generation), object));
167            }
168        }
169
170        // Write direct objects first
171        for ((id, generation), object) in objects_to_write_directly {
172            Writer::write_indirect_object(&mut target, id, generation, object, &mut xref)?;
173        }
174
175        // Write object streams
176        let mut stream_count = 0;
177        for obj_stream in object_streams.into_iter() {
178            let stream_id = self.max_id + 1 + stream_count;
179            let stream_obj = obj_stream
180                .to_stream_object()
181                .map_err(std::io::Error::other)?;
182
183            // Record compressed objects in xref
184            // Must use the same sort order as build_stream_content()
185            let mut sorted_objects: Vec<_> = obj_stream.objects.keys().cloned().collect();
186            sorted_objects.sort_by_key(|id| *id);
187            for (index_in_stream, (obj_id, _gen)) in sorted_objects.iter().enumerate() {
188                xref.insert(
189                    *obj_id,
190                    XrefEntry::Compressed {
191                        container: stream_id,
192                        index: index_in_stream as u16,
193                    },
194                );
195            }
196
197            // Write the object stream
198            Writer::write_indirect_object(
199                &mut target,
200                stream_id,
201                0,
202                &Object::Stream(stream_obj),
203                &mut xref,
204            )?;
205            stream_count += 1;
206        }
207
208        // Update max_id to account for object streams
209        self.max_id += stream_count;
210
211        let xref_start = target.bytes_written;
212
213        // Write cross-reference
214        match xref.cross_reference_type {
215            XrefType::CrossReferenceTable => {
216                Writer::write_xref(&mut target, &xref)?;
217                self.write_trailer(&mut target)?;
218            }
219            XrefType::CrossReferenceStream => {
220                self.write_cross_reference_stream(&mut target, &mut xref, xref_start as u32)?;
221            }
222        }
223
224        write!(target, "\nstartxref\n{xref_start}\n%%EOF")?;
225        Ok(())
226    }
227
228    /// Write the Cross Reference Stream.
229    ///
230    /// Insert an `Object` to the end of the PDF (not visible when inspecting `Document`).
231    /// Note: This is different from the "Cross Reference Table".
232    fn write_cross_reference_stream<W: Write>(
233        &mut self,
234        file: &mut CountingWrite<&mut W>,
235        xref: &mut Xref,
236        xref_start: u32,
237    ) -> Result<()> {
238        // Increment max_id to account for CRS.
239        self.max_id += 1;
240        let new_obj_id_for_crs = self.max_id;
241        xref.insert(
242            new_obj_id_for_crs,
243            XrefEntry::Normal {
244                offset: xref_start,
245                generation: 0,
246            },
247        );
248        self.trailer.set("Type", Name(b"XRef".to_vec()));
249        // Update `max_id` in trailer
250        self.trailer.set("Size", i64::from(self.max_id + 1));
251        // Set the size of each entry in bytes (default for PDFs is `[1 2 1]`)
252        // In our case we use `[u8, u32, u16]` for each entry
253        // to keep things simple and working at all times.
254        self.trailer
255            .set("W", Array(vec![Integer(1), Integer(4), Integer(2)]));
256        // Note that `ASCIIHexDecode` does not work correctly,
257        // but is still useful for debugging sometimes.
258        let filter = XRefStreamFilter::None;
259        let (stream, stream_length, indexes) = Writer::create_xref_steam(xref, filter)?;
260        self.trailer.set("Index", indexes);
261
262        if filter == XRefStreamFilter::ASCIIHexDecode {
263            self.trailer.set("Filter", Name(b"ASCIIHexDecode".to_vec()));
264        } else {
265            self.trailer.remove(b"Filter");
266        }
267
268        self.trailer.set("Length", stream_length as i64);
269
270        let trailer = &self.trailer;
271        let cross_reference_stream = Stream(Stream {
272            dict: trailer.clone(),
273            allows_compression: true,
274            content: stream,
275            start_position: None,
276        });
277        // Insert Cross Reference Stream as an `Object` to the end of the PDF.
278        // The `Object` is not added to `Document` because it is generated every time you save.
279        Writer::write_indirect_object(file, new_obj_id_for_crs, 0, &cross_reference_stream, xref)?;
280
281        Ok(())
282    }
283
284    fn write_trailer(&mut self, file: &mut dyn Write) -> Result<()> {
285        self.trailer.set("Size", i64::from(self.max_id + 1));
286        file.write_all(b"trailer\n")?;
287        Writer::write_dictionary(file, &self.trailer)?;
288        Ok(())
289    }
290}
291
292impl IncrementalDocument {
293    /// Save PDF document to specified file path.
294    #[inline]
295    pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<File> {
296        let mut file = BufWriter::new(File::create(path)?);
297        self.save_internal(&mut file)?;
298        Ok(file.into_inner()?)
299    }
300
301    /// Save PDF to arbitrary target
302    #[inline]
303    pub fn save_to<W: Write>(&mut self, target: &mut W) -> Result<()> {
304        self.save_internal(target)
305    }
306
307    fn save_internal<W: Write>(&mut self, target: &mut W) -> Result<()> {
308        let mut target = CountingWrite {
309            inner: target,
310            bytes_written: 0,
311        };
312
313        // Write previous document versions.
314        let prev_document_bytes = self.get_prev_documents_bytes();
315        target.inner.write_all(prev_document_bytes)?;
316        target.bytes_written += prev_document_bytes.len();
317
318        // Write/Append new document version.
319        let mut xref = Xref::new(
320            self.new_document.max_id + 1,
321            self.get_prev_documents()
322                .reference_table
323                .cross_reference_type,
324        );
325
326        if let Some(last_byte) = prev_document_bytes.last() {
327            if *last_byte != b'\n' {
328                // Add a newline if it was not already present
329                writeln!(target)?;
330            }
331        }
332        writeln!(target, "%PDF-{}", self.new_document.version)?;
333
334        Writer::write_binary_mark(&mut target, &self.new_document.binary_mark)?;
335
336        for (&(id, generation), object) in &self.new_document.objects {
337            if object
338                .type_name()
339                .map(|name| {
340                    [
341                        b"ObjStm".as_slice(),
342                        b"XRef".as_slice(),
343                        b"Linearized".as_slice(),
344                    ]
345                    .contains(&name)
346                })
347                .ok()
348                != Some(true)
349            {
350                Writer::write_indirect_object(&mut target, id, generation, object, &mut xref)?;
351            }
352        }
353
354        let xref_start = target.bytes_written;
355
356        // Pick right cross reference stream.
357        match xref.cross_reference_type {
358            XrefType::CrossReferenceTable => {
359                Writer::write_xref(&mut target, &xref)?;
360                self.new_document.write_trailer(&mut target)?;
361            }
362            XrefType::CrossReferenceStream => {
363                // Cross Reference Stream instead of XRef and Trailer
364                self.new_document.write_cross_reference_stream(
365                    &mut target,
366                    &mut xref,
367                    xref_start as u32,
368                )?;
369            }
370        }
371        // Write `startxref` part of trailer
372        write!(target, "\nstartxref\n{xref_start}\n%%EOF")?;
373
374        Ok(())
375    }
376}
377
378pub struct Writer;
379
380#[derive(Debug, PartialEq, Eq, Clone, Copy)]
381pub enum XRefStreamFilter {
382    ASCIIHexDecode,
383    _FlateDecode, //this is generally a Zlib compressed Stream.
384    None,
385}
386
387impl Writer {
388    fn need_separator(object: &Object) -> bool {
389        matches!(
390            *object,
391            Null | Boolean(_) | Integer(_) | Real(_) | Reference(_)
392        )
393    }
394
395    fn need_end_separator(object: &Object) -> bool {
396        matches!(
397            *object,
398            Null | Boolean(_) | Integer(_) | Real(_) | Name(_) | Reference(_) | Object::Stream(_)
399        )
400    }
401
402    /// Write Cross Reference Table.
403    ///
404    /// Note: This is different from a "Cross Reference Stream".
405    fn write_xref(file: &mut dyn Write, xref: &Xref) -> Result<()> {
406        file.write_all(b"xref\n")?;
407
408        let mut xref_section = XrefSection::new(0);
409        // Add first (0) entry
410        xref_section.add_unusable_free_entry();
411
412        for obj_id in 1..xref.size {
413            // If section is empty change number of starting id.
414            if xref_section.is_empty() {
415                xref_section = XrefSection::new(obj_id);
416            }
417            if let Some(entry) = xref.get(obj_id) {
418                match *entry {
419                    XrefEntry::Normal { offset, generation } => {
420                        // Add entry
421                        xref_section.add_entry(XrefEntry::Normal { offset, generation });
422                    }
423                    XrefEntry::Compressed {
424                        container: _,
425                        index: _,
426                    } => {
427                        xref_section.add_unusable_free_entry();
428                    }
429                    XrefEntry::Free => {
430                        xref_section.add_entry(XrefEntry::Free);
431                    }
432                    XrefEntry::UnusableFree => {
433                        xref_section.add_unusable_free_entry();
434                    }
435                }
436            } else {
437                // Skip over `obj_id`, but finish section if not empty.
438                if !xref_section.is_empty() {
439                    xref_section.write_xref_section(file)?;
440                    xref_section = XrefSection::new(obj_id);
441                }
442            }
443        }
444        // Print last section
445        if !xref_section.is_empty() {
446            xref_section.write_xref_section(file)?;
447        }
448        Ok(())
449    }
450
451    /// Create stream for Cross reference stream.
452    fn create_xref_steam(
453        xref: &Xref,
454        filter: XRefStreamFilter,
455    ) -> Result<(Vec<u8>, usize, Object)> {
456        let mut xref_sections = Vec::new();
457        let mut xref_section = XrefSection::new(0);
458
459        for obj_id in 1..xref.size + 1 {
460            // If section is empty change number of starting id.
461            if xref_section.is_empty() {
462                xref_section = XrefSection::new(obj_id);
463            }
464            if let Some(entry) = xref.get(obj_id) {
465                xref_section.add_entry(entry.clone());
466            } else {
467                // Skip over but finish section if not empty
468                if !xref_section.is_empty() {
469                    xref_sections.push(xref_section);
470                    xref_section = XrefSection::new(obj_id);
471                }
472            }
473        }
474        // Print last section
475        if !xref_section.is_empty() {
476            xref_sections.push(xref_section);
477        }
478
479        let mut xref_stream = Vec::new();
480        let mut xref_index = Vec::new();
481
482        for section in xref_sections {
483            // Add indexes to list
484            xref_index.push(Integer(section.starting_id as i64));
485            xref_index.push(Integer(section.entries.len() as i64));
486            // Add entries to stream
487            for (obj_id, entry) in (section.starting_id..).zip(section.entries) {
488                match entry {
489                    XrefEntry::Free => {
490                        // Type 0
491                        xref_stream.push(0);
492                        xref_stream.extend(obj_id.to_be_bytes());
493                        xref_stream.extend(vec![0, 0]); // TODO add generation number
494                    }
495                    XrefEntry::UnusableFree => {
496                        // Type 0
497                        xref_stream.push(0);
498                        xref_stream.extend(obj_id.to_be_bytes());
499                        xref_stream.extend(65535_u16.to_be_bytes());
500                    }
501                    XrefEntry::Normal { offset, generation } => {
502                        // Type 1
503                        xref_stream.push(1);
504                        xref_stream.extend(offset.to_be_bytes());
505                        xref_stream.extend(generation.to_be_bytes());
506                    }
507                    XrefEntry::Compressed { container, index } => {
508                        // Type 2
509                        xref_stream.push(2);
510                        xref_stream.extend(container.to_be_bytes());
511                        xref_stream.extend(index.to_be_bytes());
512                    }
513                }
514            }
515        }
516
517        // The end of line character should not be counted, added later.
518        let stream_length = xref_stream.len();
519
520        if filter == XRefStreamFilter::ASCIIHexDecode {
521            xref_stream = xref_stream
522                .iter()
523                .flat_map(|c| format!("{c:02X}").as_bytes().to_vec())
524                .collect::<Vec<u8>>();
525        }
526
527        Ok((xref_stream, stream_length, Array(xref_index)))
528    }
529
530    fn write_indirect_object<W: Write>(
531        file: &mut CountingWrite<&mut W>,
532        id: u32,
533        generation: u16,
534        object: &Object,
535        xref: &mut Xref,
536    ) -> Result<()> {
537        let offset = file.bytes_written as u32;
538        xref.insert(id, XrefEntry::Normal { offset, generation });
539        write!(
540            file,
541            "{} {} obj\n{}",
542            id,
543            generation,
544            if Writer::need_separator(object) {
545                " "
546            } else {
547                ""
548            }
549        )?;
550        Writer::write_object(file, object)?;
551        write!(
552            file,
553            "{}\nendobj\n",
554            if Writer::need_end_separator(object) {
555                " "
556            } else {
557                ""
558            }
559        )?;
560        Ok(())
561    }
562
563    pub fn write_object(file: &mut dyn Write, object: &Object) -> Result<()> {
564        match object {
565            Null => file.write_all(b"null"),
566            Boolean(value) => {
567                if *value {
568                    file.write_all(b"true")
569                } else {
570                    file.write_all(b"false")
571                }
572            }
573            Integer(value) => {
574                let mut buf = itoa::Buffer::new();
575                file.write_all(buf.format(*value).as_bytes())
576            }
577            Real(value) => {
578                let v = *value;
579                if !v.is_finite() {
580                    // infinity/NaN can't be represented in PDF — write 0.
581                    // Occurs when a parsed value overflows f32 (e.g. 3.4029e38).
582                    // (#content_roundtrip)
583                    file.write_all(b"0")
584                } else {
585                    let s = format!("{v}");
586                    if s.contains('.') {
587                        file.write_all(s.as_bytes())
588                    } else {
589                        // Integer-valued or very large float with no decimal point.
590                        // Append ".0" so lopdf's parser recognises it as Real, not Integer.
591                        // Without this, values like 340272...000 are too large for i64
592                        // and fail to parse, producing an op-count mismatch. (#content_roundtrip)
593                        file.write_all(s.as_bytes())?;
594                        file.write_all(b".0")
595                    }
596                }
597            }
598            Name(name) => Writer::write_name(file, name),
599            String(text, format) => Writer::write_string(file, text, format),
600            Array(array) => Writer::write_array(file, array),
601            Object::Dictionary(dict) => Writer::write_dictionary(file, dict),
602            Object::Stream(stream) => Writer::write_stream(file, stream),
603            Reference(id) => write!(file, "{} {} R", id.0, id.1),
604        }
605    }
606
607    fn write_name(file: &mut dyn Write, name: &[u8]) -> Result<()> {
608        file.write_all(b"/")?;
609        for &byte in name {
610            // white-space and delimiter chars are encoded to # sequences
611            // also encode bytes outside of the range 33 (!) to 126 (~)
612            if b" \t\n\r\x0C()<>[]{}/%#".contains(&byte) || !(33..=126).contains(&byte) {
613                write!(file, "#{byte:02X}")?;
614            } else {
615                file.write_all(&[byte])?;
616            }
617        }
618        Ok(())
619    }
620
621    fn write_string(file: &mut dyn Write, text: &[u8], format: &StringFormat) -> Result<()> {
622        match *format {
623            // Within a Literal string, backslash (\) and unbalanced parentheses should be escaped.
624            // This rule apply to each individual byte in a string object,
625            // whether the string is interpreted as single-byte or multiple-byte character codes.
626            // If an end-of-line marker appears within a literal string without a preceding backslash, the result is
627            // equivalent to \n. So \r also need be escaped.
628            StringFormat::Literal => {
629                let mut escape_indice = Vec::new();
630                let mut parentheses = Vec::new();
631                for (index, &byte) in text.iter().enumerate() {
632                    match byte {
633                        b'(' => parentheses.push(index),
634                        b')' => {
635                            if !parentheses.is_empty() {
636                                parentheses.pop();
637                            } else {
638                                escape_indice.push(index);
639                            }
640                        }
641                        b'\\' | b'\r' => escape_indice.push(index),
642                        _ => continue,
643                    }
644                }
645                escape_indice.append(&mut parentheses);
646
647                file.write_all(b"(")?;
648                if !escape_indice.is_empty() {
649                    for (index, &byte) in text.iter().enumerate() {
650                        if escape_indice.contains(&index) {
651                            file.write_all(b"\\")?;
652                            file.write_all(&[if byte == b'\r' { b'r' } else { byte }])?;
653                        } else {
654                            file.write_all(&[byte])?;
655                        }
656                    }
657                } else {
658                    file.write_all(text)?;
659                }
660                file.write_all(b")")?;
661            }
662            StringFormat::Hexadecimal => {
663                file.write_all(b"<")?;
664                for &byte in text {
665                    write!(file, "{byte:02X}")?;
666                }
667                file.write_all(b">")?;
668            }
669        }
670        Ok(())
671    }
672
673    fn write_array(file: &mut dyn Write, array: &[Object]) -> Result<()> {
674        file.write_all(b"[")?;
675        let mut first = true;
676        for object in array {
677            if first {
678                first = false;
679            } else if Writer::need_separator(object) {
680                file.write_all(b" ")?;
681            }
682            Writer::write_object(file, object)?;
683        }
684        file.write_all(b"]")?;
685        Ok(())
686    }
687
688    fn write_dictionary(file: &mut dyn Write, dictionary: &Dictionary) -> Result<()> {
689        file.write_all(b"<<")?;
690        for (key, value) in dictionary {
691            Writer::write_name(file, key)?;
692            if Writer::need_separator(value) {
693                file.write_all(b" ")?;
694            }
695            Writer::write_object(file, value)?;
696        }
697        file.write_all(b">>")?;
698        Ok(())
699    }
700
701    fn write_stream(file: &mut dyn Write, stream: &Stream) -> Result<()> {
702        // Ensure /Length matches actual content length. A wrong /Length causes
703        // PDF parsers (including veraPDF) to misparse stream boundaries, which
704        // can expose binary stream bytes as structural PDF tokens and trigger
705        // false §6.1.6 hex-string violations.
706        let actual_len = stream.content.len() as i64;
707        let length_ok = matches!(
708            stream.dict.get(b"Length").ok(),
709            Some(&Object::Integer(l)) if l == actual_len
710        );
711        if length_ok {
712            Writer::write_dictionary(file, &stream.dict)?;
713        } else {
714            let mut dict = stream.dict.clone();
715            dict.set("Length", actual_len);
716            Writer::write_dictionary(file, &dict)?;
717        }
718        file.write_all(b"stream\n")?;
719        file.write_all(&stream.content)?;
720        file.write_all(b"\nendstream")?;
721        Ok(())
722    }
723
724    /// Write Binary mark as follows: %{binary_mark[4]}\n -> %Ç쏢 or Hex(%25 c3 87 c3 ac)
725    ///
726    /// Note: Specified in  ISO 19005-2:2011, ISO 19005-3:2012
727    /// headerByte1 > 127 && headerByte2 > 127 && headerByte3 > 127 && headerByte4 > 127
728    fn write_binary_mark(file: &mut dyn Write, binary_mark: &[u8]) -> Result<()> {
729        if binary_mark.iter().all(|&byte| byte >= 128) {
730            file.write_all(b"%")?;
731            file.write_all(binary_mark)?;
732            file.write_all(b"\n")?;
733        } else {
734            return Err(std::io::Error::new(
735                std::io::ErrorKind::InvalidData,
736                "Invalid binary mark",
737            ));
738        }
739
740        Ok(())
741    }
742}
743
744pub struct CountingWrite<W: Write> {
745    inner: W,
746    bytes_written: usize,
747}
748
749impl<W: Write> Write for CountingWrite<W> {
750    #[inline]
751    fn write(&mut self, buffer: &[u8]) -> Result<usize> {
752        let result = self.inner.write(buffer);
753        if let Ok(bytes) = result {
754            self.bytes_written += bytes;
755        }
756        result
757    }
758
759    #[inline]
760    fn write_all(&mut self, buffer: &[u8]) -> Result<()> {
761        self.bytes_written += buffer.len();
762        // If this returns `Err` we can’t know how many bytes were actually written (if any)
763        // but that doesn’t matter since we’re gonna abort the entire PDF generation anyway.
764        self.inner.write_all(buffer)
765    }
766
767    #[inline]
768    fn flush(&mut self) -> Result<()> {
769        self.inner.flush()
770    }
771}
772
773#[test]
774fn save_document() {
775    let mut doc = Document::with_version("1.5");
776    doc.objects.insert((1, 0), Null);
777    doc.objects.insert((2, 0), Boolean(true));
778    doc.objects.insert((3, 0), Integer(3));
779    doc.objects.insert((4, 0), Real(0.5));
780    doc.objects.insert(
781        (5, 0),
782        String("text((\r)".as_bytes().to_vec(), StringFormat::Literal),
783    );
784    doc.objects.insert(
785        (6, 0),
786        String("text((\r)".as_bytes().to_vec(), StringFormat::Hexadecimal),
787    );
788    doc.objects.insert((7, 0), Name(b"name \t".to_vec()));
789    doc.objects.insert((8, 0), Reference((1, 0)));
790    doc.objects
791        .insert((9, 2), Array(vec![Integer(1), Integer(2), Integer(3)]));
792    doc.objects.insert(
793        (11, 0),
794        Stream(Stream::new(Dictionary::new(), vec![0x41, 0x42, 0x43])),
795    );
796    let mut dict = Dictionary::new();
797    dict.set("A", Null);
798    dict.set("B", false);
799    dict.set("C", Name(b"name".to_vec()));
800    doc.objects.insert((12, 0), Object::Dictionary(dict));
801    doc.max_id = 12;
802
803    // Create temporary folder to store file.
804    let temp_dir = tempfile::tempdir().unwrap();
805    let file_path = temp_dir.path().join("test_0_save.pdf");
806    doc.save(&file_path).unwrap();
807    // Check if file was created.
808    assert!(file_path.exists());
809    // Check if path is file
810    assert!(file_path.is_file());
811    // Check if the file is above 400 bytes (should be about 610 bytes)
812    assert!(file_path.metadata().unwrap().len() > 400);
813}