Skip to main content

justpdf_core/writer/
document.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4use crate::error::{JustPdfError, Result};
5use crate::object::{IndirectRef, PdfDict, PdfObject};
6use crate::writer::encode::make_stream;
7use crate::writer::page::PageBuilder;
8use crate::writer::serialize::serialize_pdf;
9use crate::writer::PdfWriter;
10
11/// High-level builder for creating complete PDF documents.
12pub struct DocumentBuilder {
13    writer: PdfWriter,
14    /// Page indirect references in order.
15    pages: Vec<IndirectRef>,
16    /// font_name -> (resource_name, font_ref).
17    fonts: HashMap<String, (String, IndirectRef)>,
18    font_counter: u32,
19    /// Document info dictionary entries.
20    info: PdfDict,
21    /// Pre-allocated Pages object number (for forward reference).
22    pages_obj_num: u32,
23    /// XMP metadata stream reference (attached to Catalog).
24    xmp_ref: Option<IndirectRef>,
25    /// Optional encryption configuration.
26    encryption: Option<crate::crypto::EncryptionConfig>,
27}
28
29impl DocumentBuilder {
30    /// Create a new document builder.
31    pub fn new() -> Self {
32        let mut writer = PdfWriter::new();
33        // Pre-allocate the Pages object number so pages can reference their parent.
34        let pages_obj_num = writer.alloc_object_num();
35
36        Self {
37            writer,
38            pages: Vec::new(),
39            fonts: HashMap::new(),
40            font_counter: 0,
41            info: PdfDict::new(),
42            pages_obj_num,
43            xmp_ref: None,
44            encryption: None,
45        }
46    }
47
48    /// Add a standard Type1 font (e.g. "Helvetica", "Times-Roman", "Courier").
49    ///
50    /// Returns the resource name (e.g. "F1") to use when drawing text.
51    pub fn add_standard_font(&mut self, base_font: &str) -> String {
52        // Check if already added
53        if let Some((res_name, _)) = self.fonts.get(base_font) {
54            return res_name.clone();
55        }
56
57        self.font_counter += 1;
58        let resource_name = format!("F{}", self.font_counter);
59
60        // Create font dictionary object
61        let mut font_dict = PdfDict::new();
62        font_dict.insert(b"Type".to_vec(), PdfObject::Name(b"Font".to_vec()));
63        font_dict.insert(b"Subtype".to_vec(), PdfObject::Name(b"Type1".to_vec()));
64        font_dict.insert(
65            b"BaseFont".to_vec(),
66            PdfObject::Name(base_font.as_bytes().to_vec()),
67        );
68
69        let font_ref = self.writer.add_object(PdfObject::Dict(font_dict));
70        self.fonts
71            .insert(base_font.to_string(), (resource_name.clone(), font_ref));
72
73        resource_name
74    }
75
76    /// Add a page to the document from a `PageBuilder`.
77    pub fn add_page(&mut self, page: PageBuilder) {
78        let pages_ref = IndirectRef {
79            obj_num: self.pages_obj_num,
80            gen_num: 0,
81        };
82        let page_ref = page.build(&mut self.writer, &pages_ref);
83        self.pages.push(page_ref);
84    }
85
86    /// Set the document title.
87    pub fn set_title(&mut self, title: &str) {
88        self.info.insert(
89            b"Title".to_vec(),
90            PdfObject::String(title.as_bytes().to_vec()),
91        );
92    }
93
94    /// Set the document author.
95    pub fn set_author(&mut self, author: &str) {
96        self.info.insert(
97            b"Author".to_vec(),
98            PdfObject::String(author.as_bytes().to_vec()),
99        );
100    }
101
102    /// Set the document subject.
103    pub fn set_subject(&mut self, subject: &str) {
104        self.info.insert(
105            b"Subject".to_vec(),
106            PdfObject::String(subject.as_bytes().to_vec()),
107        );
108    }
109
110    /// Set the producer field.
111    pub fn set_producer(&mut self, producer: &str) {
112        self.info.insert(
113            b"Producer".to_vec(),
114            PdfObject::String(producer.as_bytes().to_vec()),
115        );
116    }
117
118    /// Set the creator field.
119    pub fn set_creator(&mut self, creator: &str) {
120        self.info.insert(
121            b"Creator".to_vec(),
122            PdfObject::String(creator.as_bytes().to_vec()),
123        );
124    }
125
126    /// Embed a TrueType font from raw TTF data.
127    ///
128    /// Returns the resource name (e.g. "F1", "F2") for use in page content.
129    /// The font is embedded with WinAnsiEncoding and a ToUnicode CMap.
130    pub fn embed_truetype_font(&mut self, font_data: &[u8]) -> Result<String> {
131        let face = ttf_parser::Face::parse(font_data, 0).map_err(|e| JustPdfError::StreamDecode {
132            filter: "TrueType".into(),
133            detail: format!("failed to parse TTF: {}", e),
134        })?;
135
136        // Extract font metrics
137        let units_per_em = face.units_per_em() as f64;
138        let scale = 1000.0 / units_per_em;
139
140        let font_name = face
141            .names()
142            .into_iter()
143            .find(|n| n.name_id == ttf_parser::name_id::POST_SCRIPT_NAME)
144            .and_then(|n| n.to_string())
145            .unwrap_or_else(|| "UnknownFont".to_string());
146
147        let ascent = (face.ascender() as f64 * scale) as i64;
148        let descent = (face.descender() as f64 * scale) as i64;
149        let bbox = face.global_bounding_box();
150        let bbox_arr = vec![
151            PdfObject::Integer((bbox.x_min as f64 * scale) as i64),
152            PdfObject::Integer((bbox.y_min as f64 * scale) as i64),
153            PdfObject::Integer((bbox.x_max as f64 * scale) as i64),
154            PdfObject::Integer((bbox.y_max as f64 * scale) as i64),
155        ];
156        let cap_height = face.capital_height().map(|h| (h as f64 * scale) as i64).unwrap_or(ascent);
157
158        // Embed font file as FontFile2 stream (FlateDecode compressed)
159        let (ff2_dict, ff2_data) = make_stream(font_data, true);
160        let mut ff2_stream_dict = ff2_dict;
161        ff2_stream_dict.insert(
162            b"Length1".to_vec(),
163            PdfObject::Integer(font_data.len() as i64),
164        );
165        let ff2_ref = self.writer.add_object(PdfObject::Stream {
166            dict: ff2_stream_dict,
167            data: ff2_data,
168        });
169
170        // Create FontDescriptor
171        let mut fd = PdfDict::new();
172        fd.insert(b"Type".to_vec(), PdfObject::Name(b"FontDescriptor".to_vec()));
173        fd.insert(
174            b"FontName".to_vec(),
175            PdfObject::Name(font_name.as_bytes().to_vec()),
176        );
177        fd.insert(b"Flags".to_vec(), PdfObject::Integer(32)); // Nonsymbolic
178        fd.insert(b"FontBBox".to_vec(), PdfObject::Array(bbox_arr));
179        fd.insert(b"ItalicAngle".to_vec(), PdfObject::Integer(0));
180        fd.insert(b"Ascent".to_vec(), PdfObject::Integer(ascent));
181        fd.insert(b"Descent".to_vec(), PdfObject::Integer(descent));
182        fd.insert(b"CapHeight".to_vec(), PdfObject::Integer(cap_height));
183        fd.insert(b"StemV".to_vec(), PdfObject::Integer(80));
184        fd.insert(b"FontFile2".to_vec(), PdfObject::Reference(ff2_ref));
185        let fd_ref = self.writer.add_object(PdfObject::Dict(fd));
186
187        // Build Widths array for chars 32-255
188        let mut widths = Vec::with_capacity(224);
189        let mut bfchar_entries: Vec<(u8, u16)> = Vec::new();
190        for code in 32u16..=255u16 {
191            let ch = code as u8 as char;
192            let unicode_val = ch as u16;
193            if let Some(glyph_id) = face.glyph_index(ch) {
194                let w = face
195                    .glyph_hor_advance(glyph_id)
196                    .map(|a| (a as f64 * scale) as i64)
197                    .unwrap_or(0);
198                widths.push(PdfObject::Integer(w));
199                bfchar_entries.push((code as u8, unicode_val));
200            } else {
201                widths.push(PdfObject::Integer(0));
202            }
203        }
204
205        // Generate ToUnicode CMap
206        let tounicode_cmap = generate_tounicode_cmap(&bfchar_entries);
207        let (cmap_dict, cmap_data) = make_stream(tounicode_cmap.as_bytes(), true);
208        let cmap_ref = self.writer.add_object(PdfObject::Stream {
209            dict: cmap_dict,
210            data: cmap_data,
211        });
212
213        // Create Font dictionary
214        self.font_counter += 1;
215        let resource_name = format!("F{}", self.font_counter);
216
217        let mut font_dict = PdfDict::new();
218        font_dict.insert(b"Type".to_vec(), PdfObject::Name(b"Font".to_vec()));
219        font_dict.insert(b"Subtype".to_vec(), PdfObject::Name(b"TrueType".to_vec()));
220        font_dict.insert(
221            b"BaseFont".to_vec(),
222            PdfObject::Name(font_name.as_bytes().to_vec()),
223        );
224        font_dict.insert(b"FirstChar".to_vec(), PdfObject::Integer(32));
225        font_dict.insert(b"LastChar".to_vec(), PdfObject::Integer(255));
226        font_dict.insert(b"Widths".to_vec(), PdfObject::Array(widths));
227        font_dict.insert(b"FontDescriptor".to_vec(), PdfObject::Reference(fd_ref));
228        font_dict.insert(
229            b"Encoding".to_vec(),
230            PdfObject::Name(b"WinAnsiEncoding".to_vec()),
231        );
232        font_dict.insert(b"ToUnicode".to_vec(), PdfObject::Reference(cmap_ref));
233
234        let font_ref = self.writer.add_object(PdfObject::Dict(font_dict));
235        self.fonts
236            .insert(font_name.clone(), (resource_name.clone(), font_ref));
237
238        Ok(resource_name)
239    }
240
241    /// Set encryption for the document.
242    pub fn set_encryption(&mut self, config: crate::crypto::EncryptionConfig) {
243        self.encryption = Some(config);
244    }
245
246    /// Set XMP metadata on the document catalog.
247    ///
248    /// Generates an XMP XML metadata stream with the given fields and attaches
249    /// it to the Catalog as `/Metadata`.
250    pub fn set_xmp_metadata(&mut self, title: &str, author: &str, subject: &str, creator: &str) {
251        let xmp = format!(
252            "<?xpacket begin=\"\u{FEFF}\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\n\
253<x:xmpmeta xmlns:x=\"adobe:ns:meta/\">\n\
254<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n\
255<rdf:Description rdf:about=\"\"\n\
256  xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n\
257  xmlns:xmp=\"http://ns.adobe.com/xap/1.0/\"\n\
258  xmlns:pdf=\"http://ns.adobe.com/pdf/1.3/\">\n\
259<dc:title><rdf:Alt><rdf:li xml:lang=\"x-default\">{title}</rdf:li></rdf:Alt></dc:title>\n\
260<dc:creator><rdf:Seq><rdf:li>{author}</rdf:li></rdf:Seq></dc:creator>\n\
261<dc:subject><rdf:Bag><rdf:li>{subject}</rdf:li></rdf:Bag></dc:subject>\n\
262<xmp:CreatorTool>{creator}</xmp:CreatorTool>\n\
263<pdf:Producer>justpdf</pdf:Producer>\n\
264</rdf:Description>\n\
265</rdf:RDF>\n\
266</x:xmpmeta>\n\
267<?xpacket end=\"w\"?>"
268        );
269
270        let mut meta_dict = PdfDict::new();
271        meta_dict.insert(b"Type".to_vec(), PdfObject::Name(b"Metadata".to_vec()));
272        meta_dict.insert(b"Subtype".to_vec(), PdfObject::Name(b"XML".to_vec()));
273        meta_dict.insert(
274            b"Length".to_vec(),
275            PdfObject::Integer(xmp.len() as i64),
276        );
277
278        // Store as uncompressed stream so XMP can be found by text search
279        let meta_ref = self.writer.add_object(PdfObject::Stream {
280            dict: meta_dict,
281            data: xmp.into_bytes(),
282        });
283        self.xmp_ref = Some(meta_ref);
284    }
285
286    /// Build the document and return the PDF bytes.
287    pub fn build(mut self) -> Result<Vec<u8>> {
288        // Create Pages dictionary
289        let kids: Vec<PdfObject> = self
290            .pages
291            .iter()
292            .map(|r| PdfObject::Reference(r.clone()))
293            .collect();
294        let page_count = kids.len() as i64;
295
296        let mut pages_dict = PdfDict::new();
297        pages_dict.insert(b"Type".to_vec(), PdfObject::Name(b"Pages".to_vec()));
298        pages_dict.insert(b"Kids".to_vec(), PdfObject::Array(kids));
299        pages_dict.insert(b"Count".to_vec(), PdfObject::Integer(page_count));
300
301        self.writer
302            .set_object(self.pages_obj_num, PdfObject::Dict(pages_dict));
303
304        // Create Catalog dictionary
305        let mut catalog_dict = PdfDict::new();
306        catalog_dict.insert(b"Type".to_vec(), PdfObject::Name(b"Catalog".to_vec()));
307        catalog_dict.insert(
308            b"Pages".to_vec(),
309            PdfObject::Reference(IndirectRef {
310                obj_num: self.pages_obj_num,
311                gen_num: 0,
312            }),
313        );
314        if let Some(ref xmp_ref) = self.xmp_ref {
315            catalog_dict.insert(
316                b"Metadata".to_vec(),
317                PdfObject::Reference(xmp_ref.clone()),
318            );
319        }
320        let catalog_ref = self.writer.add_object(PdfObject::Dict(catalog_dict));
321
322        // Create Info dictionary if non-empty
323        let info_ref = if !self.info.is_empty() {
324            Some(self.writer.add_object(PdfObject::Dict(self.info)))
325        } else {
326            None
327        };
328
329        // Handle encryption
330        if let Some(config) = self.encryption {
331            let file_id = crate::crypto::generate_file_id(b"justpdf", 0);
332            let (state, encrypt_dict, id_array) = config.build(&file_id)?;
333
334            let encrypt_ref = self.writer.add_object(PdfObject::Dict(encrypt_dict));
335
336            // Update the state with the encrypt obj num so it won't be encrypted
337            let mut state = state;
338            state.encrypt_obj_num = Some(encrypt_ref.obj_num);
339
340            crate::writer::serialize_pdf_encrypted(
341                &self.writer.objects,
342                self.writer.version,
343                &catalog_ref,
344                info_ref.as_ref(),
345                &encrypt_ref,
346                &state,
347                &id_array,
348            )
349        } else {
350            serialize_pdf(
351                &self.writer.objects,
352                self.writer.version,
353                &catalog_ref,
354                info_ref.as_ref(),
355            )
356        }
357    }
358
359    /// Build the document and save to a file.
360    pub fn save(self, path: &Path) -> Result<()> {
361        let bytes = self.build()?;
362        std::fs::write(path, bytes)?;
363        Ok(())
364    }
365}
366
367impl Default for DocumentBuilder {
368    fn default() -> Self {
369        Self::new()
370    }
371}
372
373/// Embed a JPEG image into the document.
374///
375/// Parses the JPEG header to determine width, height, and number of color
376/// components, then creates an Image XObject stream.
377///
378/// Returns `(resource_name, indirect_ref)` for the image.
379pub fn embed_jpeg(doc: &mut DocumentBuilder, jpeg_data: &[u8]) -> Result<(String, IndirectRef)> {
380    let (width, height, components) = parse_jpeg_header(jpeg_data)?;
381
382    let color_space = match components {
383        1 => b"DeviceGray".to_vec(),
384        3 => b"DeviceRGB".to_vec(),
385        4 => b"DeviceCMYK".to_vec(),
386        _ => b"DeviceRGB".to_vec(),
387    };
388
389    let mut dict = PdfDict::new();
390    dict.insert(b"Type".to_vec(), PdfObject::Name(b"XObject".to_vec()));
391    dict.insert(b"Subtype".to_vec(), PdfObject::Name(b"Image".to_vec()));
392    dict.insert(b"Width".to_vec(), PdfObject::Integer(width as i64));
393    dict.insert(b"Height".to_vec(), PdfObject::Integer(height as i64));
394    dict.insert(
395        b"ColorSpace".to_vec(),
396        PdfObject::Name(color_space),
397    );
398    dict.insert(b"BitsPerComponent".to_vec(), PdfObject::Integer(8));
399    dict.insert(
400        b"Filter".to_vec(),
401        PdfObject::Name(b"DCTDecode".to_vec()),
402    );
403    dict.insert(
404        b"Length".to_vec(),
405        PdfObject::Integer(jpeg_data.len() as i64),
406    );
407
408    let image_obj = PdfObject::Stream {
409        dict,
410        data: jpeg_data.to_vec(),
411    };
412    let image_ref = doc.writer.add_object(image_obj);
413
414    // Assign an image resource name
415    let res_name = format!("Im{}", image_ref.obj_num);
416
417    Ok((res_name, image_ref))
418}
419
420/// Parse a JPEG header to extract width, height, and number of components.
421///
422/// Looks for a SOF0 (0xFF 0xC0) or SOF2 (0xFF 0xC2) marker and reads the
423/// frame header fields.
424fn parse_jpeg_header(data: &[u8]) -> Result<(u32, u32, u8)> {
425    if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
426        return Err(JustPdfError::StreamDecode {
427            filter: "DCTDecode".into(),
428            detail: "not a valid JPEG (missing SOI marker)".into(),
429        });
430    }
431
432    let mut pos = 2;
433    while pos + 1 < data.len() {
434        if data[pos] != 0xFF {
435            pos += 1;
436            continue;
437        }
438
439        let marker = data[pos + 1];
440        pos += 2;
441
442        // SOF0 or SOF2 (baseline or progressive)
443        if marker == 0xC0 || marker == 0xC2 {
444            if pos + 7 > data.len() {
445                break;
446            }
447            // Skip frame length (2 bytes) and precision (1 byte)
448            let height = ((data[pos + 3] as u32) << 8) | (data[pos + 4] as u32);
449            let width = ((data[pos + 5] as u32) << 8) | (data[pos + 6] as u32);
450            let components = data[pos + 7];
451            return Ok((width, height, components));
452        }
453
454        // Skip segment: read segment length
455        if pos + 1 >= data.len() {
456            break;
457        }
458        // Markers without payload
459        if marker == 0xD8 || marker == 0xD9 || (0xD0..=0xD7).contains(&marker) {
460            continue;
461        }
462        let seg_len = ((data[pos] as usize) << 8) | (data[pos + 1] as usize);
463        if seg_len < 2 {
464            break;
465        }
466        pos += seg_len;
467    }
468
469    Err(JustPdfError::StreamDecode {
470        filter: "DCTDecode".into(),
471        detail: "could not find SOF marker in JPEG data".into(),
472    })
473}
474
475/// Generate a ToUnicode CMap string mapping byte codes to Unicode values.
476fn generate_tounicode_cmap(entries: &[(u8, u16)]) -> String {
477    let mut cmap = String::new();
478    cmap.push_str("/CIDInit /ProcSet findresource begin\n");
479    cmap.push_str("12 dict begin\n");
480    cmap.push_str("begincmap\n");
481    cmap.push_str("/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n");
482    cmap.push_str("/CMapName /Adobe-Identity-UCS def\n");
483    cmap.push_str("/CMapType 2 def\n");
484    cmap.push_str("1 begincodespacerange\n");
485    cmap.push_str("<00> <FF>\n");
486    cmap.push_str("endcodespacerange\n");
487
488    // Write bfchar entries in chunks of 100 (PDF spec limit)
489    let mut i = 0;
490    while i < entries.len() {
491        let chunk_size = (entries.len() - i).min(100);
492        cmap.push_str(&format!("{} beginbfchar\n", chunk_size));
493        for &(code, unicode) in &entries[i..i + chunk_size] {
494            cmap.push_str(&format!("<{:02X}> <{:04X}>\n", code, unicode));
495        }
496        cmap.push_str("endbfchar\n");
497        i += chunk_size;
498    }
499
500    cmap.push_str("endcmap\n");
501    cmap.push_str("CMapName currentdict /CMap defineresource pop\n");
502    cmap.push_str("end\n");
503    cmap.push_str("end\n");
504    cmap
505}
506
507/// Embed a PNG image into the document.
508///
509/// Decodes the PNG, extracts RGB data (with optional alpha as SMask),
510/// and creates an Image XObject.
511///
512/// Returns `(resource_name, indirect_ref)` for the image.
513pub fn embed_png(doc: &mut DocumentBuilder, png_data: &[u8]) -> Result<(String, IndirectRef)> {
514    use crate::writer::encode::encode_flate;
515
516    let decoder = png::Decoder::new(png_data);
517    let mut reader = decoder.read_info().map_err(|e| JustPdfError::StreamDecode {
518        filter: "PNG".into(),
519        detail: format!("failed to decode PNG: {}", e),
520    })?;
521
522    let info = reader.info().clone();
523    let width = info.width;
524    let height = info.height;
525    let color_type = info.color_type;
526
527    // Read all pixel data
528    let mut img_data = vec![0u8; reader.output_buffer_size()];
529    let output_info = reader.next_frame(&mut img_data).map_err(|e| JustPdfError::StreamDecode {
530        filter: "PNG".into(),
531        detail: format!("failed to read PNG frame: {}", e),
532    })?;
533    img_data.truncate(output_info.buffer_size());
534
535    let (rgb_data, alpha_data) = match color_type {
536        png::ColorType::Rgb => (img_data, None),
537        png::ColorType::Rgba => {
538            // Split into RGB and Alpha channels
539            let pixel_count = (width * height) as usize;
540            let mut rgb = Vec::with_capacity(pixel_count * 3);
541            let mut alpha = Vec::with_capacity(pixel_count);
542            for chunk in img_data.chunks(4) {
543                if chunk.len() == 4 {
544                    rgb.extend_from_slice(&chunk[..3]);
545                    alpha.push(chunk[3]);
546                }
547            }
548            (rgb, Some(alpha))
549        }
550        png::ColorType::Grayscale => {
551            // Convert grayscale to RGB
552            let mut rgb = Vec::with_capacity(img_data.len() * 3);
553            for &g in &img_data {
554                rgb.push(g);
555                rgb.push(g);
556                rgb.push(g);
557            }
558            (rgb, None)
559        }
560        png::ColorType::GrayscaleAlpha => {
561            let pixel_count = (width * height) as usize;
562            let mut rgb = Vec::with_capacity(pixel_count * 3);
563            let mut alpha = Vec::with_capacity(pixel_count);
564            for chunk in img_data.chunks(2) {
565                if chunk.len() == 2 {
566                    rgb.push(chunk[0]);
567                    rgb.push(chunk[0]);
568                    rgb.push(chunk[0]);
569                    alpha.push(chunk[1]);
570                }
571            }
572            (rgb, Some(alpha))
573        }
574        _ => {
575            return Err(JustPdfError::StreamDecode {
576                filter: "PNG".into(),
577                detail: format!("unsupported PNG color type: {:?}", color_type),
578            });
579        }
580    };
581
582    // Compress RGB data
583    let compressed_rgb = encode_flate(&rgb_data)?;
584
585    // Create SMask if alpha channel exists
586    let smask_ref = if let Some(alpha) = alpha_data {
587        let compressed_alpha = encode_flate(&alpha)?;
588        let mut smask_dict = PdfDict::new();
589        smask_dict.insert(b"Type".to_vec(), PdfObject::Name(b"XObject".to_vec()));
590        smask_dict.insert(b"Subtype".to_vec(), PdfObject::Name(b"Image".to_vec()));
591        smask_dict.insert(b"Width".to_vec(), PdfObject::Integer(width as i64));
592        smask_dict.insert(b"Height".to_vec(), PdfObject::Integer(height as i64));
593        smask_dict.insert(
594            b"ColorSpace".to_vec(),
595            PdfObject::Name(b"DeviceGray".to_vec()),
596        );
597        smask_dict.insert(b"BitsPerComponent".to_vec(), PdfObject::Integer(8));
598        smask_dict.insert(
599            b"Filter".to_vec(),
600            PdfObject::Name(b"FlateDecode".to_vec()),
601        );
602
603        let r = doc.writer.add_object(PdfObject::Stream {
604            dict: smask_dict,
605            data: compressed_alpha,
606        });
607        Some(r)
608    } else {
609        None
610    };
611
612    // Create Image XObject
613    let mut img_dict = PdfDict::new();
614    img_dict.insert(b"Type".to_vec(), PdfObject::Name(b"XObject".to_vec()));
615    img_dict.insert(b"Subtype".to_vec(), PdfObject::Name(b"Image".to_vec()));
616    img_dict.insert(b"Width".to_vec(), PdfObject::Integer(width as i64));
617    img_dict.insert(b"Height".to_vec(), PdfObject::Integer(height as i64));
618    img_dict.insert(
619        b"ColorSpace".to_vec(),
620        PdfObject::Name(b"DeviceRGB".to_vec()),
621    );
622    img_dict.insert(b"BitsPerComponent".to_vec(), PdfObject::Integer(8));
623    img_dict.insert(
624        b"Filter".to_vec(),
625        PdfObject::Name(b"FlateDecode".to_vec()),
626    );
627    if let Some(ref smask) = smask_ref {
628        img_dict.insert(b"SMask".to_vec(), PdfObject::Reference(smask.clone()));
629    }
630
631    let image_ref = doc.writer.add_object(PdfObject::Stream {
632        dict: img_dict,
633        data: compressed_rgb,
634    });
635
636    let res_name = format!("Im{}", image_ref.obj_num);
637    Ok((res_name, image_ref))
638}
639
640#[cfg(test)]
641mod tests {
642    use super::*;
643    use crate::parser::PdfDocument;
644
645    #[test]
646    fn test_create_and_parse_pdf() {
647        let mut doc = DocumentBuilder::new();
648        let font_name = doc.add_standard_font("Helvetica");
649
650        let mut page = PageBuilder::new(612.0, 792.0);
651        page.add_font(&font_name, "Helvetica");
652        page.begin_text();
653        page.set_font(&font_name, 24.0);
654        page.move_to(72.0, 720.0);
655        page.show_text("Hello, World!");
656        page.end_text();
657
658        doc.add_page(page);
659        doc.set_title("Test PDF");
660
661        let bytes = doc.build().unwrap();
662
663        // Verify it starts with PDF header
664        assert!(bytes.starts_with(b"%PDF-1.7"));
665
666        // Parse it back
667        let mut parsed = PdfDocument::from_bytes(bytes).unwrap();
668        let pages = crate::page::collect_pages(&parsed).unwrap();
669        assert_eq!(pages.len(), 1);
670    }
671
672    #[test]
673    fn test_document_with_info() {
674        let mut doc = DocumentBuilder::new();
675        doc.set_title("My Title");
676        doc.set_author("Test Author");
677        doc.set_producer("justpdf");
678
679        // Add a minimal page
680        let page = PageBuilder::new(612.0, 792.0);
681        doc.add_page(page);
682
683        let bytes = doc.build().unwrap();
684        let text = String::from_utf8_lossy(&bytes);
685
686        assert!(text.contains("My Title"));
687        assert!(text.contains("Test Author"));
688        assert!(text.contains("justpdf"));
689    }
690
691    #[test]
692    fn test_document_multiple_pages() {
693        let mut doc = DocumentBuilder::new();
694
695        let page1 = PageBuilder::new(612.0, 792.0);
696        doc.add_page(page1);
697
698        let page2 = PageBuilder::new(612.0, 792.0);
699        doc.add_page(page2);
700
701        let bytes = doc.build().unwrap();
702
703        let mut parsed = PdfDocument::from_bytes(bytes).unwrap();
704        let pages = crate::page::collect_pages(&parsed).unwrap();
705        assert_eq!(pages.len(), 2);
706    }
707
708    #[test]
709    fn test_add_standard_font_idempotent() {
710        let mut doc = DocumentBuilder::new();
711        let name1 = doc.add_standard_font("Helvetica");
712        let name2 = doc.add_standard_font("Helvetica");
713        assert_eq!(name1, name2);
714
715        let name3 = doc.add_standard_font("Courier");
716        assert_ne!(name1, name3);
717    }
718
719    #[test]
720    fn test_embed_truetype_invalid_data() {
721        let mut doc = DocumentBuilder::new();
722        let result = doc.embed_truetype_font(b"not a font");
723        assert!(result.is_err());
724    }
725
726    #[test]
727    fn test_embed_png_minimal() {
728        // Create a minimal 2x2 RGB PNG in memory using the png encoder
729        let mut png_bytes = Vec::new();
730        {
731            let mut encoder = png::Encoder::new(std::io::Cursor::new(&mut png_bytes), 2, 2);
732            encoder.set_color(png::ColorType::Rgb);
733            encoder.set_depth(png::BitDepth::Eight);
734            let mut writer = encoder.write_header().unwrap();
735            // 2x2 RGB = 12 bytes (R,G,B per pixel)
736            let data: [u8; 12] = [
737                255, 0, 0, // red
738                0, 255, 0, // green
739                0, 0, 255, // blue
740                255, 255, 0, // yellow
741            ];
742            writer.write_image_data(&data).unwrap();
743        }
744
745        let mut doc = DocumentBuilder::new();
746        let (name, img_ref) = embed_png(&mut doc, &png_bytes).unwrap();
747        assert!(name.starts_with("Im"));
748        assert!(img_ref.obj_num > 0);
749
750        // Build a doc with the image
751        let mut page = PageBuilder::new(612.0, 792.0);
752        page.add_image(&name, img_ref);
753        page.draw_image(&name, 0.0, 0.0, 100.0, 100.0);
754        doc.add_page(page);
755        let bytes = doc.build().unwrap();
756        assert!(bytes.starts_with(b"%PDF-1.7"));
757    }
758
759    #[test]
760    fn test_embed_png_with_alpha() {
761        // Create a 2x2 RGBA PNG
762        let mut png_bytes = Vec::new();
763        {
764            let mut encoder = png::Encoder::new(std::io::Cursor::new(&mut png_bytes), 2, 2);
765            encoder.set_color(png::ColorType::Rgba);
766            encoder.set_depth(png::BitDepth::Eight);
767            let mut writer = encoder.write_header().unwrap();
768            let data: [u8; 16] = [
769                255, 0, 0, 128, // red, semi-transparent
770                0, 255, 0, 255, // green, opaque
771                0, 0, 255, 0,   // blue, fully transparent
772                255, 255, 0, 64, // yellow, mostly transparent
773            ];
774            writer.write_image_data(&data).unwrap();
775        }
776
777        let mut doc = DocumentBuilder::new();
778        let (name, img_ref) = embed_png(&mut doc, &png_bytes).unwrap();
779        assert!(name.starts_with("Im"));
780
781        // The writer should have at least 2 objects: SMask + Image
782        // (SMask for alpha channel)
783        assert!(doc.writer.objects.len() >= 2);
784
785        let mut page = PageBuilder::new(612.0, 792.0);
786        page.add_image(&name, img_ref);
787        page.draw_image(&name, 0.0, 0.0, 100.0, 100.0);
788        doc.add_page(page);
789        let bytes = doc.build().unwrap();
790        let text = String::from_utf8_lossy(&bytes);
791        assert!(text.contains("SMask"));
792    }
793
794    #[test]
795    fn test_xmp_metadata() {
796        let mut doc = DocumentBuilder::new();
797        doc.set_xmp_metadata("Test Title", "Test Author", "Test Subject", "TestCreator");
798
799        let page = PageBuilder::new(612.0, 792.0);
800        doc.add_page(page);
801
802        let bytes = doc.build().unwrap();
803        let text = String::from_utf8_lossy(&bytes);
804        assert!(text.contains("Test Title"));
805        assert!(text.contains("Test Author"));
806        assert!(text.contains("Test Subject"));
807        assert!(text.contains("TestCreator"));
808        assert!(text.contains("xmpmeta"));
809        assert!(text.contains("/Metadata"));
810    }
811
812    #[test]
813    fn test_tounicode_cmap_generation() {
814        let entries = vec![(0x41u8, 0x0041u16), (0x42, 0x0042)];
815        let cmap = generate_tounicode_cmap(&entries);
816        assert!(cmap.contains("beginbfchar"));
817        assert!(cmap.contains("<41> <0041>"));
818        assert!(cmap.contains("<42> <0042>"));
819        assert!(cmap.contains("endbfchar"));
820    }
821
822    #[test]
823    fn test_parse_jpeg_header() {
824        // Minimal JPEG with SOI, SOF0 marker
825        // SOI: FF D8
826        // APP0: FF E0 00 02
827        // SOF0: FF C0 00 0B 08 00 64 00 C8 03 ...
828        //   precision=8, height=100, width=200, components=3
829        let jpeg_fixed = vec![
830            0xFF, 0xD8, // SOI
831            0xFF, 0xE0, 0x00, 0x02, // APP0 segment, length=2 (just the length bytes)
832            0xFF, 0xC0, // SOF0 marker
833            0x00, 0x0B, // frame header length = 11
834            0x08, // precision = 8 bits
835            0x00, 0x64, // height = 100
836            0x00, 0xC8, // width = 200
837            0x03, // components = 3
838            // component specs would follow but we don't need them
839        ];
840
841        // After SOI: pos=2
842        // FF E0: marker, pos=4. seg_len=2, pos=4+2=6
843        // FF C0: pos=8 (after consuming marker bytes at [6],[7])
844        // data[8+3]=data[11]=0x00, data[8+4]=data[12]=0x64 => height=100
845        // data[8+5]=data[13]=0x00, data[8+6]=data[14]=0xC8 => width=200
846        // data[8+7]=data[15]=0x03 => components=3
847
848        let (w, h, c) = parse_jpeg_header(&jpeg_fixed).unwrap();
849        assert_eq!(w, 200);
850        assert_eq!(h, 100);
851        assert_eq!(c, 3);
852    }
853
854    // --- Negative Tests ---
855
856    #[test]
857    fn test_negative_page_size() {
858        // Negative or zero page size should still produce a valid PDF
859        // (the writer doesn't validate dimensions — that's the renderer's job)
860        // But we verify it doesn't panic
861        let mut doc = DocumentBuilder::new();
862        let page = PageBuilder::new(-100.0, 0.0);
863        doc.add_page(page);
864        let result = doc.build();
865        assert!(result.is_ok());
866    }
867
868    #[test]
869    fn test_empty_font_name() {
870        // Empty font name should still work (no panic)
871        let mut doc = DocumentBuilder::new();
872        let name = doc.add_standard_font("");
873        assert!(!name.is_empty()); // returns "F1" regardless
874    }
875
876    #[test]
877    fn test_embed_truetype_invalid_data_returns_error() {
878        let mut doc = DocumentBuilder::new();
879        let result = doc.embed_truetype_font(b"not a font file");
880        assert!(result.is_err());
881    }
882
883    #[test]
884    fn test_embed_jpeg_invalid_data_returns_error() {
885        let mut doc = DocumentBuilder::new();
886        let result = embed_jpeg(&mut doc, b"not a jpeg");
887        assert!(result.is_err());
888    }
889
890    #[test]
891    fn test_embed_png_invalid_data_returns_error() {
892        let mut doc = DocumentBuilder::new();
893        let result = embed_png(&mut doc, b"not a png");
894        assert!(result.is_err());
895    }
896
897    #[test]
898    fn test_delete_page_out_of_range() {
899        use crate::writer::modify::DocumentModifier;
900
901        let mut doc = DocumentBuilder::new();
902        let page = PageBuilder::new(612.0, 792.0);
903        doc.add_page(page);
904        let bytes = doc.build().unwrap();
905
906        let mut parsed = PdfDocument::from_bytes(bytes).unwrap();
907        let mut modifier = DocumentModifier::from_document(&mut parsed).unwrap();
908
909        // Deleting page 999 on a 1-page doc should not panic
910        let result = modifier.delete_page(999);
911        assert!(result.is_ok());
912
913        // Original page should still be there
914        let new_bytes = modifier.build().unwrap();
915        let mut reparsed = PdfDocument::from_bytes(new_bytes).unwrap();
916        let pages = crate::page::collect_pages(&reparsed).unwrap();
917        assert_eq!(pages.len(), 1);
918    }
919
920    #[test]
921    fn test_save_io_error() {
922        // Writing to an invalid path should return an I/O error
923        let mut doc = DocumentBuilder::new();
924        let page = PageBuilder::new(612.0, 792.0);
925        doc.add_page(page);
926
927        let result = doc.save(std::path::Path::new("/nonexistent/path/to/file.pdf"));
928        assert!(result.is_err());
929    }
930}