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
11pub struct DocumentBuilder {
13 writer: PdfWriter,
14 pages: Vec<IndirectRef>,
16 fonts: HashMap<String, (String, IndirectRef)>,
18 font_counter: u32,
19 info: PdfDict,
21 pages_obj_num: u32,
23 xmp_ref: Option<IndirectRef>,
25 encryption: Option<crate::crypto::EncryptionConfig>,
27}
28
29impl DocumentBuilder {
30 pub fn new() -> Self {
32 let mut writer = PdfWriter::new();
33 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 pub fn add_standard_font(&mut self, base_font: &str) -> String {
52 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 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 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 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 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 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 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 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 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 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 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 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)); 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 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 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 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 pub fn set_encryption(&mut self, config: crate::crypto::EncryptionConfig) {
243 self.encryption = Some(config);
244 }
245
246 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 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 pub fn build(mut self) -> Result<Vec<u8>> {
288 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 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 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 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 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 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
373pub 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 let res_name = format!("Im{}", image_ref.obj_num);
416
417 Ok((res_name, image_ref))
418}
419
420fn 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 if marker == 0xC0 || marker == 0xC2 {
444 if pos + 7 > data.len() {
445 break;
446 }
447 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 if pos + 1 >= data.len() {
456 break;
457 }
458 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
475fn 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 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
507pub 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 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 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 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 let compressed_rgb = encode_flate(&rgb_data)?;
584
585 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 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 assert!(bytes.starts_with(b"%PDF-1.7"));
665
666 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 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 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 let data: [u8; 12] = [
737 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0, ];
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 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 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, 0, 255, 0, 255, 0, 0, 255, 0, 255, 255, 0, 64, ];
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 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 let jpeg_fixed = vec![
830 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x02, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x64, 0x00, 0xC8, 0x03, ];
840
841 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 #[test]
857 fn test_negative_page_size() {
858 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 let mut doc = DocumentBuilder::new();
872 let name = doc.add_standard_font("");
873 assert!(!name.is_empty()); }
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 let result = modifier.delete_page(999);
911 assert!(result.is_ok());
912
913 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 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}