Skip to main content

pdf_lib_rs/core/writers/
pdf_writer.rs

1use crate::core::context::PdfContext;
2use crate::core::document::{PdfCrossRefSection, PdfTrailer, PdfTrailerDict};
3use crate::core::objects::*;
4use crate::core::objects::pdf_object::PdfObjectTrait;
5use crate::core::syntax::CharCodes;
6use crate::utils::copy_string_into_buffer;
7
8/// Serializes a PdfContext back to PDF bytes.
9pub struct PdfWriter;
10
11impl PdfWriter {
12    /// Serialize the context to a PDF byte buffer.
13    pub fn serialize_to_buffer(context: &PdfContext) -> Vec<u8> {
14        let objects = context.enumerate_indirect_objects();
15
16        // First pass: compute size
17        let header_size = context.header.size_in_bytes();
18        let mut total_size = header_size + 2; // header + 2 newlines
19
20        // Track offsets for xref
21        let mut object_sizes = Vec::new();
22        for (pdf_ref, object) in &objects {
23            let obj_num_str = pdf_ref.object_number.to_string();
24            let gen_num_str = pdf_ref.generation_number.to_string();
25            let obj_header_size = obj_num_str.len() + 1 + gen_num_str.len() + 5; // "num gen obj\n"
26            let obj_footer_size = 9; // "\nendobj\n\n"
27            let obj_content_size = object.size_in_bytes();
28            let obj_total = obj_header_size + obj_content_size + obj_footer_size;
29            object_sizes.push(obj_total);
30            total_size += obj_total;
31        }
32
33        // Build xref section
34        let mut xref = PdfCrossRefSection::create();
35        let mut offset = header_size + 2;
36        for (i, (pdf_ref, _)) in objects.iter().enumerate() {
37            xref.add_entry(
38                PdfRef::of(pdf_ref.object_number, pdf_ref.generation_number),
39                offset as u64,
40            );
41            offset += object_sizes[i];
42        }
43        let xref_offset = offset;
44
45        // Build trailer dict
46        let mut trailer_dict = PdfDict::new();
47        trailer_dict.set(
48            PdfName::of("Size"),
49            PdfObject::Number(PdfNumber::of((context.largest_object_number + 1) as f64)),
50        );
51        if let Some(root) = &context.trailer_info.root {
52            trailer_dict.set(PdfName::of("Root"), root.clone());
53        }
54        if let Some(info) = &context.trailer_info.info {
55            trailer_dict.set(PdfName::of("Info"), info.clone());
56        }
57
58        let trailer_dict_obj = PdfTrailerDict::of(trailer_dict);
59        let trailer = PdfTrailer::for_last_cross_ref_section_offset(xref_offset as u64);
60
61        total_size += xref.size_in_bytes();
62        total_size += trailer_dict_obj.size_in_bytes();
63        total_size += 1; // newline between trailer dict and startxref
64        total_size += trailer.size_in_bytes();
65
66        // Second pass: write
67        let mut buffer = vec![0u8; total_size];
68        let mut off = 0;
69
70        // Header
71        off += context.header.copy_bytes_into(&mut buffer, off);
72        buffer[off] = CharCodes::Newline;
73        off += 1;
74        buffer[off] = CharCodes::Newline;
75        off += 1;
76
77        // Objects
78        for (pdf_ref, object) in &objects {
79            let obj_num_str = pdf_ref.object_number.to_string();
80            off += copy_string_into_buffer(&obj_num_str, &mut buffer, off);
81            buffer[off] = CharCodes::Space;
82            off += 1;
83
84            let gen_num_str = pdf_ref.generation_number.to_string();
85            off += copy_string_into_buffer(&gen_num_str, &mut buffer, off);
86            buffer[off] = CharCodes::Space;
87            off += 1;
88
89            buffer[off] = b'o';
90            off += 1;
91            buffer[off] = b'b';
92            off += 1;
93            buffer[off] = b'j';
94            off += 1;
95            buffer[off] = CharCodes::Newline;
96            off += 1;
97
98            off += object.copy_bytes_into(&mut buffer, off);
99
100            buffer[off] = CharCodes::Newline;
101            off += 1;
102            for &b in b"endobj" {
103                buffer[off] = b;
104                off += 1;
105            }
106            buffer[off] = CharCodes::Newline;
107            off += 1;
108            buffer[off] = CharCodes::Newline;
109            off += 1;
110        }
111
112        // Xref
113        off += xref.copy_bytes_into(&mut buffer, off);
114
115        // Trailer dict
116        off += trailer_dict_obj.copy_bytes_into(&mut buffer, off);
117        buffer[off] = CharCodes::Newline;
118        off += 1;
119
120        // Startxref + %%EOF
121        off += trailer.copy_bytes_into(&mut buffer, off);
122
123        buffer.truncate(off);
124        buffer
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::core::parser::PdfParser;
132
133    #[test]
134    fn can_serialize_empty_context() {
135        let context = PdfContext::create();
136        let bytes = PdfWriter::serialize_to_buffer(&context);
137        assert!(bytes.starts_with(b"%PDF-1.7"));
138        assert!(bytes.windows(5).any(|w| w == b"%%EOF"));
139    }
140
141    #[test]
142    fn can_serialize_context_with_objects() {
143        let mut context = PdfContext::create();
144        context.register(PdfObject::Number(PdfNumber::of(42.0)));
145        context.register(PdfObject::Name(PdfName::of("Foo")));
146        let bytes = PdfWriter::serialize_to_buffer(&context);
147
148        let output = String::from_utf8_lossy(&bytes);
149        assert!(output.contains("1 0 obj"));
150        assert!(output.contains("42"));
151        assert!(output.contains("2 0 obj"));
152        assert!(output.contains("/Foo"));
153        assert!(output.contains("endobj"));
154        assert!(output.contains("xref"));
155        assert!(output.contains("%%EOF"));
156    }
157
158    #[test]
159    fn roundtrip_parse_serialize_parse() {
160        let mut context = PdfContext::create();
161
162        let mut page_dict = PdfDict::new();
163        page_dict.set(PdfName::of("Type"), PdfObject::Name(PdfName::of("Page")));
164        let page_ref = context.register(PdfObject::Dict(page_dict));
165
166        let mut catalog = PdfDict::new();
167        catalog.set(PdfName::of("Type"), PdfObject::Name(PdfName::of("Catalog")));
168        catalog.set(PdfName::of("Pages"), PdfObject::Ref(page_ref));
169        let catalog_ref = context.register(PdfObject::Dict(catalog));
170
171        context.trailer_info.root = Some(PdfObject::Ref(catalog_ref));
172
173        let bytes = PdfWriter::serialize_to_buffer(&context);
174        let parser = PdfParser::for_bytes(&bytes);
175        let context2 = parser.parse_document().unwrap();
176
177        assert_eq!(context2.object_count(), context.object_count());
178    }
179}