oxidize_pdf/writer/pdf_writer.rs
1use crate::document::Document;
2use crate::error::Result;
3use crate::objects::{Dictionary, Object, ObjectId};
4use crate::writer::XRefStreamWriter;
5use chrono::{DateTime, Utc};
6use std::collections::HashMap;
7use std::io::{BufWriter, Write};
8use std::path::Path;
9
10/// Configuration for PDF writer
11#[derive(Debug, Clone)]
12pub struct WriterConfig {
13 /// Use XRef streams instead of traditional XRef tables (PDF 1.5+)
14 pub use_xref_streams: bool,
15 /// PDF version to write (default: 1.7)
16 pub pdf_version: String,
17 /// Enable compression for streams (default: true)
18 pub compress_streams: bool,
19}
20
21impl Default for WriterConfig {
22 fn default() -> Self {
23 Self {
24 use_xref_streams: false,
25 pdf_version: "1.7".to_string(),
26 compress_streams: true,
27 }
28 }
29}
30
31pub struct PdfWriter<W: Write> {
32 writer: W,
33 xref_positions: HashMap<ObjectId, u64>,
34 current_position: u64,
35 next_object_id: u32,
36 // Maps for tracking object IDs during writing
37 catalog_id: Option<ObjectId>,
38 pages_id: Option<ObjectId>,
39 info_id: Option<ObjectId>,
40 // Maps for tracking form fields and their widgets
41 #[allow(dead_code)]
42 field_widget_map: HashMap<String, Vec<ObjectId>>, // field name -> widget IDs
43 #[allow(dead_code)]
44 field_id_map: HashMap<String, ObjectId>, // field name -> field ID
45 form_field_ids: Vec<ObjectId>, // form field IDs to add to page annotations
46 page_ids: Vec<ObjectId>, // page IDs for form field references
47 // Configuration
48 config: WriterConfig,
49 // Characters used in document (for font subsetting)
50 document_used_chars: Option<std::collections::HashSet<char>>,
51}
52
53impl<W: Write> PdfWriter<W> {
54 pub fn new_with_writer(writer: W) -> Self {
55 Self::with_config(writer, WriterConfig::default())
56 }
57
58 pub fn with_config(writer: W, config: WriterConfig) -> Self {
59 Self {
60 writer,
61 xref_positions: HashMap::new(),
62 current_position: 0,
63 next_object_id: 1, // Start at 1 for sequential numbering
64 catalog_id: None,
65 pages_id: None,
66 info_id: None,
67 field_widget_map: HashMap::new(),
68 field_id_map: HashMap::new(),
69 form_field_ids: Vec::new(),
70 page_ids: Vec::new(),
71 config,
72 document_used_chars: None,
73 }
74 }
75
76 pub fn write_document(&mut self, document: &mut Document) -> Result<()> {
77 // Store used characters for font subsetting
78 if !document.used_characters.is_empty() {
79 self.document_used_chars = Some(document.used_characters.clone());
80 }
81
82 self.write_header()?;
83
84 // Reserve object IDs for fixed objects (written in order)
85 self.catalog_id = Some(self.allocate_object_id());
86 self.pages_id = Some(self.allocate_object_id());
87 self.info_id = Some(self.allocate_object_id());
88
89 // Write custom fonts first (so pages can reference them)
90 let font_refs = self.write_fonts(document)?;
91
92 // Write pages (they contain widget annotations and font references)
93 self.write_pages(document, &font_refs)?;
94
95 // Write form fields (must be after pages so we can track widgets)
96 self.write_form_fields(document)?;
97
98 // Write catalog (must be after forms so AcroForm has correct field references)
99 self.write_catalog(document)?;
100
101 // Write document info
102 self.write_info(document)?;
103
104 // Write xref table or stream
105 let xref_position = self.current_position;
106 if self.config.use_xref_streams {
107 self.write_xref_stream()?;
108 } else {
109 self.write_xref()?;
110 }
111
112 // Write trailer (only for traditional xref)
113 if !self.config.use_xref_streams {
114 self.write_trailer(xref_position)?;
115 }
116
117 if let Ok(()) = self.writer.flush() {
118 // Flush succeeded
119 }
120 Ok(())
121 }
122
123 fn write_header(&mut self) -> Result<()> {
124 let header = format!("%PDF-{}\n", self.config.pdf_version);
125 self.write_bytes(header.as_bytes())?;
126 // Binary comment to ensure file is treated as binary
127 self.write_bytes(&[b'%', 0xE2, 0xE3, 0xCF, 0xD3, b'\n'])?;
128 Ok(())
129 }
130
131 fn write_catalog(&mut self, document: &mut Document) -> Result<()> {
132 let catalog_id = self.catalog_id.expect("catalog_id must be set");
133 let pages_id = self.pages_id.expect("pages_id must be set");
134
135 let mut catalog = Dictionary::new();
136 catalog.set("Type", Object::Name("Catalog".to_string()));
137 catalog.set("Pages", Object::Reference(pages_id));
138
139 // Process FormManager if present to update AcroForm
140 // We'll write the actual fields after pages are written
141 if let Some(_form_manager) = &document.form_manager {
142 // Ensure AcroForm exists
143 if document.acro_form.is_none() {
144 document.acro_form = Some(crate::forms::AcroForm::new());
145 }
146 }
147
148 // Add AcroForm if present
149 if let Some(acro_form) = &document.acro_form {
150 // Reserve object ID for AcroForm
151 let acro_form_id = self.allocate_object_id();
152
153 // Write AcroForm object
154 self.write_object(acro_form_id, Object::Dictionary(acro_form.to_dict()))?;
155
156 // Reference it in catalog
157 catalog.set("AcroForm", Object::Reference(acro_form_id));
158 }
159
160 // Add Outlines if present
161 if let Some(outline_tree) = &document.outline {
162 if !outline_tree.items.is_empty() {
163 let outline_root_id = self.write_outline_tree(outline_tree)?;
164 catalog.set("Outlines", Object::Reference(outline_root_id));
165 }
166 }
167
168 self.write_object(catalog_id, Object::Dictionary(catalog))?;
169 Ok(())
170 }
171
172 fn write_page_content(&mut self, content_id: ObjectId, page: &crate::page::Page) -> Result<()> {
173 let mut page_copy = page.clone();
174 let content = page_copy.generate_content()?;
175
176 // Create stream with compression if enabled
177 #[cfg(feature = "compression")]
178 {
179 use crate::objects::Stream;
180 let mut stream = Stream::new(content);
181 // Only compress if config allows it
182 if self.config.compress_streams {
183 stream.compress_flate()?;
184 }
185
186 self.write_object(
187 content_id,
188 Object::Stream(stream.dictionary().clone(), stream.data().to_vec()),
189 )?;
190 }
191
192 #[cfg(not(feature = "compression"))]
193 {
194 let mut stream_dict = Dictionary::new();
195 stream_dict.set("Length", Object::Integer(content.len() as i64));
196
197 self.write_object(content_id, Object::Stream(stream_dict, content))?;
198 }
199
200 Ok(())
201 }
202
203 fn write_outline_tree(
204 &mut self,
205 outline_tree: &crate::structure::OutlineTree,
206 ) -> Result<ObjectId> {
207 // Create root outline dictionary
208 let outline_root_id = self.allocate_object_id();
209
210 let mut outline_root = Dictionary::new();
211 outline_root.set("Type", Object::Name("Outlines".to_string()));
212
213 if !outline_tree.items.is_empty() {
214 // Reserve IDs for all outline items
215 let mut item_ids = Vec::new();
216
217 // Count all items and assign IDs
218 fn count_items(items: &[crate::structure::OutlineItem]) -> usize {
219 let mut count = items.len();
220 for item in items {
221 count += count_items(&item.children);
222 }
223 count
224 }
225
226 let total_items = count_items(&outline_tree.items);
227
228 // Reserve IDs for all items
229 for _ in 0..total_items {
230 item_ids.push(self.allocate_object_id());
231 }
232
233 let mut id_index = 0;
234
235 // Write root items
236 let first_id = item_ids[0];
237 let last_id = item_ids[outline_tree.items.len() - 1];
238
239 outline_root.set("First", Object::Reference(first_id));
240 outline_root.set("Last", Object::Reference(last_id));
241
242 // Visible count
243 let visible_count = outline_tree.visible_count();
244 outline_root.set("Count", Object::Integer(visible_count));
245
246 // Write all items recursively
247 let mut written_items = Vec::new();
248
249 for (i, item) in outline_tree.items.iter().enumerate() {
250 let item_id = item_ids[id_index];
251 id_index += 1;
252
253 let prev_id = if i > 0 { Some(item_ids[i - 1]) } else { None };
254 let next_id = if i < outline_tree.items.len() - 1 {
255 Some(item_ids[i + 1])
256 } else {
257 None
258 };
259
260 // Write this item and its children
261 let children_ids = self.write_outline_item(
262 item,
263 item_id,
264 outline_root_id,
265 prev_id,
266 next_id,
267 &mut item_ids,
268 &mut id_index,
269 )?;
270
271 written_items.extend(children_ids);
272 }
273 }
274
275 self.write_object(outline_root_id, Object::Dictionary(outline_root))?;
276 Ok(outline_root_id)
277 }
278
279 #[allow(clippy::too_many_arguments)]
280 fn write_outline_item(
281 &mut self,
282 item: &crate::structure::OutlineItem,
283 item_id: ObjectId,
284 parent_id: ObjectId,
285 prev_id: Option<ObjectId>,
286 next_id: Option<ObjectId>,
287 all_ids: &mut Vec<ObjectId>,
288 id_index: &mut usize,
289 ) -> Result<Vec<ObjectId>> {
290 let mut written_ids = vec![item_id];
291
292 // Handle children if any
293 let (first_child_id, last_child_id) = if !item.children.is_empty() {
294 let first_idx = *id_index;
295 let first_id = all_ids[first_idx];
296 let last_idx = first_idx + item.children.len() - 1;
297 let last_id = all_ids[last_idx];
298
299 // Write children
300 for (i, child) in item.children.iter().enumerate() {
301 let child_id = all_ids[*id_index];
302 *id_index += 1;
303
304 let child_prev = if i > 0 {
305 Some(all_ids[first_idx + i - 1])
306 } else {
307 None
308 };
309 let child_next = if i < item.children.len() - 1 {
310 Some(all_ids[first_idx + i + 1])
311 } else {
312 None
313 };
314
315 let child_ids = self.write_outline_item(
316 child, child_id, item_id, // This item is the parent
317 child_prev, child_next, all_ids, id_index,
318 )?;
319
320 written_ids.extend(child_ids);
321 }
322
323 (Some(first_id), Some(last_id))
324 } else {
325 (None, None)
326 };
327
328 // Create item dictionary
329 let item_dict = crate::structure::outline_item_to_dict(
330 item,
331 parent_id,
332 first_child_id,
333 last_child_id,
334 prev_id,
335 next_id,
336 );
337
338 self.write_object(item_id, Object::Dictionary(item_dict))?;
339
340 Ok(written_ids)
341 }
342
343 fn write_form_fields(&mut self, document: &mut Document) -> Result<()> {
344 // Add collected form field IDs to AcroForm
345 if !self.form_field_ids.is_empty() {
346 if let Some(acro_form) = &mut document.acro_form {
347 // Clear any existing fields and add the ones we found
348 acro_form.fields.clear();
349 for field_id in &self.form_field_ids {
350 acro_form.add_field(*field_id);
351 }
352
353 // Ensure AcroForm has the right properties
354 acro_form.need_appearances = true;
355 if acro_form.da.is_none() {
356 acro_form.da = Some("/Helv 12 Tf 0 g".to_string());
357 }
358 }
359 }
360 Ok(())
361 }
362
363 fn write_info(&mut self, document: &Document) -> Result<()> {
364 let info_id = self.info_id.expect("info_id must be set");
365 let mut info_dict = Dictionary::new();
366
367 if let Some(ref title) = document.metadata.title {
368 info_dict.set("Title", Object::String(title.clone()));
369 }
370 if let Some(ref author) = document.metadata.author {
371 info_dict.set("Author", Object::String(author.clone()));
372 }
373 if let Some(ref subject) = document.metadata.subject {
374 info_dict.set("Subject", Object::String(subject.clone()));
375 }
376 if let Some(ref keywords) = document.metadata.keywords {
377 info_dict.set("Keywords", Object::String(keywords.clone()));
378 }
379 if let Some(ref creator) = document.metadata.creator {
380 info_dict.set("Creator", Object::String(creator.clone()));
381 }
382 if let Some(ref producer) = document.metadata.producer {
383 info_dict.set("Producer", Object::String(producer.clone()));
384 }
385
386 // Add creation date
387 if let Some(creation_date) = document.metadata.creation_date {
388 let date_string = format_pdf_date(creation_date);
389 info_dict.set("CreationDate", Object::String(date_string));
390 }
391
392 // Add modification date
393 if let Some(mod_date) = document.metadata.modification_date {
394 let date_string = format_pdf_date(mod_date);
395 info_dict.set("ModDate", Object::String(date_string));
396 }
397
398 self.write_object(info_id, Object::Dictionary(info_dict))?;
399 Ok(())
400 }
401
402 fn write_fonts(&mut self, document: &Document) -> Result<HashMap<String, ObjectId>> {
403 let mut font_refs = HashMap::new();
404
405 // Write custom fonts from the document
406 for font_name in document.custom_font_names() {
407 if let Some(font) = document.get_custom_font(&font_name) {
408 // For now, write all custom fonts as TrueType with Identity-H for Unicode support
409 // The font from document is Arc<fonts::Font>, not text::font_manager::CustomFont
410 let font_id = self.write_font_with_unicode_support(&font_name, &font)?;
411 font_refs.insert(font_name.clone(), font_id);
412 }
413 }
414
415 Ok(font_refs)
416 }
417
418 /// Write font with automatic Unicode support detection
419 fn write_font_with_unicode_support(
420 &mut self,
421 font_name: &str,
422 font: &crate::fonts::Font,
423 ) -> Result<ObjectId> {
424 // Check if any text in the document needs Unicode
425 // For simplicity, always use Type0 for full Unicode support
426 self.write_type0_font_from_font(font_name, font)
427 }
428
429 /// Write a Type0 font with CID support from fonts::Font
430 fn write_type0_font_from_font(
431 &mut self,
432 font_name: &str,
433 font: &crate::fonts::Font,
434 ) -> Result<ObjectId> {
435 // Get used characters from document for subsetting
436 let used_chars = self.document_used_chars.clone().unwrap_or_else(|| {
437 // If no tracking, include common characters as fallback
438 let mut chars = std::collections::HashSet::new();
439 for ch in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,!?".chars()
440 {
441 chars.insert(ch);
442 }
443 chars
444 });
445 // Allocate IDs for all font objects
446 let font_id = self.allocate_object_id();
447 let descendant_font_id = self.allocate_object_id();
448 let descriptor_id = self.allocate_object_id();
449 let font_file_id = self.allocate_object_id();
450 let to_unicode_id = self.allocate_object_id();
451
452 // Write font file (embedded TTF data with subsetting for large fonts)
453 // Keep track of the glyph mapping if we subset the font
454 // IMPORTANT: We need the ORIGINAL font for width calculations, not the subset
455 let (font_data_to_embed, subset_glyph_mapping, original_font_for_widths) =
456 if font.data.len() > 100_000 && !used_chars.is_empty() {
457 // Large font - try to subset it
458 match crate::text::fonts::truetype_subsetter::subset_font(
459 font.data.clone(),
460 &used_chars,
461 ) {
462 Ok(subset_result) => {
463 // Successfully subsetted - keep both font data and mapping
464 // Also keep reference to original font for width calculations
465 (
466 subset_result.font_data,
467 Some(subset_result.glyph_mapping),
468 font.clone(),
469 )
470 }
471 Err(_) => {
472 // Subsetting failed, use original if under 25MB
473 if font.data.len() < 25_000_000 {
474 (font.data.clone(), None, font.clone())
475 } else {
476 // Too large even for fallback
477 (Vec::new(), None, font.clone())
478 }
479 }
480 }
481 } else {
482 // Small font or no character tracking - use as-is
483 (font.data.clone(), None, font.clone())
484 };
485
486 if !font_data_to_embed.is_empty() {
487 let mut font_file_dict = Dictionary::new();
488 font_file_dict.set("Length1", Object::Integer(font_data_to_embed.len() as i64));
489 let font_stream_obj = Object::Stream(font_file_dict, font_data_to_embed);
490 self.write_object(font_file_id, font_stream_obj)?;
491 } else {
492 // No font data to embed
493 let font_file_dict = Dictionary::new();
494 let font_stream_obj = Object::Stream(font_file_dict, Vec::new());
495 self.write_object(font_file_id, font_stream_obj)?;
496 }
497
498 // Write font descriptor
499 let mut descriptor = Dictionary::new();
500 descriptor.set("Type", Object::Name("FontDescriptor".to_string()));
501 descriptor.set("FontName", Object::Name(font_name.to_string()));
502 descriptor.set("Flags", Object::Integer(4)); // Symbolic font
503 descriptor.set(
504 "FontBBox",
505 Object::Array(vec![
506 Object::Integer(font.descriptor.font_bbox[0] as i64),
507 Object::Integer(font.descriptor.font_bbox[1] as i64),
508 Object::Integer(font.descriptor.font_bbox[2] as i64),
509 Object::Integer(font.descriptor.font_bbox[3] as i64),
510 ]),
511 );
512 descriptor.set(
513 "ItalicAngle",
514 Object::Real(font.descriptor.italic_angle as f64),
515 );
516 descriptor.set("Ascent", Object::Real(font.descriptor.ascent as f64));
517 descriptor.set("Descent", Object::Real(font.descriptor.descent as f64));
518 descriptor.set("CapHeight", Object::Real(font.descriptor.cap_height as f64));
519 descriptor.set("StemV", Object::Real(font.descriptor.stem_v as f64));
520 descriptor.set("FontFile2", Object::Reference(font_file_id));
521 self.write_object(descriptor_id, Object::Dictionary(descriptor))?;
522
523 // Write CIDFont (descendant font)
524 let mut cid_font = Dictionary::new();
525 cid_font.set("Type", Object::Name("Font".to_string()));
526 cid_font.set("Subtype", Object::Name("CIDFontType2".to_string()));
527 cid_font.set("BaseFont", Object::Name(font_name.to_string()));
528
529 // CIDSystemInfo
530 let mut cid_system_info = Dictionary::new();
531 cid_system_info.set("Registry", Object::String("Adobe".to_string()));
532 cid_system_info.set("Ordering", Object::String("Identity".to_string()));
533 cid_system_info.set("Supplement", Object::Integer(0));
534 cid_font.set("CIDSystemInfo", Object::Dictionary(cid_system_info));
535
536 cid_font.set("FontDescriptor", Object::Reference(descriptor_id));
537
538 // Calculate a better default width based on font metrics
539 let default_width = self.calculate_default_width(font);
540 cid_font.set("DW", Object::Integer(default_width));
541
542 // Generate proper width array from font metrics
543 // IMPORTANT: Use the ORIGINAL font for width calculations, not the subset
544 // But pass the subset mapping to know which characters we're using
545 let w_array = self.generate_width_array(
546 &original_font_for_widths,
547 default_width,
548 subset_glyph_mapping.as_ref(),
549 );
550 cid_font.set("W", Object::Array(w_array));
551
552 // CIDToGIDMap - Generate proper mapping from CID (Unicode) to GlyphID
553 // This is critical for Type0 fonts to work correctly
554 // If we subsetted the font, use the new glyph mapping
555 let cid_to_gid_map = self.generate_cid_to_gid_map(font, subset_glyph_mapping.as_ref())?;
556 if !cid_to_gid_map.is_empty() {
557 // Write the CIDToGIDMap as a stream
558 let cid_to_gid_map_id = self.allocate_object_id();
559 let mut map_dict = Dictionary::new();
560 map_dict.set("Length", Object::Integer(cid_to_gid_map.len() as i64));
561 let map_stream = Object::Stream(map_dict, cid_to_gid_map);
562 self.write_object(cid_to_gid_map_id, map_stream)?;
563 cid_font.set("CIDToGIDMap", Object::Reference(cid_to_gid_map_id));
564 } else {
565 cid_font.set("CIDToGIDMap", Object::Name("Identity".to_string()));
566 }
567
568 self.write_object(descendant_font_id, Object::Dictionary(cid_font))?;
569
570 // Write ToUnicode CMap
571 let cmap_data = self.generate_tounicode_cmap_from_font(font);
572 let cmap_dict = Dictionary::new();
573 let cmap_stream = Object::Stream(cmap_dict, cmap_data);
574 self.write_object(to_unicode_id, cmap_stream)?;
575
576 // Write Type0 font (main font)
577 let mut type0_font = Dictionary::new();
578 type0_font.set("Type", Object::Name("Font".to_string()));
579 type0_font.set("Subtype", Object::Name("Type0".to_string()));
580 type0_font.set("BaseFont", Object::Name(font_name.to_string()));
581 type0_font.set("Encoding", Object::Name("Identity-H".to_string()));
582 type0_font.set(
583 "DescendantFonts",
584 Object::Array(vec![Object::Reference(descendant_font_id)]),
585 );
586 type0_font.set("ToUnicode", Object::Reference(to_unicode_id));
587
588 self.write_object(font_id, Object::Dictionary(type0_font))?;
589
590 Ok(font_id)
591 }
592
593 /// Calculate default width based on common characters
594 fn calculate_default_width(&self, font: &crate::fonts::Font) -> i64 {
595 use crate::text::fonts::truetype::TrueTypeFont;
596
597 // Try to calculate from actual font metrics
598 if let Ok(tt_font) = TrueTypeFont::parse(font.data.clone()) {
599 if let Ok(cmap_tables) = tt_font.parse_cmap() {
600 if let Some(cmap) = cmap_tables
601 .iter()
602 .find(|t| t.platform_id == 3 && t.encoding_id == 1)
603 .or_else(|| cmap_tables.iter().find(|t| t.platform_id == 0))
604 {
605 if let Ok(widths) = tt_font.get_glyph_widths(&cmap.mappings) {
606 // NOTE: get_glyph_widths already returns widths in PDF units (1000 per em)
607
608 // Calculate average width of common Latin characters
609 let common_chars =
610 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ";
611 let mut total_width = 0;
612 let mut count = 0;
613
614 for ch in common_chars.chars() {
615 let unicode = ch as u32;
616 if let Some(&pdf_width) = widths.get(&unicode) {
617 total_width += pdf_width as i64;
618 count += 1;
619 }
620 }
621
622 if count > 0 {
623 return total_width / count;
624 }
625 }
626 }
627 }
628 }
629
630 // Fallback default if we can't calculate
631 500
632 }
633
634 /// Generate width array for CID font
635 fn generate_width_array(
636 &self,
637 font: &crate::fonts::Font,
638 _default_width: i64,
639 subset_mapping: Option<&HashMap<u32, u16>>,
640 ) -> Vec<Object> {
641 use crate::text::fonts::truetype::TrueTypeFont;
642
643 let mut w_array = Vec::new();
644
645 // Try to get actual glyph widths from the font
646 if let Ok(tt_font) = TrueTypeFont::parse(font.data.clone()) {
647 // IMPORTANT: Always use ORIGINAL mappings for width calculation
648 // The subset_mapping has NEW GlyphIDs which don't correspond to the right glyphs
649 // in the original font's width table
650 let char_to_glyph = {
651 // Parse cmap to get original mappings
652 if let Ok(cmap_tables) = tt_font.parse_cmap() {
653 if let Some(cmap) = cmap_tables
654 .iter()
655 .find(|t| t.platform_id == 3 && t.encoding_id == 1)
656 .or_else(|| cmap_tables.iter().find(|t| t.platform_id == 0))
657 {
658 // If we have subset_mapping, filter to only include used characters
659 if let Some(subset_map) = subset_mapping {
660 let mut filtered = HashMap::new();
661 for unicode in subset_map.keys() {
662 // Get the ORIGINAL GlyphID for this Unicode
663 if let Some(&orig_glyph) = cmap.mappings.get(unicode) {
664 filtered.insert(*unicode, orig_glyph);
665 }
666 }
667 filtered
668 } else {
669 cmap.mappings.clone()
670 }
671 } else {
672 HashMap::new()
673 }
674 } else {
675 HashMap::new()
676 }
677 };
678
679 if !char_to_glyph.is_empty() {
680 // Get actual widths from the font
681 if let Ok(widths) = tt_font.get_glyph_widths(&char_to_glyph) {
682 // NOTE: get_glyph_widths already returns widths scaled to PDF units (1000 per em)
683 // So we DON'T need to scale them again here
684
685 // Group consecutive characters with same width for efficiency
686 let mut sorted_chars: Vec<_> = widths.iter().collect();
687 sorted_chars.sort_by_key(|(unicode, _)| *unicode);
688
689 let mut i = 0;
690 while i < sorted_chars.len() {
691 let start_unicode = *sorted_chars[i].0;
692 // Width is already in PDF units from get_glyph_widths
693 let pdf_width = *sorted_chars[i].1 as i64;
694
695 // Find consecutive characters with same width
696 let mut end_unicode = start_unicode;
697 let mut j = i + 1;
698 while j < sorted_chars.len() && *sorted_chars[j].0 == end_unicode + 1 {
699 let next_pdf_width = *sorted_chars[j].1 as i64;
700 if next_pdf_width == pdf_width {
701 end_unicode = *sorted_chars[j].0;
702 j += 1;
703 } else {
704 break;
705 }
706 }
707
708 // Add to W array
709 if start_unicode == end_unicode {
710 // Single character
711 w_array.push(Object::Integer(start_unicode as i64));
712 w_array.push(Object::Array(vec![Object::Integer(pdf_width)]));
713 } else {
714 // Range of characters
715 w_array.push(Object::Integer(start_unicode as i64));
716 w_array.push(Object::Integer(end_unicode as i64));
717 w_array.push(Object::Integer(pdf_width));
718 }
719
720 i = j;
721 }
722
723 return w_array;
724 }
725 }
726 }
727
728 // Fallback to reasonable default widths if we can't parse the font
729 let ranges = vec![
730 // Space character should be narrower
731 (0x20, 0x20, 250), // Space
732 (0x21, 0x2F, 333), // Punctuation
733 (0x30, 0x39, 500), // Numbers (0-9)
734 (0x3A, 0x40, 333), // More punctuation
735 (0x41, 0x5A, 667), // Uppercase letters (A-Z)
736 (0x5B, 0x60, 333), // Brackets
737 (0x61, 0x7A, 500), // Lowercase letters (a-z)
738 (0x7B, 0x7E, 333), // More brackets
739 // Extended Latin
740 (0xA0, 0xA0, 250), // Non-breaking space
741 (0xA1, 0xBF, 333), // Latin-1 punctuation
742 (0xC0, 0xD6, 667), // Latin-1 uppercase
743 (0xD7, 0xD7, 564), // Multiplication sign
744 (0xD8, 0xDE, 667), // More Latin-1 uppercase
745 (0xDF, 0xF6, 500), // Latin-1 lowercase
746 (0xF7, 0xF7, 564), // Division sign
747 (0xF8, 0xFF, 500), // More Latin-1 lowercase
748 // Latin Extended-A
749 (0x100, 0x17F, 500), // Latin Extended-A
750 // Symbols and special characters
751 (0x2000, 0x200F, 250), // Various spaces
752 (0x2010, 0x2027, 333), // Hyphens and dashes
753 (0x2028, 0x202F, 250), // More spaces
754 (0x2030, 0x206F, 500), // General Punctuation
755 (0x2070, 0x209F, 400), // Superscripts
756 (0x20A0, 0x20CF, 600), // Currency symbols
757 (0x2100, 0x214F, 700), // Letterlike symbols
758 (0x2190, 0x21FF, 600), // Arrows
759 (0x2200, 0x22FF, 600), // Mathematical operators
760 (0x2300, 0x23FF, 600), // Miscellaneous technical
761 (0x2500, 0x257F, 500), // Box drawing
762 (0x2580, 0x259F, 500), // Block elements
763 (0x25A0, 0x25FF, 600), // Geometric shapes
764 (0x2600, 0x26FF, 600), // Miscellaneous symbols
765 (0x2700, 0x27BF, 600), // Dingbats
766 ];
767
768 // Convert ranges to W array format
769 for (start, end, width) in ranges {
770 if start == end {
771 // Single character
772 w_array.push(Object::Integer(start));
773 w_array.push(Object::Array(vec![Object::Integer(width)]));
774 } else {
775 // Range of characters
776 w_array.push(Object::Integer(start));
777 w_array.push(Object::Integer(end));
778 w_array.push(Object::Integer(width));
779 }
780 }
781
782 w_array
783 }
784
785 /// Generate CIDToGIDMap for Type0 font
786 fn generate_cid_to_gid_map(
787 &mut self,
788 font: &crate::fonts::Font,
789 subset_mapping: Option<&HashMap<u32, u16>>,
790 ) -> Result<Vec<u8>> {
791 use crate::text::fonts::truetype::TrueTypeFont;
792
793 // If we have a subset mapping, use it directly
794 // Otherwise, parse the font to get the original cmap table
795 let cmap_mappings = if let Some(subset_map) = subset_mapping {
796 // Use the subset mapping directly
797 subset_map.clone()
798 } else {
799 // Parse the font to get the original cmap table
800 let tt_font = TrueTypeFont::parse(font.data.clone())?;
801 let cmap_tables = tt_font.parse_cmap()?;
802
803 // Find the best cmap table (Unicode)
804 let cmap = cmap_tables
805 .iter()
806 .find(|t| t.platform_id == 3 && t.encoding_id == 1) // Windows Unicode
807 .or_else(|| cmap_tables.iter().find(|t| t.platform_id == 0)) // Unicode
808 .ok_or_else(|| {
809 crate::error::PdfError::FontError("No Unicode cmap table found".to_string())
810 })?;
811
812 cmap.mappings.clone()
813 };
814
815 // Build the CIDToGIDMap
816 // Since we use Unicode code points as CIDs, we need to map Unicode → GlyphID
817 // The map is a binary array where index = CID (Unicode) * 2, value = GlyphID (big-endian)
818
819 // OPTIMIZATION: Only create map for characters actually used in the document
820 // Get used characters from document tracking
821 let used_chars = self.document_used_chars.clone().unwrap_or_default();
822
823 // Find the maximum Unicode value from used characters or full font
824 let max_unicode = if !used_chars.is_empty() {
825 // If we have used chars tracking, only map up to the highest used character
826 used_chars
827 .iter()
828 .map(|ch| *ch as u32)
829 .max()
830 .unwrap_or(0x00FF) // At least Basic Latin
831 .min(0xFFFF) as usize
832 } else {
833 // Fallback to original behavior if no tracking
834 cmap_mappings
835 .keys()
836 .max()
837 .copied()
838 .unwrap_or(0xFFFF)
839 .min(0xFFFF) as usize
840 };
841
842 // Create the map: 2 bytes per entry
843 let mut map = vec![0u8; (max_unicode + 1) * 2];
844
845 // Fill in the mappings
846 let mut sample_mappings = Vec::new();
847 for (&unicode, &glyph_id) in &cmap_mappings {
848 if unicode <= max_unicode as u32 {
849 let idx = (unicode as usize) * 2;
850 // Write glyph_id in big-endian format
851 map[idx] = (glyph_id >> 8) as u8;
852 map[idx + 1] = (glyph_id & 0xFF) as u8;
853
854 // Collect some sample mappings for debugging
855 if unicode == 0x0041 || unicode == 0x0061 || unicode == 0x00E1 || unicode == 0x00F1
856 {
857 sample_mappings.push((unicode, glyph_id));
858 }
859 }
860 }
861
862 // Debug output
863 let optimization_ratio = if !used_chars.is_empty() {
864 let full_size = (0xFFFF + 1) * 2;
865 let optimized_size = map.len();
866 format!(
867 " (optimized from {} KB to {} KB, {:.1}% reduction)",
868 full_size / 1024,
869 optimized_size / 1024,
870 (1.0 - optimized_size as f32 / full_size as f32) * 100.0
871 )
872 } else {
873 String::new()
874 };
875
876 println!(
877 "Generated CIDToGIDMap: {} bytes for max CID {:#06X}{}",
878 map.len(),
879 max_unicode,
880 optimization_ratio
881 );
882
883 // Show mappings for test characters
884 let test_chars = "Hello áéíóú";
885 println!("Sample mappings:");
886 for ch in test_chars.chars() {
887 let unicode = ch as u32;
888 if let Some(&glyph_id) = cmap_mappings.get(&unicode) {
889 println!(" '{}' (U+{:04X}) → GlyphID {}", ch, unicode, glyph_id);
890 } else {
891 println!(" '{}' (U+{:04X}) → NOT FOUND", ch, unicode);
892 }
893 }
894
895 Ok(map)
896 }
897
898 /// Generate ToUnicode CMap for Type0 font from fonts::Font
899 fn generate_tounicode_cmap_from_font(&self, font: &crate::fonts::Font) -> Vec<u8> {
900 use crate::text::fonts::truetype::TrueTypeFont;
901
902 let mut cmap = String::new();
903
904 // CMap header
905 cmap.push_str("/CIDInit /ProcSet findresource begin\n");
906 cmap.push_str("12 dict begin\n");
907 cmap.push_str("begincmap\n");
908 cmap.push_str("/CIDSystemInfo\n");
909 cmap.push_str("<< /Registry (Adobe)\n");
910 cmap.push_str(" /Ordering (UCS)\n");
911 cmap.push_str(" /Supplement 0\n");
912 cmap.push_str(">> def\n");
913 cmap.push_str("/CMapName /Adobe-Identity-UCS def\n");
914 cmap.push_str("/CMapType 2 def\n");
915 cmap.push_str("1 begincodespacerange\n");
916 cmap.push_str("<0000> <FFFF>\n");
917 cmap.push_str("endcodespacerange\n");
918
919 // Try to get actual mappings from the font
920 let mut mappings = Vec::new();
921 let mut has_font_mappings = false;
922
923 if let Ok(tt_font) = TrueTypeFont::parse(font.data.clone()) {
924 if let Ok(cmap_tables) = tt_font.parse_cmap() {
925 // Find the best cmap table (Unicode)
926 if let Some(cmap_table) = cmap_tables
927 .iter()
928 .find(|t| t.platform_id == 3 && t.encoding_id == 1) // Windows Unicode
929 .or_else(|| cmap_tables.iter().find(|t| t.platform_id == 0))
930 // Unicode
931 {
932 // For Identity-H encoding, we use Unicode code points as CIDs
933 // So the ToUnicode CMap should map CID (=Unicode) → Unicode
934 for (&unicode, &glyph_id) in &cmap_table.mappings {
935 if glyph_id > 0 && unicode <= 0xFFFF {
936 // Only non-.notdef glyphs
937 // Map CID (which is Unicode value) to Unicode
938 mappings.push((unicode, unicode));
939 }
940 }
941 has_font_mappings = true;
942 }
943 }
944 }
945
946 // If we couldn't get font mappings, use identity mapping for common ranges
947 if !has_font_mappings {
948 // Basic Latin and Latin-1 Supplement (0x0020-0x00FF)
949 for i in 0x0020..=0x00FF {
950 mappings.push((i, i));
951 }
952
953 // Latin Extended-A (0x0100-0x017F)
954 for i in 0x0100..=0x017F {
955 mappings.push((i, i));
956 }
957
958 // Common symbols and punctuation
959 for i in 0x2000..=0x206F {
960 mappings.push((i, i));
961 }
962
963 // Mathematical symbols
964 for i in 0x2200..=0x22FF {
965 mappings.push((i, i));
966 }
967
968 // Arrows
969 for i in 0x2190..=0x21FF {
970 mappings.push((i, i));
971 }
972
973 // Box drawing
974 for i in 0x2500..=0x259F {
975 mappings.push((i, i));
976 }
977
978 // Geometric shapes
979 for i in 0x25A0..=0x25FF {
980 mappings.push((i, i));
981 }
982
983 // Miscellaneous symbols
984 for i in 0x2600..=0x26FF {
985 mappings.push((i, i));
986 }
987 }
988
989 // Sort mappings by CID for better organization
990 mappings.sort_by_key(|&(cid, _)| cid);
991
992 // Use more efficient bfrange where possible
993 let mut i = 0;
994 while i < mappings.len() {
995 // Check if we can use a range
996 let start_cid = mappings[i].0;
997 let start_unicode = mappings[i].1;
998 let mut end_idx = i;
999
1000 // Find consecutive mappings
1001 while end_idx + 1 < mappings.len()
1002 && mappings[end_idx + 1].0 == mappings[end_idx].0 + 1
1003 && mappings[end_idx + 1].1 == mappings[end_idx].1 + 1
1004 && end_idx - i < 99
1005 // Max 100 per block
1006 {
1007 end_idx += 1;
1008 }
1009
1010 if end_idx > i {
1011 // Use bfrange for consecutive mappings
1012 cmap.push_str("1 beginbfrange\n");
1013 cmap.push_str(&format!(
1014 "<{:04X}> <{:04X}> <{:04X}>\n",
1015 start_cid, mappings[end_idx].0, start_unicode
1016 ));
1017 cmap.push_str("endbfrange\n");
1018 i = end_idx + 1;
1019 } else {
1020 // Use bfchar for individual mappings
1021 let mut chars = Vec::new();
1022 let chunk_end = (i + 100).min(mappings.len());
1023
1024 for item in &mappings[i..chunk_end] {
1025 chars.push(*item);
1026 }
1027
1028 if !chars.is_empty() {
1029 cmap.push_str(&format!("{} beginbfchar\n", chars.len()));
1030 for (cid, unicode) in chars {
1031 cmap.push_str(&format!("<{:04X}> <{:04X}>\n", cid, unicode));
1032 }
1033 cmap.push_str("endbfchar\n");
1034 }
1035
1036 i = chunk_end;
1037 }
1038 }
1039
1040 // CMap footer
1041 cmap.push_str("endcmap\n");
1042 cmap.push_str("CMapName currentdict /CMap defineresource pop\n");
1043 cmap.push_str("end\n");
1044 cmap.push_str("end\n");
1045
1046 cmap.into_bytes()
1047 }
1048
1049 /// Write a regular TrueType font
1050 #[allow(dead_code)]
1051 fn write_truetype_font(
1052 &mut self,
1053 font_name: &str,
1054 font: &crate::text::font_manager::CustomFont,
1055 ) -> Result<ObjectId> {
1056 // Allocate IDs for font objects
1057 let font_id = self.allocate_object_id();
1058 let descriptor_id = self.allocate_object_id();
1059 let font_file_id = self.allocate_object_id();
1060
1061 // Write font file (embedded TTF data)
1062 if let Some(ref data) = font.font_data {
1063 let mut font_file_dict = Dictionary::new();
1064 font_file_dict.set("Length1", Object::Integer(data.len() as i64));
1065 let font_stream_obj = Object::Stream(font_file_dict, data.clone());
1066 self.write_object(font_file_id, font_stream_obj)?;
1067 }
1068
1069 // Write font descriptor
1070 let mut descriptor = Dictionary::new();
1071 descriptor.set("Type", Object::Name("FontDescriptor".to_string()));
1072 descriptor.set("FontName", Object::Name(font_name.to_string()));
1073 descriptor.set("Flags", Object::Integer(32)); // Non-symbolic font
1074 descriptor.set(
1075 "FontBBox",
1076 Object::Array(vec![
1077 Object::Integer(-1000),
1078 Object::Integer(-1000),
1079 Object::Integer(2000),
1080 Object::Integer(2000),
1081 ]),
1082 );
1083 descriptor.set("ItalicAngle", Object::Integer(0));
1084 descriptor.set("Ascent", Object::Integer(font.descriptor.ascent as i64));
1085 descriptor.set("Descent", Object::Integer(font.descriptor.descent as i64));
1086 descriptor.set(
1087 "CapHeight",
1088 Object::Integer(font.descriptor.cap_height as i64),
1089 );
1090 descriptor.set("StemV", Object::Integer(font.descriptor.stem_v as i64));
1091 descriptor.set("FontFile2", Object::Reference(font_file_id));
1092 self.write_object(descriptor_id, Object::Dictionary(descriptor))?;
1093
1094 // Write font dictionary
1095 let mut font_dict = Dictionary::new();
1096 font_dict.set("Type", Object::Name("Font".to_string()));
1097 font_dict.set("Subtype", Object::Name("TrueType".to_string()));
1098 font_dict.set("BaseFont", Object::Name(font_name.to_string()));
1099 font_dict.set("FirstChar", Object::Integer(0));
1100 font_dict.set("LastChar", Object::Integer(255));
1101
1102 // Create widths array (simplified - all 600)
1103 let widths: Vec<Object> = (0..256).map(|_| Object::Integer(600)).collect();
1104 font_dict.set("Widths", Object::Array(widths));
1105 font_dict.set("FontDescriptor", Object::Reference(descriptor_id));
1106
1107 // Use WinAnsiEncoding for regular TrueType
1108 font_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
1109
1110 self.write_object(font_id, Object::Dictionary(font_dict))?;
1111
1112 Ok(font_id)
1113 }
1114
1115 fn write_pages(
1116 &mut self,
1117 document: &Document,
1118 font_refs: &HashMap<String, ObjectId>,
1119 ) -> Result<()> {
1120 let pages_id = self.pages_id.expect("pages_id must be set");
1121 let mut pages_dict = Dictionary::new();
1122 pages_dict.set("Type", Object::Name("Pages".to_string()));
1123 pages_dict.set("Count", Object::Integer(document.pages.len() as i64));
1124
1125 let mut kids = Vec::new();
1126
1127 // Allocate page object IDs sequentially
1128 let mut page_ids = Vec::new();
1129 let mut content_ids = Vec::new();
1130 for _ in 0..document.pages.len() {
1131 page_ids.push(self.allocate_object_id());
1132 content_ids.push(self.allocate_object_id());
1133 }
1134
1135 for page_id in &page_ids {
1136 kids.push(Object::Reference(*page_id));
1137 }
1138
1139 pages_dict.set("Kids", Object::Array(kids));
1140
1141 self.write_object(pages_id, Object::Dictionary(pages_dict))?;
1142
1143 // Store page IDs for form field references
1144 self.page_ids = page_ids.clone();
1145
1146 // Write individual pages with font references
1147 for (i, page) in document.pages.iter().enumerate() {
1148 let page_id = page_ids[i];
1149 let content_id = content_ids[i];
1150
1151 self.write_page_with_fonts(page_id, pages_id, content_id, page, document, font_refs)?;
1152 self.write_page_content(content_id, page)?;
1153 }
1154
1155 Ok(())
1156 }
1157
1158 /// Compatibility alias for `write_pages` to maintain backwards compatibility
1159 #[allow(dead_code)]
1160 fn write_pages_with_fonts(
1161 &mut self,
1162 document: &Document,
1163 font_refs: &HashMap<String, ObjectId>,
1164 ) -> Result<()> {
1165 self.write_pages(document, font_refs)
1166 }
1167
1168 fn write_page_with_fonts(
1169 &mut self,
1170 page_id: ObjectId,
1171 parent_id: ObjectId,
1172 content_id: ObjectId,
1173 page: &crate::page::Page,
1174 _document: &Document,
1175 font_refs: &HashMap<String, ObjectId>,
1176 ) -> Result<()> {
1177 // Start with the page's dictionary which includes annotations
1178 let mut page_dict = page.to_dict();
1179
1180 page_dict.set("Type", Object::Name("Page".to_string()));
1181 page_dict.set("Parent", Object::Reference(parent_id));
1182 page_dict.set("Contents", Object::Reference(content_id));
1183
1184 // Get resources dictionary or create new one
1185 let mut resources = if let Some(Object::Dictionary(res)) = page_dict.get("Resources") {
1186 res.clone()
1187 } else {
1188 Dictionary::new()
1189 };
1190
1191 // Add font resources
1192 let mut font_dict = Dictionary::new();
1193
1194 // Add ALL standard PDF fonts (Type1) with WinAnsiEncoding
1195 // This fixes the text rendering issue in dashboards where HelveticaBold was missing
1196
1197 // Helvetica family
1198 let mut helvetica_dict = Dictionary::new();
1199 helvetica_dict.set("Type", Object::Name("Font".to_string()));
1200 helvetica_dict.set("Subtype", Object::Name("Type1".to_string()));
1201 helvetica_dict.set("BaseFont", Object::Name("Helvetica".to_string()));
1202 helvetica_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
1203 font_dict.set("Helvetica", Object::Dictionary(helvetica_dict));
1204
1205 let mut helvetica_bold_dict = Dictionary::new();
1206 helvetica_bold_dict.set("Type", Object::Name("Font".to_string()));
1207 helvetica_bold_dict.set("Subtype", Object::Name("Type1".to_string()));
1208 helvetica_bold_dict.set("BaseFont", Object::Name("Helvetica-Bold".to_string()));
1209 helvetica_bold_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
1210 font_dict.set("Helvetica-Bold", Object::Dictionary(helvetica_bold_dict));
1211
1212 let mut helvetica_oblique_dict = Dictionary::new();
1213 helvetica_oblique_dict.set("Type", Object::Name("Font".to_string()));
1214 helvetica_oblique_dict.set("Subtype", Object::Name("Type1".to_string()));
1215 helvetica_oblique_dict.set("BaseFont", Object::Name("Helvetica-Oblique".to_string()));
1216 helvetica_oblique_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
1217 font_dict.set(
1218 "Helvetica-Oblique",
1219 Object::Dictionary(helvetica_oblique_dict),
1220 );
1221
1222 let mut helvetica_bold_oblique_dict = Dictionary::new();
1223 helvetica_bold_oblique_dict.set("Type", Object::Name("Font".to_string()));
1224 helvetica_bold_oblique_dict.set("Subtype", Object::Name("Type1".to_string()));
1225 helvetica_bold_oblique_dict.set(
1226 "BaseFont",
1227 Object::Name("Helvetica-BoldOblique".to_string()),
1228 );
1229 helvetica_bold_oblique_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
1230 font_dict.set(
1231 "Helvetica-BoldOblique",
1232 Object::Dictionary(helvetica_bold_oblique_dict),
1233 );
1234
1235 // Times family
1236 let mut times_dict = Dictionary::new();
1237 times_dict.set("Type", Object::Name("Font".to_string()));
1238 times_dict.set("Subtype", Object::Name("Type1".to_string()));
1239 times_dict.set("BaseFont", Object::Name("Times-Roman".to_string()));
1240 times_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
1241 font_dict.set("Times-Roman", Object::Dictionary(times_dict));
1242
1243 let mut times_bold_dict = Dictionary::new();
1244 times_bold_dict.set("Type", Object::Name("Font".to_string()));
1245 times_bold_dict.set("Subtype", Object::Name("Type1".to_string()));
1246 times_bold_dict.set("BaseFont", Object::Name("Times-Bold".to_string()));
1247 times_bold_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
1248 font_dict.set("Times-Bold", Object::Dictionary(times_bold_dict));
1249
1250 let mut times_italic_dict = Dictionary::new();
1251 times_italic_dict.set("Type", Object::Name("Font".to_string()));
1252 times_italic_dict.set("Subtype", Object::Name("Type1".to_string()));
1253 times_italic_dict.set("BaseFont", Object::Name("Times-Italic".to_string()));
1254 times_italic_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
1255 font_dict.set("Times-Italic", Object::Dictionary(times_italic_dict));
1256
1257 let mut times_bold_italic_dict = Dictionary::new();
1258 times_bold_italic_dict.set("Type", Object::Name("Font".to_string()));
1259 times_bold_italic_dict.set("Subtype", Object::Name("Type1".to_string()));
1260 times_bold_italic_dict.set("BaseFont", Object::Name("Times-BoldItalic".to_string()));
1261 times_bold_italic_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
1262 font_dict.set(
1263 "Times-BoldItalic",
1264 Object::Dictionary(times_bold_italic_dict),
1265 );
1266
1267 // Courier family
1268 let mut courier_dict = Dictionary::new();
1269 courier_dict.set("Type", Object::Name("Font".to_string()));
1270 courier_dict.set("Subtype", Object::Name("Type1".to_string()));
1271 courier_dict.set("BaseFont", Object::Name("Courier".to_string()));
1272 courier_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
1273 font_dict.set("Courier", Object::Dictionary(courier_dict));
1274
1275 let mut courier_bold_dict = Dictionary::new();
1276 courier_bold_dict.set("Type", Object::Name("Font".to_string()));
1277 courier_bold_dict.set("Subtype", Object::Name("Type1".to_string()));
1278 courier_bold_dict.set("BaseFont", Object::Name("Courier-Bold".to_string()));
1279 courier_bold_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
1280 font_dict.set("Courier-Bold", Object::Dictionary(courier_bold_dict));
1281
1282 let mut courier_oblique_dict = Dictionary::new();
1283 courier_oblique_dict.set("Type", Object::Name("Font".to_string()));
1284 courier_oblique_dict.set("Subtype", Object::Name("Type1".to_string()));
1285 courier_oblique_dict.set("BaseFont", Object::Name("Courier-Oblique".to_string()));
1286 courier_oblique_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
1287 font_dict.set("Courier-Oblique", Object::Dictionary(courier_oblique_dict));
1288
1289 let mut courier_bold_oblique_dict = Dictionary::new();
1290 courier_bold_oblique_dict.set("Type", Object::Name("Font".to_string()));
1291 courier_bold_oblique_dict.set("Subtype", Object::Name("Type1".to_string()));
1292 courier_bold_oblique_dict.set("BaseFont", Object::Name("Courier-BoldOblique".to_string()));
1293 courier_bold_oblique_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
1294 font_dict.set(
1295 "Courier-BoldOblique",
1296 Object::Dictionary(courier_bold_oblique_dict),
1297 );
1298
1299 // Add custom fonts (Type0 fonts for Unicode support)
1300 for (font_name, font_id) in font_refs {
1301 font_dict.set(font_name, Object::Reference(*font_id));
1302 }
1303
1304 resources.set("Font", Object::Dictionary(font_dict));
1305
1306 // Add images as XObjects
1307 if !page.images().is_empty() {
1308 let mut xobject_dict = Dictionary::new();
1309
1310 for (name, image) in page.images() {
1311 // Use sequential ObjectId allocation to avoid conflicts
1312 let image_id = self.allocate_object_id();
1313
1314 // Write the image XObject
1315 self.write_object(image_id, image.to_pdf_object())?;
1316
1317 // Add reference to XObject dictionary
1318 xobject_dict.set(name, Object::Reference(image_id));
1319 }
1320
1321 resources.set("XObject", Object::Dictionary(xobject_dict));
1322 }
1323
1324 // Add ExtGState resources for transparency
1325 if let Some(extgstate_states) = page.get_extgstate_resources() {
1326 let mut extgstate_dict = Dictionary::new();
1327 for (name, state) in extgstate_states {
1328 let mut state_dict = Dictionary::new();
1329 state_dict.set("Type", Object::Name("ExtGState".to_string()));
1330
1331 // Add transparency parameters
1332 if let Some(alpha_stroke) = state.alpha_stroke {
1333 state_dict.set("CA", Object::Real(alpha_stroke));
1334 }
1335 if let Some(alpha_fill) = state.alpha_fill {
1336 state_dict.set("ca", Object::Real(alpha_fill));
1337 }
1338
1339 // Add other parameters as needed
1340 if let Some(line_width) = state.line_width {
1341 state_dict.set("LW", Object::Real(line_width));
1342 }
1343 if let Some(line_cap) = state.line_cap {
1344 state_dict.set("LC", Object::Integer(line_cap as i64));
1345 }
1346 if let Some(line_join) = state.line_join {
1347 state_dict.set("LJ", Object::Integer(line_join as i64));
1348 }
1349 if let Some(dash_pattern) = &state.dash_pattern {
1350 let dash_objects: Vec<Object> = dash_pattern
1351 .array
1352 .iter()
1353 .map(|&d| Object::Real(d))
1354 .collect();
1355 state_dict.set(
1356 "D",
1357 Object::Array(vec![
1358 Object::Array(dash_objects),
1359 Object::Real(dash_pattern.phase),
1360 ]),
1361 );
1362 }
1363
1364 extgstate_dict.set(name, Object::Dictionary(state_dict));
1365 }
1366 if !extgstate_dict.is_empty() {
1367 resources.set("ExtGState", Object::Dictionary(extgstate_dict));
1368 }
1369 }
1370
1371 page_dict.set("Resources", Object::Dictionary(resources));
1372
1373 // Handle form widget annotations
1374 if let Some(Object::Array(annots)) = page_dict.get("Annots") {
1375 let mut new_annots = Vec::new();
1376
1377 for annot in annots {
1378 if let Object::Dictionary(ref annot_dict) = annot {
1379 if let Some(Object::Name(subtype)) = annot_dict.get("Subtype") {
1380 if subtype == "Widget" {
1381 // Process widget annotation
1382 let widget_id = self.allocate_object_id();
1383 self.write_object(widget_id, annot.clone())?;
1384 new_annots.push(Object::Reference(widget_id));
1385
1386 // Track widget for form fields
1387 if let Some(Object::Name(_ft)) = annot_dict.get("FT") {
1388 if let Some(Object::String(field_name)) = annot_dict.get("T") {
1389 self.field_widget_map
1390 .entry(field_name.clone())
1391 .or_default()
1392 .push(widget_id);
1393 self.field_id_map.insert(field_name.clone(), widget_id);
1394 self.form_field_ids.push(widget_id);
1395 }
1396 }
1397 continue;
1398 }
1399 }
1400 }
1401 new_annots.push(annot.clone());
1402 }
1403
1404 if !new_annots.is_empty() {
1405 page_dict.set("Annots", Object::Array(new_annots));
1406 }
1407 }
1408
1409 self.write_object(page_id, Object::Dictionary(page_dict))?;
1410 Ok(())
1411 }
1412}
1413
1414impl PdfWriter<BufWriter<std::fs::File>> {
1415 pub fn new(path: impl AsRef<Path>) -> Result<Self> {
1416 let file = std::fs::File::create(path)?;
1417 let writer = BufWriter::new(file);
1418
1419 Ok(Self {
1420 writer,
1421 xref_positions: HashMap::new(),
1422 current_position: 0,
1423 next_object_id: 1,
1424 catalog_id: None,
1425 pages_id: None,
1426 info_id: None,
1427 field_widget_map: HashMap::new(),
1428 field_id_map: HashMap::new(),
1429 form_field_ids: Vec::new(),
1430 page_ids: Vec::new(),
1431 config: WriterConfig::default(),
1432 document_used_chars: None,
1433 })
1434 }
1435}
1436
1437impl<W: Write> PdfWriter<W> {
1438 fn allocate_object_id(&mut self) -> ObjectId {
1439 let id = ObjectId::new(self.next_object_id, 0);
1440 self.next_object_id += 1;
1441 id
1442 }
1443
1444 fn write_object(&mut self, id: ObjectId, object: Object) -> Result<()> {
1445 self.xref_positions.insert(id, self.current_position);
1446
1447 let header = format!("{} {} obj\n", id.number(), id.generation());
1448 self.write_bytes(header.as_bytes())?;
1449
1450 self.write_object_value(&object)?;
1451
1452 self.write_bytes(b"\nendobj\n")?;
1453 Ok(())
1454 }
1455
1456 fn write_object_value(&mut self, object: &Object) -> Result<()> {
1457 match object {
1458 Object::Null => self.write_bytes(b"null")?,
1459 Object::Boolean(b) => self.write_bytes(if *b { b"true" } else { b"false" })?,
1460 Object::Integer(i) => self.write_bytes(i.to_string().as_bytes())?,
1461 Object::Real(f) => self.write_bytes(
1462 format!("{f:.6}")
1463 .trim_end_matches('0')
1464 .trim_end_matches('.')
1465 .as_bytes(),
1466 )?,
1467 Object::String(s) => {
1468 self.write_bytes(b"(")?;
1469 self.write_bytes(s.as_bytes())?;
1470 self.write_bytes(b")")?;
1471 }
1472 Object::Name(n) => {
1473 self.write_bytes(b"/")?;
1474 self.write_bytes(n.as_bytes())?;
1475 }
1476 Object::Array(arr) => {
1477 self.write_bytes(b"[")?;
1478 for (i, obj) in arr.iter().enumerate() {
1479 if i > 0 {
1480 self.write_bytes(b" ")?;
1481 }
1482 self.write_object_value(obj)?;
1483 }
1484 self.write_bytes(b"]")?;
1485 }
1486 Object::Dictionary(dict) => {
1487 self.write_bytes(b"<<")?;
1488 for (key, value) in dict.entries() {
1489 self.write_bytes(b"\n/")?;
1490 self.write_bytes(key.as_bytes())?;
1491 self.write_bytes(b" ")?;
1492 self.write_object_value(value)?;
1493 }
1494 self.write_bytes(b"\n>>")?;
1495 }
1496 Object::Stream(dict, data) => {
1497 self.write_object_value(&Object::Dictionary(dict.clone()))?;
1498 self.write_bytes(b"\nstream\n")?;
1499 self.write_bytes(data)?;
1500 self.write_bytes(b"\nendstream")?;
1501 }
1502 Object::Reference(id) => {
1503 let ref_str = format!("{} {} R", id.number(), id.generation());
1504 self.write_bytes(ref_str.as_bytes())?;
1505 }
1506 }
1507 Ok(())
1508 }
1509
1510 fn write_xref(&mut self) -> Result<()> {
1511 self.write_bytes(b"xref\n")?;
1512
1513 // Sort by object number and write entries
1514 let mut entries: Vec<_> = self
1515 .xref_positions
1516 .iter()
1517 .map(|(id, pos)| (*id, *pos))
1518 .collect();
1519 entries.sort_by_key(|(id, _)| id.number());
1520
1521 // Find the highest object number to determine size
1522 let max_obj_num = entries.iter().map(|(id, _)| id.number()).max().unwrap_or(0);
1523
1524 // Write subsection header - PDF 1.7 spec allows multiple subsections
1525 // For simplicity, write one subsection from 0 to max
1526 self.write_bytes(b"0 ")?;
1527 self.write_bytes((max_obj_num + 1).to_string().as_bytes())?;
1528 self.write_bytes(b"\n")?;
1529
1530 // Write free object entry
1531 self.write_bytes(b"0000000000 65535 f \n")?;
1532
1533 // Write entries for all object numbers from 1 to max
1534 // Fill in gaps with free entries
1535 for obj_num in 1..=max_obj_num {
1536 let _obj_id = ObjectId::new(obj_num, 0);
1537 if let Some((_, position)) = entries.iter().find(|(id, _)| id.number() == obj_num) {
1538 let entry = format!("{:010} {:05} n \n", position, 0);
1539 self.write_bytes(entry.as_bytes())?;
1540 } else {
1541 // Free entry for gap
1542 self.write_bytes(b"0000000000 00000 f \n")?;
1543 }
1544 }
1545
1546 Ok(())
1547 }
1548
1549 fn write_xref_stream(&mut self) -> Result<()> {
1550 let catalog_id = self.catalog_id.expect("catalog_id must be set");
1551 let info_id = self.info_id.expect("info_id must be set");
1552
1553 // Allocate object ID for the xref stream
1554 let xref_stream_id = self.allocate_object_id();
1555 let xref_position = self.current_position;
1556
1557 // Create XRef stream writer with trailer information
1558 let mut xref_writer = XRefStreamWriter::new(xref_stream_id);
1559 xref_writer.set_trailer_info(catalog_id, info_id);
1560
1561 // Add free entry for object 0
1562 xref_writer.add_free_entry(0, 65535);
1563
1564 // Sort entries by object number
1565 let mut entries: Vec<_> = self
1566 .xref_positions
1567 .iter()
1568 .map(|(id, pos)| (*id, *pos))
1569 .collect();
1570 entries.sort_by_key(|(id, _)| id.number());
1571
1572 // Find the highest object number (including the xref stream itself)
1573 let max_obj_num = entries
1574 .iter()
1575 .map(|(id, _)| id.number())
1576 .max()
1577 .unwrap_or(0)
1578 .max(xref_stream_id.number());
1579
1580 // Add entries for all objects
1581 for obj_num in 1..=max_obj_num {
1582 if obj_num == xref_stream_id.number() {
1583 // The xref stream entry will be added with the correct position
1584 xref_writer.add_in_use_entry(xref_position, 0);
1585 } else if let Some((id, position)) =
1586 entries.iter().find(|(id, _)| id.number() == obj_num)
1587 {
1588 xref_writer.add_in_use_entry(*position, id.generation());
1589 } else {
1590 // Free entry for gap
1591 xref_writer.add_free_entry(0, 0);
1592 }
1593 }
1594
1595 // Mark position for xref stream object
1596 self.xref_positions.insert(xref_stream_id, xref_position);
1597
1598 // Write object header
1599 self.write_bytes(
1600 format!(
1601 "{} {} obj\n",
1602 xref_stream_id.number(),
1603 xref_stream_id.generation()
1604 )
1605 .as_bytes(),
1606 )?;
1607
1608 // Get the encoded data
1609 let uncompressed_data = xref_writer.encode_entries();
1610 let final_data = if self.config.compress_streams {
1611 crate::compression::compress(&uncompressed_data)?
1612 } else {
1613 uncompressed_data
1614 };
1615
1616 // Create and write dictionary
1617 let mut dict = xref_writer.create_dictionary(None);
1618 dict.set("Length", Object::Integer(final_data.len() as i64));
1619
1620 // Add filter if compression is enabled
1621 if self.config.compress_streams {
1622 dict.set("Filter", Object::Name("FlateDecode".to_string()));
1623 }
1624 self.write_bytes(b"<<")?;
1625 for (key, value) in dict.iter() {
1626 self.write_bytes(b"\n/")?;
1627 self.write_bytes(key.as_bytes())?;
1628 self.write_bytes(b" ")?;
1629 self.write_object_value(value)?;
1630 }
1631 self.write_bytes(b"\n>>\n")?;
1632
1633 // Write stream
1634 self.write_bytes(b"stream\n")?;
1635 self.write_bytes(&final_data)?;
1636 self.write_bytes(b"\nendstream\n")?;
1637 self.write_bytes(b"endobj\n")?;
1638
1639 // Write startxref and EOF
1640 self.write_bytes(b"\nstartxref\n")?;
1641 self.write_bytes(xref_position.to_string().as_bytes())?;
1642 self.write_bytes(b"\n%%EOF\n")?;
1643
1644 Ok(())
1645 }
1646
1647 fn write_trailer(&mut self, xref_position: u64) -> Result<()> {
1648 let catalog_id = self.catalog_id.expect("catalog_id must be set");
1649 let info_id = self.info_id.expect("info_id must be set");
1650 // Find the highest object number to determine size
1651 let max_obj_num = self
1652 .xref_positions
1653 .keys()
1654 .map(|id| id.number())
1655 .max()
1656 .unwrap_or(0);
1657
1658 let mut trailer = Dictionary::new();
1659 trailer.set("Size", Object::Integer((max_obj_num + 1) as i64));
1660 trailer.set("Root", Object::Reference(catalog_id));
1661 trailer.set("Info", Object::Reference(info_id));
1662
1663 self.write_bytes(b"trailer\n")?;
1664 self.write_object_value(&Object::Dictionary(trailer))?;
1665 self.write_bytes(b"\nstartxref\n")?;
1666 self.write_bytes(xref_position.to_string().as_bytes())?;
1667 self.write_bytes(b"\n%%EOF\n")?;
1668
1669 Ok(())
1670 }
1671
1672 fn write_bytes(&mut self, data: &[u8]) -> Result<()> {
1673 self.writer.write_all(data)?;
1674 self.current_position += data.len() as u64;
1675 Ok(())
1676 }
1677
1678 #[allow(dead_code)]
1679 fn create_widget_appearance_stream(&mut self, widget_dict: &Dictionary) -> Result<ObjectId> {
1680 // Get widget rectangle
1681 let rect = if let Some(Object::Array(rect_array)) = widget_dict.get("Rect") {
1682 if rect_array.len() >= 4 {
1683 if let (
1684 Some(Object::Real(x1)),
1685 Some(Object::Real(y1)),
1686 Some(Object::Real(x2)),
1687 Some(Object::Real(y2)),
1688 ) = (
1689 rect_array.first(),
1690 rect_array.get(1),
1691 rect_array.get(2),
1692 rect_array.get(3),
1693 ) {
1694 (*x1, *y1, *x2, *y2)
1695 } else {
1696 (0.0, 0.0, 100.0, 20.0) // Default
1697 }
1698 } else {
1699 (0.0, 0.0, 100.0, 20.0) // Default
1700 }
1701 } else {
1702 (0.0, 0.0, 100.0, 20.0) // Default
1703 };
1704
1705 let width = rect.2 - rect.0;
1706 let height = rect.3 - rect.1;
1707
1708 // Create appearance stream content
1709 let mut content = String::new();
1710
1711 // Set graphics state
1712 content.push_str("q\n");
1713
1714 // Draw border (black)
1715 content.push_str("0 0 0 RG\n"); // Black stroke color
1716 content.push_str("1 w\n"); // 1pt line width
1717
1718 // Draw rectangle border
1719 content.push_str(&format!("0 0 {width} {height} re\n"));
1720 content.push_str("S\n"); // Stroke
1721
1722 // Fill with white background
1723 content.push_str("1 1 1 rg\n"); // White fill color
1724 content.push_str(&format!("0.5 0.5 {} {} re\n", width - 1.0, height - 1.0));
1725 content.push_str("f\n"); // Fill
1726
1727 // Restore graphics state
1728 content.push_str("Q\n");
1729
1730 // Create stream dictionary
1731 let mut stream_dict = Dictionary::new();
1732 stream_dict.set("Type", Object::Name("XObject".to_string()));
1733 stream_dict.set("Subtype", Object::Name("Form".to_string()));
1734 stream_dict.set(
1735 "BBox",
1736 Object::Array(vec![
1737 Object::Real(0.0),
1738 Object::Real(0.0),
1739 Object::Real(width),
1740 Object::Real(height),
1741 ]),
1742 );
1743 stream_dict.set("Resources", Object::Dictionary(Dictionary::new()));
1744 stream_dict.set("Length", Object::Integer(content.len() as i64));
1745
1746 // Write the appearance stream
1747 let stream_id = self.allocate_object_id();
1748 self.write_object(stream_id, Object::Stream(stream_dict, content.into_bytes()))?;
1749
1750 Ok(stream_id)
1751 }
1752
1753 #[allow(dead_code)]
1754 fn create_field_appearance_stream(
1755 &mut self,
1756 field_dict: &Dictionary,
1757 widget: &crate::forms::Widget,
1758 ) -> Result<ObjectId> {
1759 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
1760 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
1761
1762 // Create appearance stream content
1763 let mut content = String::new();
1764
1765 // Set graphics state
1766 content.push_str("q\n");
1767
1768 // Draw background if specified
1769 if let Some(bg_color) = &widget.appearance.background_color {
1770 match bg_color {
1771 crate::graphics::Color::Gray(g) => {
1772 content.push_str(&format!("{g} g\n"));
1773 }
1774 crate::graphics::Color::Rgb(r, g, b) => {
1775 content.push_str(&format!("{r} {g} {b} rg\n"));
1776 }
1777 crate::graphics::Color::Cmyk(c, m, y, k) => {
1778 content.push_str(&format!("{c} {m} {y} {k} k\n"));
1779 }
1780 }
1781 content.push_str(&format!("0 0 {width} {height} re\n"));
1782 content.push_str("f\n");
1783 }
1784
1785 // Draw border
1786 if let Some(border_color) = &widget.appearance.border_color {
1787 match border_color {
1788 crate::graphics::Color::Gray(g) => {
1789 content.push_str(&format!("{g} G\n"));
1790 }
1791 crate::graphics::Color::Rgb(r, g, b) => {
1792 content.push_str(&format!("{r} {g} {b} RG\n"));
1793 }
1794 crate::graphics::Color::Cmyk(c, m, y, k) => {
1795 content.push_str(&format!("{c} {m} {y} {k} K\n"));
1796 }
1797 }
1798 content.push_str(&format!("{} w\n", widget.appearance.border_width));
1799 content.push_str(&format!("0 0 {width} {height} re\n"));
1800 content.push_str("S\n");
1801 }
1802
1803 // For checkboxes, add a checkmark if checked
1804 if let Some(Object::Name(ft)) = field_dict.get("FT") {
1805 if ft == "Btn" {
1806 if let Some(Object::Name(v)) = field_dict.get("V") {
1807 if v == "Yes" {
1808 // Draw checkmark
1809 content.push_str("0 0 0 RG\n"); // Black
1810 content.push_str("2 w\n");
1811 let margin = width * 0.2;
1812 content.push_str(&format!("{} {} m\n", margin, height / 2.0));
1813 content.push_str(&format!("{} {} l\n", width / 2.0, margin));
1814 content.push_str(&format!("{} {} l\n", width - margin, height - margin));
1815 content.push_str("S\n");
1816 }
1817 }
1818 }
1819 }
1820
1821 // Restore graphics state
1822 content.push_str("Q\n");
1823
1824 // Create stream dictionary
1825 let mut stream_dict = Dictionary::new();
1826 stream_dict.set("Type", Object::Name("XObject".to_string()));
1827 stream_dict.set("Subtype", Object::Name("Form".to_string()));
1828 stream_dict.set(
1829 "BBox",
1830 Object::Array(vec![
1831 Object::Real(0.0),
1832 Object::Real(0.0),
1833 Object::Real(width),
1834 Object::Real(height),
1835 ]),
1836 );
1837 stream_dict.set("Resources", Object::Dictionary(Dictionary::new()));
1838 stream_dict.set("Length", Object::Integer(content.len() as i64));
1839
1840 // Write the appearance stream
1841 let stream_id = self.allocate_object_id();
1842 self.write_object(stream_id, Object::Stream(stream_dict, content.into_bytes()))?;
1843
1844 Ok(stream_id)
1845 }
1846}
1847
1848/// Format a DateTime as a PDF date string (D:YYYYMMDDHHmmSSOHH'mm)
1849fn format_pdf_date(date: DateTime<Utc>) -> String {
1850 // Format the UTC date according to PDF specification
1851 // D:YYYYMMDDHHmmSSOHH'mm where O is the relationship of local time to UTC (+ or -)
1852 let formatted = date.format("D:%Y%m%d%H%M%S");
1853
1854 // For UTC, the offset is always +00'00
1855 format!("{formatted}+00'00")
1856}
1857
1858#[cfg(test)]
1859mod tests {
1860 use super::*;
1861 use crate::objects::{Object, ObjectId};
1862 use crate::page::Page;
1863
1864 #[test]
1865 fn test_pdf_writer_new_with_writer() {
1866 let buffer = Vec::new();
1867 let writer = PdfWriter::new_with_writer(buffer);
1868 assert_eq!(writer.current_position, 0);
1869 assert!(writer.xref_positions.is_empty());
1870 }
1871
1872 #[test]
1873 fn test_write_header() {
1874 let mut buffer = Vec::new();
1875 let mut writer = PdfWriter::new_with_writer(&mut buffer);
1876
1877 writer.write_header().unwrap();
1878
1879 // Check PDF version
1880 assert!(buffer.starts_with(b"%PDF-1.7\n"));
1881 // Check binary comment
1882 assert_eq!(buffer.len(), 15); // 9 bytes for header + 6 bytes for binary comment
1883 assert_eq!(buffer[9], b'%');
1884 assert_eq!(buffer[10], 0xE2);
1885 assert_eq!(buffer[11], 0xE3);
1886 assert_eq!(buffer[12], 0xCF);
1887 assert_eq!(buffer[13], 0xD3);
1888 assert_eq!(buffer[14], b'\n');
1889 }
1890
1891 #[test]
1892 fn test_write_catalog() {
1893 let mut buffer = Vec::new();
1894 let mut writer = PdfWriter::new_with_writer(&mut buffer);
1895
1896 let mut document = Document::new();
1897 // Set required IDs before calling write_catalog
1898 writer.catalog_id = Some(writer.allocate_object_id());
1899 writer.pages_id = Some(writer.allocate_object_id());
1900 writer.info_id = Some(writer.allocate_object_id());
1901 writer.write_catalog(&mut document).unwrap();
1902
1903 let catalog_id = writer.catalog_id.unwrap();
1904 assert_eq!(catalog_id.number(), 1);
1905 assert_eq!(catalog_id.generation(), 0);
1906 assert!(!buffer.is_empty());
1907
1908 let content = String::from_utf8_lossy(&buffer);
1909 assert!(content.contains("1 0 obj"));
1910 assert!(content.contains("/Type /Catalog"));
1911 assert!(content.contains("/Pages 2 0 R"));
1912 assert!(content.contains("endobj"));
1913 }
1914
1915 #[test]
1916 fn test_write_empty_document() {
1917 let mut buffer = Vec::new();
1918 let mut document = Document::new();
1919
1920 {
1921 let mut writer = PdfWriter::new_with_writer(&mut buffer);
1922 writer.write_document(&mut document).unwrap();
1923 }
1924
1925 // Verify PDF structure
1926 let content = String::from_utf8_lossy(&buffer);
1927 assert!(content.starts_with("%PDF-1.7\n"));
1928 assert!(content.contains("trailer"));
1929 assert!(content.contains("%%EOF"));
1930 }
1931
1932 #[test]
1933 fn test_write_document_with_pages() {
1934 let mut buffer = Vec::new();
1935 let mut document = Document::new();
1936 document.add_page(Page::a4());
1937 document.add_page(Page::letter());
1938
1939 {
1940 let mut writer = PdfWriter::new_with_writer(&mut buffer);
1941 writer.write_document(&mut document).unwrap();
1942 }
1943
1944 let content = String::from_utf8_lossy(&buffer);
1945 assert!(content.contains("/Type /Pages"));
1946 assert!(content.contains("/Count 2"));
1947 assert!(content.contains("/MediaBox"));
1948 }
1949
1950 #[test]
1951 fn test_write_info() {
1952 let mut buffer = Vec::new();
1953 let mut document = Document::new();
1954 document.set_title("Test Title");
1955 document.set_author("Test Author");
1956 document.set_subject("Test Subject");
1957 document.set_keywords("test, keywords");
1958
1959 {
1960 let mut writer = PdfWriter::new_with_writer(&mut buffer);
1961 // Set required info_id before calling write_info
1962 writer.info_id = Some(writer.allocate_object_id());
1963 writer.write_info(&document).unwrap();
1964 let info_id = writer.info_id.unwrap();
1965 assert!(info_id.number() > 0);
1966 }
1967
1968 let content = String::from_utf8_lossy(&buffer);
1969 assert!(content.contains("/Title (Test Title)"));
1970 assert!(content.contains("/Author (Test Author)"));
1971 assert!(content.contains("/Subject (Test Subject)"));
1972 assert!(content.contains("/Keywords (test, keywords)"));
1973 assert!(content.contains("/Producer (oxidize_pdf v"));
1974 assert!(content.contains("/Creator (oxidize_pdf)"));
1975 assert!(content.contains("/CreationDate"));
1976 assert!(content.contains("/ModDate"));
1977 }
1978
1979 #[test]
1980 fn test_write_info_with_dates() {
1981 use chrono::{TimeZone, Utc};
1982
1983 let mut buffer = Vec::new();
1984 let mut document = Document::new();
1985
1986 // Set specific dates
1987 let creation_date = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
1988 let mod_date = Utc.with_ymd_and_hms(2023, 6, 15, 18, 30, 0).unwrap();
1989
1990 document.set_creation_date(creation_date);
1991 document.set_modification_date(mod_date);
1992 document.set_creator("Test Creator");
1993 document.set_producer("Test Producer");
1994
1995 {
1996 let mut writer = PdfWriter::new_with_writer(&mut buffer);
1997 // Set required info_id before calling write_info
1998 writer.info_id = Some(writer.allocate_object_id());
1999 writer.write_info(&document).unwrap();
2000 }
2001
2002 let content = String::from_utf8_lossy(&buffer);
2003 assert!(content.contains("/CreationDate (D:20230101"));
2004 assert!(content.contains("/ModDate (D:20230615"));
2005 assert!(content.contains("/Creator (Test Creator)"));
2006 assert!(content.contains("/Producer (Test Producer)"));
2007 }
2008
2009 #[test]
2010 fn test_format_pdf_date() {
2011 use chrono::{TimeZone, Utc};
2012
2013 let date = Utc.with_ymd_and_hms(2023, 12, 25, 15, 30, 45).unwrap();
2014 let formatted = format_pdf_date(date);
2015
2016 // Should start with D: and contain date/time components
2017 assert!(formatted.starts_with("D:"));
2018 assert!(formatted.contains("20231225"));
2019 assert!(formatted.contains("153045"));
2020
2021 // Should contain timezone offset
2022 assert!(formatted.contains("+") || formatted.contains("-"));
2023 }
2024
2025 #[test]
2026 fn test_write_object() {
2027 let mut buffer = Vec::new();
2028 let obj_id = ObjectId::new(5, 0);
2029 let obj = Object::String("Hello PDF".to_string());
2030
2031 {
2032 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2033 writer.write_object(obj_id, obj).unwrap();
2034 assert!(writer.xref_positions.contains_key(&obj_id));
2035 }
2036
2037 let content = String::from_utf8_lossy(&buffer);
2038 assert!(content.contains("5 0 obj"));
2039 assert!(content.contains("(Hello PDF)"));
2040 assert!(content.contains("endobj"));
2041 }
2042
2043 #[test]
2044 fn test_write_xref() {
2045 let mut buffer = Vec::new();
2046 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2047
2048 // Add some objects to xref
2049 writer.xref_positions.insert(ObjectId::new(1, 0), 15);
2050 writer.xref_positions.insert(ObjectId::new(2, 0), 94);
2051 writer.xref_positions.insert(ObjectId::new(3, 0), 152);
2052
2053 writer.write_xref().unwrap();
2054
2055 let content = String::from_utf8_lossy(&buffer);
2056 assert!(content.contains("xref"));
2057 assert!(content.contains("0 4")); // 0 to 3
2058 assert!(content.contains("0000000000 65535 f "));
2059 assert!(content.contains("0000000015 00000 n "));
2060 assert!(content.contains("0000000094 00000 n "));
2061 assert!(content.contains("0000000152 00000 n "));
2062 }
2063
2064 #[test]
2065 fn test_write_trailer() {
2066 let mut buffer = Vec::new();
2067 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2068
2069 writer.xref_positions.insert(ObjectId::new(1, 0), 15);
2070 writer.xref_positions.insert(ObjectId::new(2, 0), 94);
2071
2072 let catalog_id = ObjectId::new(1, 0);
2073 let info_id = ObjectId::new(2, 0);
2074
2075 writer.catalog_id = Some(catalog_id);
2076 writer.info_id = Some(info_id);
2077 writer.write_trailer(1234).unwrap();
2078
2079 let content = String::from_utf8_lossy(&buffer);
2080 assert!(content.contains("trailer"));
2081 assert!(content.contains("/Size 3"));
2082 assert!(content.contains("/Root 1 0 R"));
2083 assert!(content.contains("/Info 2 0 R"));
2084 assert!(content.contains("startxref"));
2085 assert!(content.contains("1234"));
2086 assert!(content.contains("%%EOF"));
2087 }
2088
2089 #[test]
2090 fn test_write_bytes() {
2091 let mut buffer = Vec::new();
2092
2093 {
2094 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2095
2096 assert_eq!(writer.current_position, 0);
2097
2098 writer.write_bytes(b"Hello").unwrap();
2099 assert_eq!(writer.current_position, 5);
2100
2101 writer.write_bytes(b" World").unwrap();
2102 assert_eq!(writer.current_position, 11);
2103 }
2104
2105 assert_eq!(buffer, b"Hello World");
2106 }
2107
2108 #[test]
2109 fn test_complete_pdf_generation() {
2110 let mut buffer = Vec::new();
2111 let mut document = Document::new();
2112 document.set_title("Complete Test");
2113 document.add_page(Page::a4());
2114
2115 {
2116 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2117 writer.write_document(&mut document).unwrap();
2118 }
2119
2120 // Verify complete PDF structure
2121 assert!(buffer.starts_with(b"%PDF-1.7\n"));
2122 assert!(buffer.ends_with(b"%%EOF\n"));
2123
2124 let content = String::from_utf8_lossy(&buffer);
2125 assert!(content.contains("obj"));
2126 assert!(content.contains("endobj"));
2127 assert!(content.contains("xref"));
2128 assert!(content.contains("trailer"));
2129 assert!(content.contains("/Type /Catalog"));
2130 assert!(content.contains("/Type /Pages"));
2131 assert!(content.contains("/Type /Page"));
2132 }
2133
2134 // Integration tests for Writer ↔ Document ↔ Page interactions
2135 mod integration_tests {
2136 use super::*;
2137 use crate::graphics::Color;
2138 use crate::graphics::Image;
2139 use crate::text::Font;
2140 use std::fs;
2141 use tempfile::TempDir;
2142
2143 #[test]
2144 fn test_writer_document_integration() {
2145 let temp_dir = TempDir::new().unwrap();
2146 let file_path = temp_dir.path().join("writer_document_integration.pdf");
2147
2148 let mut document = Document::new();
2149 document.set_title("Writer Document Integration Test");
2150 document.set_author("Integration Test Suite");
2151 document.set_subject("Testing writer-document integration");
2152 document.set_keywords("writer, document, integration, test");
2153
2154 // Add multiple pages with different content
2155 let mut page1 = Page::a4();
2156 page1
2157 .text()
2158 .set_font(Font::Helvetica, 16.0)
2159 .at(100.0, 750.0)
2160 .write("Page 1 Content")
2161 .unwrap();
2162
2163 let mut page2 = Page::letter();
2164 page2
2165 .text()
2166 .set_font(Font::TimesRoman, 14.0)
2167 .at(100.0, 750.0)
2168 .write("Page 2 Content")
2169 .unwrap();
2170
2171 document.add_page(page1);
2172 document.add_page(page2);
2173
2174 // Write document
2175 let mut writer = PdfWriter::new(&file_path).unwrap();
2176 writer.write_document(&mut document).unwrap();
2177
2178 // Verify file creation and structure
2179 assert!(file_path.exists());
2180 let metadata = fs::metadata(&file_path).unwrap();
2181 assert!(metadata.len() > 1000);
2182
2183 // Verify PDF structure
2184 let content = fs::read(&file_path).unwrap();
2185 let content_str = String::from_utf8_lossy(&content);
2186 assert!(content_str.contains("/Type /Catalog"));
2187 assert!(content_str.contains("/Type /Pages"));
2188 assert!(content_str.contains("/Count 2"));
2189 assert!(content_str.contains("/Title (Writer Document Integration Test)"));
2190 assert!(content_str.contains("/Author (Integration Test Suite)"));
2191 }
2192
2193 #[test]
2194 fn test_writer_page_content_integration() {
2195 let temp_dir = TempDir::new().unwrap();
2196 let file_path = temp_dir.path().join("writer_page_content.pdf");
2197
2198 let mut document = Document::new();
2199 document.set_title("Writer Page Content Test");
2200
2201 let mut page = Page::a4();
2202 page.set_margins(50.0, 50.0, 50.0, 50.0);
2203
2204 // Add complex content to page
2205 page.text()
2206 .set_font(Font::HelveticaBold, 18.0)
2207 .at(100.0, 750.0)
2208 .write("Complex Page Content")
2209 .unwrap();
2210
2211 page.graphics()
2212 .set_fill_color(Color::rgb(0.2, 0.4, 0.8))
2213 .rect(100.0, 600.0, 200.0, 100.0)
2214 .fill();
2215
2216 page.graphics()
2217 .set_stroke_color(Color::rgb(0.8, 0.2, 0.2))
2218 .set_line_width(3.0)
2219 .circle(400.0, 650.0, 50.0)
2220 .stroke();
2221
2222 // Add multiple text elements
2223 for i in 0..5 {
2224 let y = 550.0 - (i as f64 * 20.0);
2225 page.text()
2226 .set_font(Font::TimesRoman, 12.0)
2227 .at(100.0, y)
2228 .write(&format!("Text line {line}", line = i + 1))
2229 .unwrap();
2230 }
2231
2232 document.add_page(page);
2233
2234 // Write and verify
2235 let mut writer = PdfWriter::new(&file_path).unwrap();
2236 writer.write_document(&mut document).unwrap();
2237
2238 assert!(file_path.exists());
2239 let metadata = fs::metadata(&file_path).unwrap();
2240 assert!(metadata.len() > 800);
2241
2242 // Verify content streams are present
2243 let content = fs::read(&file_path).unwrap();
2244 let content_str = String::from_utf8_lossy(&content);
2245 assert!(content_str.contains("stream"));
2246 assert!(content_str.contains("endstream"));
2247 assert!(content_str.contains("/Length"));
2248 }
2249
2250 #[test]
2251 fn test_writer_image_integration() {
2252 let temp_dir = TempDir::new().unwrap();
2253 let file_path = temp_dir.path().join("writer_image_integration.pdf");
2254
2255 let mut document = Document::new();
2256 document.set_title("Writer Image Integration Test");
2257
2258 let mut page = Page::a4();
2259
2260 // Create test images
2261 let jpeg_data1 = vec![
2262 0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x64, 0x00, 0xC8, 0x03, 0xFF, 0xD9,
2263 ];
2264 let image1 = Image::from_jpeg_data(jpeg_data1).unwrap();
2265
2266 let jpeg_data2 = vec![
2267 0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x32, 0x00, 0x32, 0x01, 0xFF, 0xD9,
2268 ];
2269 let image2 = Image::from_jpeg_data(jpeg_data2).unwrap();
2270
2271 // Add images to page
2272 page.add_image("test_image1", image1);
2273 page.add_image("test_image2", image2);
2274
2275 // Draw images
2276 page.draw_image("test_image1", 100.0, 600.0, 200.0, 100.0)
2277 .unwrap();
2278 page.draw_image("test_image2", 350.0, 600.0, 100.0, 100.0)
2279 .unwrap();
2280
2281 // Add text labels
2282 page.text()
2283 .set_font(Font::Helvetica, 14.0)
2284 .at(100.0, 750.0)
2285 .write("Image Integration Test")
2286 .unwrap();
2287
2288 document.add_page(page);
2289
2290 // Write and verify
2291 let mut writer = PdfWriter::new(&file_path).unwrap();
2292 writer.write_document(&mut document).unwrap();
2293
2294 assert!(file_path.exists());
2295 let metadata = fs::metadata(&file_path).unwrap();
2296 assert!(metadata.len() > 1000);
2297
2298 // Verify XObject and image resources
2299 let content = fs::read(&file_path).unwrap();
2300 let content_str = String::from_utf8_lossy(&content);
2301
2302 // Debug output
2303 println!("PDF size: {} bytes", content.len());
2304 println!("Contains 'XObject': {}", content_str.contains("XObject"));
2305
2306 // Verify XObject is properly written
2307 assert!(content_str.contains("XObject"));
2308 assert!(content_str.contains("test_image1"));
2309 assert!(content_str.contains("test_image2"));
2310 assert!(content_str.contains("/Type /XObject"));
2311 assert!(content_str.contains("/Subtype /Image"));
2312 }
2313
2314 #[test]
2315 fn test_writer_buffer_vs_file_output() {
2316 let temp_dir = TempDir::new().unwrap();
2317 let file_path = temp_dir.path().join("buffer_vs_file_output.pdf");
2318
2319 let mut document = Document::new();
2320 document.set_title("Buffer vs File Output Test");
2321
2322 let mut page = Page::a4();
2323 page.text()
2324 .set_font(Font::Helvetica, 12.0)
2325 .at(100.0, 700.0)
2326 .write("Testing buffer vs file output")
2327 .unwrap();
2328
2329 document.add_page(page);
2330
2331 // Write to buffer
2332 let mut buffer = Vec::new();
2333 {
2334 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2335 writer.write_document(&mut document).unwrap();
2336 }
2337
2338 // Write to file
2339 {
2340 let mut writer = PdfWriter::new(&file_path).unwrap();
2341 writer.write_document(&mut document).unwrap();
2342 }
2343
2344 // Read file content
2345 let file_content = fs::read(&file_path).unwrap();
2346
2347 // Both should be valid PDFs
2348 assert!(buffer.starts_with(b"%PDF-1.7"));
2349 assert!(file_content.starts_with(b"%PDF-1.7"));
2350 assert!(buffer.ends_with(b"%%EOF\n"));
2351 assert!(file_content.ends_with(b"%%EOF\n"));
2352
2353 // Both should contain the same structural elements
2354 let buffer_str = String::from_utf8_lossy(&buffer);
2355 let file_str = String::from_utf8_lossy(&file_content);
2356
2357 assert!(buffer_str.contains("obj"));
2358 assert!(file_str.contains("obj"));
2359 assert!(buffer_str.contains("xref"));
2360 assert!(file_str.contains("xref"));
2361 assert!(buffer_str.contains("trailer"));
2362 assert!(file_str.contains("trailer"));
2363 }
2364
2365 #[test]
2366 fn test_writer_large_document_performance() {
2367 let temp_dir = TempDir::new().unwrap();
2368 let file_path = temp_dir.path().join("large_document_performance.pdf");
2369
2370 let mut document = Document::new();
2371 document.set_title("Large Document Performance Test");
2372
2373 // Create many pages with content
2374 for i in 0..20 {
2375 let mut page = Page::a4();
2376
2377 // Add title
2378 page.text()
2379 .set_font(Font::HelveticaBold, 16.0)
2380 .at(100.0, 750.0)
2381 .write(&format!("Page {page}", page = i + 1))
2382 .unwrap();
2383
2384 // Add content lines
2385 for j in 0..30 {
2386 let y = 700.0 - (j as f64 * 20.0);
2387 if y > 100.0 {
2388 page.text()
2389 .set_font(Font::TimesRoman, 10.0)
2390 .at(100.0, y)
2391 .write(&format!(
2392 "Line {line} on page {page}",
2393 line = j + 1,
2394 page = i + 1
2395 ))
2396 .unwrap();
2397 }
2398 }
2399
2400 // Add some graphics
2401 page.graphics()
2402 .set_fill_color(Color::rgb(0.8, 0.8, 0.9))
2403 .rect(50.0, 50.0, 100.0, 50.0)
2404 .fill();
2405
2406 document.add_page(page);
2407 }
2408
2409 // Write document and measure performance
2410 let start = std::time::Instant::now();
2411 let mut writer = PdfWriter::new(&file_path).unwrap();
2412 writer.write_document(&mut document).unwrap();
2413 let duration = start.elapsed();
2414
2415 // Verify file creation and reasonable performance
2416 assert!(file_path.exists());
2417 let metadata = fs::metadata(&file_path).unwrap();
2418 assert!(metadata.len() > 10000); // Should be substantial
2419 assert!(duration.as_secs() < 5); // Should complete within 5 seconds
2420
2421 // Verify PDF structure
2422 let content = fs::read(&file_path).unwrap();
2423 let content_str = String::from_utf8_lossy(&content);
2424 assert!(content_str.contains("/Count 20"));
2425 }
2426
2427 #[test]
2428 fn test_writer_metadata_handling() {
2429 let temp_dir = TempDir::new().unwrap();
2430 let file_path = temp_dir.path().join("metadata_handling.pdf");
2431
2432 let mut document = Document::new();
2433 document.set_title("Metadata Handling Test");
2434 document.set_author("Test Author");
2435 document.set_subject("Testing metadata handling in writer");
2436 document.set_keywords("metadata, writer, test, integration");
2437
2438 let mut page = Page::a4();
2439 page.text()
2440 .set_font(Font::Helvetica, 14.0)
2441 .at(100.0, 700.0)
2442 .write("Metadata Test Document")
2443 .unwrap();
2444
2445 document.add_page(page);
2446
2447 // Write document
2448 let mut writer = PdfWriter::new(&file_path).unwrap();
2449 writer.write_document(&mut document).unwrap();
2450
2451 // Verify metadata in PDF
2452 let content = fs::read(&file_path).unwrap();
2453 let content_str = String::from_utf8_lossy(&content);
2454
2455 assert!(content_str.contains("/Title (Metadata Handling Test)"));
2456 assert!(content_str.contains("/Author (Test Author)"));
2457 assert!(content_str.contains("/Subject (Testing metadata handling in writer)"));
2458 assert!(content_str.contains("/Keywords (metadata, writer, test, integration)"));
2459 assert!(content_str.contains("/Creator (oxidize_pdf)"));
2460 assert!(content_str.contains("/Producer (oxidize_pdf v"));
2461 assert!(content_str.contains("/CreationDate"));
2462 assert!(content_str.contains("/ModDate"));
2463 }
2464
2465 #[test]
2466 fn test_writer_empty_document() {
2467 let temp_dir = TempDir::new().unwrap();
2468 let file_path = temp_dir.path().join("empty_document.pdf");
2469
2470 let mut document = Document::new();
2471 document.set_title("Empty Document Test");
2472
2473 // Write empty document (no pages)
2474 let mut writer = PdfWriter::new(&file_path).unwrap();
2475 writer.write_document(&mut document).unwrap();
2476
2477 // Verify valid PDF structure even with no pages
2478 assert!(file_path.exists());
2479 let metadata = fs::metadata(&file_path).unwrap();
2480 assert!(metadata.len() > 200); // Should have basic structure
2481
2482 let content = fs::read(&file_path).unwrap();
2483 let content_str = String::from_utf8_lossy(&content);
2484 assert!(content_str.contains("%PDF-1.7"));
2485 assert!(content_str.contains("/Type /Catalog"));
2486 assert!(content_str.contains("/Type /Pages"));
2487 assert!(content_str.contains("/Count 0"));
2488 assert!(content_str.contains("%%EOF"));
2489 }
2490
2491 #[test]
2492 fn test_writer_error_handling() {
2493 let mut document = Document::new();
2494 document.set_title("Error Handling Test");
2495 document.add_page(Page::a4());
2496
2497 // Test invalid path
2498 let result = PdfWriter::new("/invalid/path/that/does/not/exist.pdf");
2499 assert!(result.is_err());
2500
2501 // Test writing to buffer should work
2502 let mut buffer = Vec::new();
2503 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2504 let result = writer.write_document(&mut document);
2505 assert!(result.is_ok());
2506 assert!(!buffer.is_empty());
2507 }
2508
2509 #[test]
2510 fn test_writer_object_id_management() {
2511 let mut buffer = Vec::new();
2512 let mut document = Document::new();
2513 document.set_title("Object ID Management Test");
2514
2515 // Add multiple pages to test object ID generation
2516 for i in 0..5 {
2517 let mut page = Page::a4();
2518 page.text()
2519 .set_font(Font::Helvetica, 12.0)
2520 .at(100.0, 700.0)
2521 .write(&format!("Page {page}", page = i + 1))
2522 .unwrap();
2523 document.add_page(page);
2524 }
2525
2526 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2527 writer.write_document(&mut document).unwrap();
2528
2529 // Verify object numbering in PDF
2530 let content = String::from_utf8_lossy(&buffer);
2531 assert!(content.contains("1 0 obj")); // Catalog
2532 assert!(content.contains("2 0 obj")); // Pages
2533 assert!(content.contains("3 0 obj")); // First page
2534 assert!(content.contains("4 0 obj")); // First page content
2535 assert!(content.contains("5 0 obj")); // Second page
2536 assert!(content.contains("6 0 obj")); // Second page content
2537
2538 // Verify xref table
2539 assert!(content.contains("xref"));
2540 assert!(content.contains("0 ")); // Subsection start
2541 assert!(content.contains("0000000000 65535 f")); // Free object entry
2542 }
2543
2544 #[test]
2545 fn test_writer_content_stream_handling() {
2546 let mut buffer = Vec::new();
2547 let mut document = Document::new();
2548 document.set_title("Content Stream Test");
2549
2550 let mut page = Page::a4();
2551
2552 // Add content that will generate a content stream
2553 page.text()
2554 .set_font(Font::Helvetica, 12.0)
2555 .at(100.0, 700.0)
2556 .write("Content Stream Test")
2557 .unwrap();
2558
2559 page.graphics()
2560 .set_fill_color(Color::rgb(0.5, 0.5, 0.5))
2561 .rect(100.0, 600.0, 200.0, 50.0)
2562 .fill();
2563
2564 document.add_page(page);
2565
2566 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2567 writer.write_document(&mut document).unwrap();
2568
2569 // Verify content stream structure
2570 let content = String::from_utf8_lossy(&buffer);
2571 assert!(content.contains("stream"));
2572 assert!(content.contains("endstream"));
2573 assert!(content.contains("/Length"));
2574
2575 // Should contain content stream operations (may be compressed)
2576 assert!(content.contains("stream\n")); // Should have at least one stream
2577 assert!(content.contains("endstream")); // Should have matching endstream
2578 }
2579
2580 #[test]
2581 fn test_writer_font_resource_handling() {
2582 let mut buffer = Vec::new();
2583 let mut document = Document::new();
2584 document.set_title("Font Resource Test");
2585
2586 let mut page = Page::a4();
2587
2588 // Use different fonts to test font resource generation
2589 page.text()
2590 .set_font(Font::Helvetica, 12.0)
2591 .at(100.0, 700.0)
2592 .write("Helvetica Font")
2593 .unwrap();
2594
2595 page.text()
2596 .set_font(Font::TimesRoman, 14.0)
2597 .at(100.0, 650.0)
2598 .write("Times Roman Font")
2599 .unwrap();
2600
2601 page.text()
2602 .set_font(Font::Courier, 10.0)
2603 .at(100.0, 600.0)
2604 .write("Courier Font")
2605 .unwrap();
2606
2607 document.add_page(page);
2608
2609 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2610 writer.write_document(&mut document).unwrap();
2611
2612 // Verify font resources in PDF
2613 let content = String::from_utf8_lossy(&buffer);
2614 assert!(content.contains("/Font"));
2615 assert!(content.contains("/Helvetica"));
2616 assert!(content.contains("/Times-Roman"));
2617 assert!(content.contains("/Courier"));
2618 assert!(content.contains("/Type /Font"));
2619 assert!(content.contains("/Subtype /Type1"));
2620 }
2621
2622 #[test]
2623 fn test_writer_cross_reference_table() {
2624 let mut buffer = Vec::new();
2625 let mut document = Document::new();
2626 document.set_title("Cross Reference Test");
2627
2628 // Add content to generate multiple objects
2629 for i in 0..3 {
2630 let mut page = Page::a4();
2631 page.text()
2632 .set_font(Font::Helvetica, 12.0)
2633 .at(100.0, 700.0)
2634 .write(&format!("Page {page}", page = i + 1))
2635 .unwrap();
2636 document.add_page(page);
2637 }
2638
2639 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2640 writer.write_document(&mut document).unwrap();
2641
2642 // Verify cross-reference table structure
2643 let content = String::from_utf8_lossy(&buffer);
2644 assert!(content.contains("xref"));
2645 assert!(content.contains("trailer"));
2646 assert!(content.contains("startxref"));
2647 assert!(content.contains("%%EOF"));
2648
2649 // Verify xref entries format
2650 let xref_start = content.find("xref").unwrap();
2651 let xref_section = &content[xref_start..];
2652 assert!(xref_section.contains("0000000000 65535 f")); // Free object entry
2653
2654 // Should contain 'n' entries for used objects
2655 let n_count = xref_section.matches(" n ").count();
2656 assert!(n_count > 0); // Should have some object entries
2657
2658 // Verify trailer dictionary
2659 assert!(content.contains("/Size"));
2660 assert!(content.contains("/Root"));
2661 assert!(content.contains("/Info"));
2662 }
2663 }
2664
2665 // Comprehensive tests for writer.rs
2666 #[cfg(test)]
2667 mod comprehensive_tests {
2668 use super::*;
2669 use crate::page::Page;
2670 use crate::text::Font;
2671 use std::io::{self, ErrorKind, Write};
2672
2673 // Mock writer that simulates IO errors
2674 struct FailingWriter {
2675 fail_after: usize,
2676 written: usize,
2677 error_kind: ErrorKind,
2678 }
2679
2680 impl FailingWriter {
2681 fn new(fail_after: usize, error_kind: ErrorKind) -> Self {
2682 Self {
2683 fail_after,
2684 written: 0,
2685 error_kind,
2686 }
2687 }
2688 }
2689
2690 impl Write for FailingWriter {
2691 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
2692 if self.written >= self.fail_after {
2693 return Err(io::Error::new(self.error_kind, "Simulated write error"));
2694 }
2695 self.written += buf.len();
2696 Ok(buf.len())
2697 }
2698
2699 fn flush(&mut self) -> io::Result<()> {
2700 if self.written >= self.fail_after {
2701 return Err(io::Error::new(self.error_kind, "Simulated flush error"));
2702 }
2703 Ok(())
2704 }
2705 }
2706
2707 // Test 1: Write failure during header
2708 #[test]
2709 fn test_write_failure_during_header() {
2710 let failing_writer = FailingWriter::new(5, ErrorKind::PermissionDenied);
2711 let mut writer = PdfWriter::new_with_writer(failing_writer);
2712 let mut document = Document::new();
2713
2714 let result = writer.write_document(&mut document);
2715 assert!(result.is_err());
2716 }
2717
2718 // Test 2: Empty arrays and dictionaries
2719 #[test]
2720 fn test_write_empty_collections() {
2721 let mut buffer = Vec::new();
2722 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2723
2724 // Empty array
2725 writer
2726 .write_object(ObjectId::new(1, 0), Object::Array(vec![]))
2727 .unwrap();
2728
2729 // Empty dictionary
2730 let empty_dict = Dictionary::new();
2731 writer
2732 .write_object(ObjectId::new(2, 0), Object::Dictionary(empty_dict))
2733 .unwrap();
2734
2735 let content = String::from_utf8_lossy(&buffer);
2736 assert!(content.contains("[]")); // Empty array
2737 assert!(content.contains("<<\n>>")); // Empty dictionary
2738 }
2739
2740 // Test 3: Deeply nested structures
2741 #[test]
2742 fn test_write_deeply_nested_structures() {
2743 let mut buffer = Vec::new();
2744 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2745
2746 // Create deeply nested array
2747 let mut nested = Object::Array(vec![Object::Integer(1)]);
2748 for _ in 0..10 {
2749 nested = Object::Array(vec![nested]);
2750 }
2751
2752 writer.write_object(ObjectId::new(1, 0), nested).unwrap();
2753
2754 let content = String::from_utf8_lossy(&buffer);
2755 assert!(content.contains("[[[[[[[[[["));
2756 assert!(content.contains("]]]]]]]]]]"));
2757 }
2758
2759 // Test 4: Large integers
2760 #[test]
2761 fn test_write_large_integers() {
2762 let mut buffer = Vec::new();
2763 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2764
2765 let test_cases = vec![i64::MAX, i64::MIN, 0, -1, 1, 999999999999999];
2766
2767 for (i, &value) in test_cases.iter().enumerate() {
2768 writer
2769 .write_object(ObjectId::new(i as u32 + 1, 0), Object::Integer(value))
2770 .unwrap();
2771 }
2772
2773 let content = String::from_utf8_lossy(&buffer);
2774 for value in test_cases {
2775 assert!(content.contains(&value.to_string()));
2776 }
2777 }
2778
2779 // Test 5: Floating point edge cases
2780 #[test]
2781 fn test_write_float_edge_cases() {
2782 let mut buffer = Vec::new();
2783 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2784
2785 let test_cases = [
2786 0.0, -0.0, 1.0, -1.0, 0.123456, -0.123456, 1234.56789, 0.000001, 1000000.0,
2787 ];
2788
2789 for (i, &value) in test_cases.iter().enumerate() {
2790 writer
2791 .write_object(ObjectId::new(i as u32 + 1, 0), Object::Real(value))
2792 .unwrap();
2793 }
2794
2795 let content = String::from_utf8_lossy(&buffer);
2796
2797 // Check formatting rules
2798 assert!(content.contains("0")); // 0.0 should be "0"
2799 assert!(content.contains("1")); // 1.0 should be "1"
2800 assert!(content.contains("0.123456"));
2801 assert!(content.contains("1234.567")); // Should be rounded
2802 }
2803
2804 // Test 6: Special characters in strings
2805 #[test]
2806 fn test_write_special_characters_in_strings() {
2807 let mut buffer = Vec::new();
2808 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2809
2810 let test_strings = vec![
2811 "Simple string",
2812 "String with (parentheses)",
2813 "String with \\backslash",
2814 "String with \nnewline",
2815 "String with \ttab",
2816 "String with \rcarriage return",
2817 "Unicode: café",
2818 "Emoji: 🎯",
2819 "", // Empty string
2820 ];
2821
2822 for (i, s) in test_strings.iter().enumerate() {
2823 writer
2824 .write_object(
2825 ObjectId::new(i as u32 + 1, 0),
2826 Object::String(s.to_string()),
2827 )
2828 .unwrap();
2829 }
2830
2831 let content = String::from_utf8_lossy(&buffer);
2832
2833 // Verify strings are properly enclosed
2834 assert!(content.contains("(Simple string)"));
2835 assert!(content.contains("()")); // Empty string
2836 }
2837
2838 // Test 7: Escape sequences in names
2839 #[test]
2840 fn test_write_names_with_special_chars() {
2841 let mut buffer = Vec::new();
2842 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2843
2844 let test_names = vec![
2845 "SimpleName",
2846 "Name With Spaces",
2847 "Name#With#Hash",
2848 "Name/With/Slash",
2849 "Name(With)Parens",
2850 "Name[With]Brackets",
2851 "", // Empty name
2852 ];
2853
2854 for (i, name) in test_names.iter().enumerate() {
2855 writer
2856 .write_object(
2857 ObjectId::new(i as u32 + 1, 0),
2858 Object::Name(name.to_string()),
2859 )
2860 .unwrap();
2861 }
2862
2863 let content = String::from_utf8_lossy(&buffer);
2864
2865 // Names should be prefixed with /
2866 assert!(content.contains("/SimpleName"));
2867 assert!(content.contains("/")); // Empty name should be just /
2868 }
2869
2870 // Test 8: Binary data in streams
2871 #[test]
2872 fn test_write_binary_streams() {
2873 let mut buffer = Vec::new();
2874 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2875
2876 // Create stream with binary data
2877 let mut dict = Dictionary::new();
2878 let binary_data: Vec<u8> = (0..=255).collect();
2879 dict.set("Length", Object::Integer(binary_data.len() as i64));
2880
2881 writer
2882 .write_object(ObjectId::new(1, 0), Object::Stream(dict, binary_data))
2883 .unwrap();
2884
2885 let content = buffer;
2886
2887 // Verify stream structure
2888 assert!(content.windows(6).any(|w| w == b"stream"));
2889 assert!(content.windows(9).any(|w| w == b"endstream"));
2890
2891 // Verify binary data is present
2892 let stream_start = content.windows(6).position(|w| w == b"stream").unwrap() + 7; // "stream\n"
2893 let stream_end = content.windows(9).position(|w| w == b"endstream").unwrap();
2894
2895 assert!(stream_end > stream_start);
2896 // Allow for line ending differences
2897 let data_length = stream_end - stream_start;
2898 assert!((256..=257).contains(&data_length));
2899 }
2900
2901 // Test 9: Zero-length streams
2902 #[test]
2903 fn test_write_zero_length_stream() {
2904 let mut buffer = Vec::new();
2905 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2906
2907 let mut dict = Dictionary::new();
2908 dict.set("Length", Object::Integer(0));
2909
2910 writer
2911 .write_object(ObjectId::new(1, 0), Object::Stream(dict, vec![]))
2912 .unwrap();
2913
2914 let content = String::from_utf8_lossy(&buffer);
2915 assert!(content.contains("/Length 0"));
2916 assert!(content.contains("stream\n\nendstream"));
2917 }
2918
2919 // Test 10: Duplicate dictionary keys
2920 #[test]
2921 fn test_write_duplicate_dictionary_keys() {
2922 let mut buffer = Vec::new();
2923 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2924
2925 let mut dict = Dictionary::new();
2926 dict.set("Key", Object::Integer(1));
2927 dict.set("Key", Object::Integer(2)); // Overwrite
2928
2929 writer
2930 .write_object(ObjectId::new(1, 0), Object::Dictionary(dict))
2931 .unwrap();
2932
2933 let content = String::from_utf8_lossy(&buffer);
2934
2935 // Should only have the last value
2936 assert!(content.contains("/Key 2"));
2937 assert!(!content.contains("/Key 1"));
2938 }
2939
2940 // Test 11: Unicode in metadata
2941 #[test]
2942 fn test_write_unicode_metadata() {
2943 let mut buffer = Vec::new();
2944 let mut document = Document::new();
2945
2946 document.set_title("Título en Español");
2947 document.set_author("作者");
2948 document.set_subject("Тема документа");
2949 document.set_keywords("מילות מפתח");
2950
2951 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2952 writer.write_document(&mut document).unwrap();
2953
2954 let content = buffer;
2955
2956 // Verify metadata is present in some form
2957 let content_str = String::from_utf8_lossy(&content);
2958 assert!(content_str.contains("Title") || content_str.contains("Título"));
2959 assert!(content_str.contains("Author") || content_str.contains("作者"));
2960 }
2961
2962 // Test 12: Very long strings
2963 #[test]
2964 fn test_write_very_long_strings() {
2965 let mut buffer = Vec::new();
2966 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2967
2968 let long_string = "A".repeat(10000);
2969 writer
2970 .write_object(ObjectId::new(1, 0), Object::String(long_string.clone()))
2971 .unwrap();
2972
2973 let content = String::from_utf8_lossy(&buffer);
2974 assert!(content.contains(&format!("({long_string})")));
2975 }
2976
2977 // Test 13: Maximum object ID
2978 #[test]
2979 fn test_write_maximum_object_id() {
2980 let mut buffer = Vec::new();
2981 let mut writer = PdfWriter::new_with_writer(&mut buffer);
2982
2983 let max_id = ObjectId::new(u32::MAX, 65535);
2984 writer.write_object(max_id, Object::Null).unwrap();
2985
2986 let content = String::from_utf8_lossy(&buffer);
2987 assert!(content.contains(&format!("{} 65535 obj", u32::MAX)));
2988 }
2989
2990 // Test 14: Complex page with multiple resources
2991 #[test]
2992 fn test_write_complex_page() {
2993 let mut buffer = Vec::new();
2994 let mut document = Document::new();
2995
2996 let mut page = Page::a4();
2997
2998 // Add various content
2999 page.text()
3000 .set_font(Font::Helvetica, 12.0)
3001 .at(100.0, 700.0)
3002 .write("Text with Helvetica")
3003 .unwrap();
3004
3005 page.text()
3006 .set_font(Font::TimesRoman, 14.0)
3007 .at(100.0, 650.0)
3008 .write("Text with Times")
3009 .unwrap();
3010
3011 page.graphics()
3012 .set_fill_color(crate::graphics::Color::Rgb(1.0, 0.0, 0.0))
3013 .rect(50.0, 50.0, 100.0, 100.0)
3014 .fill();
3015
3016 page.graphics()
3017 .set_stroke_color(crate::graphics::Color::Rgb(0.0, 0.0, 1.0))
3018 .move_to(200.0, 200.0)
3019 .line_to(300.0, 300.0)
3020 .stroke();
3021
3022 document.add_page(page);
3023
3024 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3025 writer.write_document(&mut document).unwrap();
3026
3027 let content = String::from_utf8_lossy(&buffer);
3028
3029 // Verify multiple fonts
3030 assert!(content.contains("/Helvetica"));
3031 assert!(content.contains("/Times-Roman"));
3032
3033 // Verify graphics operations (content is compressed, so check for stream presence)
3034 assert!(content.contains("stream"));
3035 assert!(content.contains("endstream"));
3036 assert!(content.contains("/FlateDecode")); // Compression filter
3037 }
3038
3039 // Test 15: Document with 100 pages
3040 #[test]
3041 fn test_write_many_pages_document() {
3042 let mut buffer = Vec::new();
3043 let mut document = Document::new();
3044
3045 for i in 0..100 {
3046 let mut page = Page::a4();
3047 page.text()
3048 .set_font(Font::Helvetica, 12.0)
3049 .at(100.0, 700.0)
3050 .write(&format!("Page {}", i + 1))
3051 .unwrap();
3052 document.add_page(page);
3053 }
3054
3055 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3056 writer.write_document(&mut document).unwrap();
3057
3058 let content = String::from_utf8_lossy(&buffer);
3059
3060 // Verify page count
3061 assert!(content.contains("/Count 100"));
3062
3063 // Verify that we have page objects (100 pages + 1 pages tree = 101 total)
3064 let page_type_count = content.matches("/Type /Page").count();
3065 assert!(page_type_count >= 100);
3066
3067 // Verify content streams exist (compressed)
3068 assert!(content.contains("/FlateDecode"));
3069 }
3070
3071 // Test 16: Write failure during xref
3072 #[test]
3073 fn test_write_failure_during_xref() {
3074 let failing_writer = FailingWriter::new(1000, ErrorKind::Other);
3075 let mut writer = PdfWriter::new_with_writer(failing_writer);
3076 let mut document = Document::new();
3077
3078 // Add some content to ensure we get past header
3079 for _ in 0..5 {
3080 document.add_page(Page::a4());
3081 }
3082
3083 let result = writer.write_document(&mut document);
3084 assert!(result.is_err());
3085 }
3086
3087 // Test 17: Position tracking accuracy
3088 #[test]
3089 fn test_position_tracking_accuracy() {
3090 let mut buffer = Vec::new();
3091 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3092
3093 // Write several objects and verify positions
3094 let ids = vec![
3095 ObjectId::new(1, 0),
3096 ObjectId::new(2, 0),
3097 ObjectId::new(3, 0),
3098 ];
3099
3100 for id in &ids {
3101 writer.write_object(*id, Object::Null).unwrap();
3102 }
3103
3104 // Verify positions were tracked
3105 for id in &ids {
3106 assert!(writer.xref_positions.contains_key(id));
3107 let pos = writer.xref_positions[id];
3108 assert!(pos < writer.current_position);
3109 }
3110 }
3111
3112 // Test 18: Object reference cycles
3113 #[test]
3114 fn test_write_object_reference_cycles() {
3115 let mut buffer = Vec::new();
3116 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3117
3118 // Create dictionary with self-reference
3119 let mut dict = Dictionary::new();
3120 dict.set("Self", Object::Reference(ObjectId::new(1, 0)));
3121 dict.set("Other", Object::Reference(ObjectId::new(2, 0)));
3122
3123 writer
3124 .write_object(ObjectId::new(1, 0), Object::Dictionary(dict))
3125 .unwrap();
3126
3127 let content = String::from_utf8_lossy(&buffer);
3128 assert!(content.contains("/Self 1 0 R"));
3129 assert!(content.contains("/Other 2 0 R"));
3130 }
3131
3132 // Test 19: Different page sizes
3133 #[test]
3134 fn test_write_different_page_sizes() {
3135 let mut buffer = Vec::new();
3136 let mut document = Document::new();
3137
3138 // Add pages with different sizes
3139 document.add_page(Page::a4());
3140 document.add_page(Page::letter());
3141 document.add_page(Page::new(200.0, 300.0)); // Custom size
3142
3143 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3144 writer.write_document(&mut document).unwrap();
3145
3146 let content = String::from_utf8_lossy(&buffer);
3147
3148 // Verify different MediaBox values
3149 assert!(content.contains("[0 0 595")); // A4 width
3150 assert!(content.contains("[0 0 612")); // Letter width
3151 assert!(content.contains("[0 0 200 300]")); // Custom size
3152 }
3153
3154 // Test 20: Empty metadata fields
3155 #[test]
3156 fn test_write_empty_metadata() {
3157 let mut buffer = Vec::new();
3158 let mut document = Document::new();
3159
3160 // Set empty strings
3161 document.set_title("");
3162 document.set_author("");
3163
3164 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3165 writer.write_document(&mut document).unwrap();
3166
3167 let content = String::from_utf8_lossy(&buffer);
3168
3169 // Should have empty strings
3170 assert!(content.contains("/Title ()"));
3171 assert!(content.contains("/Author ()"));
3172 }
3173
3174 // Test 21: Write to read-only location (simulated)
3175 #[test]
3176 fn test_write_permission_error() {
3177 let failing_writer = FailingWriter::new(0, ErrorKind::PermissionDenied);
3178 let mut writer = PdfWriter::new_with_writer(failing_writer);
3179 let mut document = Document::new();
3180
3181 let result = writer.write_document(&mut document);
3182 assert!(result.is_err());
3183 }
3184
3185 // Test 22: Xref with many objects
3186 #[test]
3187 fn test_write_xref_many_objects() {
3188 let mut buffer = Vec::new();
3189 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3190
3191 // Create many objects
3192 for i in 1..=1000 {
3193 writer
3194 .xref_positions
3195 .insert(ObjectId::new(i, 0), (i * 100) as u64);
3196 }
3197
3198 writer.write_xref().unwrap();
3199
3200 let content = String::from_utf8_lossy(&buffer);
3201
3202 // Verify xref structure
3203 assert!(content.contains("xref"));
3204 assert!(content.contains("0 1001")); // 0 + 1000 objects
3205
3206 // Verify proper formatting of positions
3207 assert!(content.contains("0000000000 65535 f"));
3208 assert!(content.contains(" n "));
3209 }
3210
3211 // Test 23: Stream with compression markers
3212 #[test]
3213 fn test_write_stream_with_filter() {
3214 let mut buffer = Vec::new();
3215 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3216
3217 let mut dict = Dictionary::new();
3218 dict.set("Length", Object::Integer(100));
3219 dict.set("Filter", Object::Name("FlateDecode".to_string()));
3220
3221 let data = vec![0u8; 100];
3222 writer
3223 .write_object(ObjectId::new(1, 0), Object::Stream(dict, data))
3224 .unwrap();
3225
3226 let content = String::from_utf8_lossy(&buffer);
3227 assert!(content.contains("/Filter /FlateDecode"));
3228 assert!(content.contains("/Length 100"));
3229 }
3230
3231 // Test 24: Arrays with mixed types
3232 #[test]
3233 fn test_write_mixed_type_arrays() {
3234 let mut buffer = Vec::new();
3235 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3236
3237 let array = vec![
3238 Object::Integer(42),
3239 Object::Real(3.14),
3240 Object::String("Hello".to_string()),
3241 Object::Name("World".to_string()),
3242 Object::Boolean(true),
3243 Object::Null,
3244 Object::Reference(ObjectId::new(5, 0)),
3245 ];
3246
3247 writer
3248 .write_object(ObjectId::new(1, 0), Object::Array(array))
3249 .unwrap();
3250
3251 let content = String::from_utf8_lossy(&buffer);
3252 assert!(content.contains("[42 3.14 (Hello) /World true null 5 0 R]"));
3253 }
3254
3255 // Test 25: Dictionary with nested structures
3256 #[test]
3257 fn test_write_nested_dictionaries() {
3258 let mut buffer = Vec::new();
3259 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3260
3261 let mut inner = Dictionary::new();
3262 inner.set("Inner", Object::Integer(1));
3263
3264 let mut middle = Dictionary::new();
3265 middle.set("Middle", Object::Dictionary(inner));
3266
3267 let mut outer = Dictionary::new();
3268 outer.set("Outer", Object::Dictionary(middle));
3269
3270 writer
3271 .write_object(ObjectId::new(1, 0), Object::Dictionary(outer))
3272 .unwrap();
3273
3274 let content = String::from_utf8_lossy(&buffer);
3275 assert!(content.contains("/Outer <<"));
3276 assert!(content.contains("/Middle <<"));
3277 assert!(content.contains("/Inner 1"));
3278 }
3279
3280 // Test 26: Maximum generation number
3281 #[test]
3282 fn test_write_max_generation_number() {
3283 let mut buffer = Vec::new();
3284 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3285
3286 let id = ObjectId::new(1, 65535);
3287 writer.write_object(id, Object::Null).unwrap();
3288
3289 let content = String::from_utf8_lossy(&buffer);
3290 assert!(content.contains("1 65535 obj"));
3291 }
3292
3293 // Test 27: Cross-platform line endings
3294 #[test]
3295 fn test_write_consistent_line_endings() {
3296 let mut buffer = Vec::new();
3297 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3298
3299 writer.write_header().unwrap();
3300
3301 let content = buffer;
3302
3303 // PDF should use \n consistently
3304 assert!(content.windows(2).filter(|w| w == b"\r\n").count() == 0);
3305 assert!(content.windows(1).filter(|w| w == b"\n").count() > 0);
3306 }
3307
3308 // Test 28: Flush behavior
3309 #[test]
3310 fn test_writer_flush_behavior() {
3311 struct FlushCounter {
3312 buffer: Vec<u8>,
3313 flush_count: std::cell::RefCell<usize>,
3314 }
3315
3316 impl Write for FlushCounter {
3317 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3318 self.buffer.extend_from_slice(buf);
3319 Ok(buf.len())
3320 }
3321
3322 fn flush(&mut self) -> io::Result<()> {
3323 *self.flush_count.borrow_mut() += 1;
3324 Ok(())
3325 }
3326 }
3327
3328 let flush_counter = FlushCounter {
3329 buffer: Vec::new(),
3330 flush_count: std::cell::RefCell::new(0),
3331 };
3332
3333 let mut writer = PdfWriter::new_with_writer(flush_counter);
3334 let mut document = Document::new();
3335
3336 writer.write_document(&mut document).unwrap();
3337
3338 // Verify flush was called
3339 assert!(*writer.writer.flush_count.borrow() > 0);
3340 }
3341
3342 // Test 29: Special PDF characters in content
3343 #[test]
3344 fn test_write_pdf_special_characters() {
3345 let mut buffer = Vec::new();
3346 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3347
3348 // Test parentheses in strings
3349 writer
3350 .write_object(
3351 ObjectId::new(1, 0),
3352 Object::String("Text with ) and ( parentheses".to_string()),
3353 )
3354 .unwrap();
3355
3356 // Test backslash
3357 writer
3358 .write_object(
3359 ObjectId::new(2, 0),
3360 Object::String("Text with \\ backslash".to_string()),
3361 )
3362 .unwrap();
3363
3364 let content = String::from_utf8_lossy(&buffer);
3365
3366 // Should properly handle special characters
3367 assert!(content.contains("(Text with ) and ( parentheses)"));
3368 assert!(content.contains("(Text with \\ backslash)"));
3369 }
3370
3371 // Test 30: Resource dictionary structure
3372 #[test]
3373 fn test_write_resource_dictionary() {
3374 let mut buffer = Vec::new();
3375 let mut document = Document::new();
3376
3377 let mut page = Page::a4();
3378
3379 // Add multiple resources
3380 page.text()
3381 .set_font(Font::Helvetica, 12.0)
3382 .at(100.0, 700.0)
3383 .write("Test")
3384 .unwrap();
3385
3386 page.graphics()
3387 .set_fill_color(crate::graphics::Color::Rgb(1.0, 0.0, 0.0))
3388 .rect(50.0, 50.0, 100.0, 100.0)
3389 .fill();
3390
3391 document.add_page(page);
3392
3393 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3394 writer.write_document(&mut document).unwrap();
3395
3396 let content = String::from_utf8_lossy(&buffer);
3397
3398 // Verify resource dictionary structure
3399 assert!(content.contains("/Resources"));
3400 assert!(content.contains("/Font"));
3401 // Basic structure verification
3402 assert!(content.contains("stream") && content.contains("endstream"));
3403 }
3404
3405 // Test 31: Error recovery after failed write
3406 #[test]
3407 fn test_error_recovery_after_failed_write() {
3408 let mut buffer = Vec::new();
3409 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3410
3411 // Attempt to write an object
3412 writer
3413 .write_object(ObjectId::new(1, 0), Object::Null)
3414 .unwrap();
3415
3416 // Verify state is still consistent
3417 assert!(writer.xref_positions.contains_key(&ObjectId::new(1, 0)));
3418 assert!(writer.current_position > 0);
3419
3420 // Should be able to continue writing
3421 writer
3422 .write_object(ObjectId::new(2, 0), Object::Null)
3423 .unwrap();
3424 assert!(writer.xref_positions.contains_key(&ObjectId::new(2, 0)));
3425 }
3426
3427 // Test 32: Memory efficiency with large document
3428 #[test]
3429 fn test_memory_efficiency_large_document() {
3430 let mut buffer = Vec::new();
3431 let mut document = Document::new();
3432
3433 // Create document with repetitive content
3434 for i in 0..50 {
3435 let mut page = Page::a4();
3436
3437 // Add lots of text
3438 for j in 0..20 {
3439 page.text()
3440 .set_font(Font::Helvetica, 10.0)
3441 .at(50.0, 700.0 - (j as f64 * 30.0))
3442 .write(&format!("Line {j} on page {i}"))
3443 .unwrap();
3444 }
3445
3446 document.add_page(page);
3447 }
3448
3449 let _initial_capacity = buffer.capacity();
3450 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3451 writer.write_document(&mut document).unwrap();
3452
3453 // Verify reasonable memory usage
3454 assert!(!buffer.is_empty());
3455 assert!(buffer.capacity() <= buffer.len() * 2); // No excessive allocation
3456 }
3457
3458 // Test 33: Trailer dictionary validation
3459 #[test]
3460 fn test_trailer_dictionary_content() {
3461 let mut buffer = Vec::new();
3462 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3463
3464 // Set required IDs before calling write_trailer
3465 writer.catalog_id = Some(ObjectId::new(1, 0));
3466 writer.info_id = Some(ObjectId::new(2, 0));
3467 writer.xref_positions.insert(ObjectId::new(1, 0), 0);
3468 writer.xref_positions.insert(ObjectId::new(2, 0), 0);
3469
3470 // Write minimal content
3471 writer.write_trailer(1000).unwrap();
3472
3473 let content = String::from_utf8_lossy(&buffer);
3474
3475 // Verify trailer structure
3476 assert!(content.contains("trailer"));
3477 assert!(content.contains("/Size"));
3478 assert!(content.contains("/Root 1 0 R"));
3479 assert!(content.contains("/Info 2 0 R"));
3480 assert!(content.contains("startxref"));
3481 assert!(content.contains("1000"));
3482 assert!(content.contains("%%EOF"));
3483 }
3484
3485 // Test 34: Write bytes handles partial writes
3486 #[test]
3487 fn test_write_bytes_partial_writes() {
3488 struct PartialWriter {
3489 buffer: Vec<u8>,
3490 max_per_write: usize,
3491 }
3492
3493 impl Write for PartialWriter {
3494 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3495 let to_write = buf.len().min(self.max_per_write);
3496 self.buffer.extend_from_slice(&buf[..to_write]);
3497 Ok(to_write)
3498 }
3499
3500 fn flush(&mut self) -> io::Result<()> {
3501 Ok(())
3502 }
3503 }
3504
3505 let partial_writer = PartialWriter {
3506 buffer: Vec::new(),
3507 max_per_write: 10,
3508 };
3509
3510 let mut writer = PdfWriter::new_with_writer(partial_writer);
3511
3512 // Write large data
3513 let large_data = vec![b'A'; 100];
3514 writer.write_bytes(&large_data).unwrap();
3515
3516 // Verify all data was written
3517 assert_eq!(writer.writer.buffer.len(), 100);
3518 assert!(writer.writer.buffer.iter().all(|&b| b == b'A'));
3519 }
3520
3521 // Test 35: Object ID conflicts
3522 #[test]
3523 fn test_object_id_conflict_handling() {
3524 let mut buffer = Vec::new();
3525 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3526
3527 let id = ObjectId::new(1, 0);
3528
3529 // Write same ID twice
3530 writer.write_object(id, Object::Integer(1)).unwrap();
3531 writer.write_object(id, Object::Integer(2)).unwrap();
3532
3533 // Position should be updated
3534 assert!(writer.xref_positions.contains_key(&id));
3535
3536 let content = String::from_utf8_lossy(&buffer);
3537
3538 // Both objects should be written
3539 assert!(content.matches("1 0 obj").count() == 2);
3540 }
3541
3542 // Test 36: Content stream encoding
3543 #[test]
3544 fn test_content_stream_encoding() {
3545 let mut buffer = Vec::new();
3546 let mut document = Document::new();
3547
3548 let mut page = Page::a4();
3549
3550 // Add text with special characters
3551 page.text()
3552 .set_font(Font::Helvetica, 12.0)
3553 .at(100.0, 700.0)
3554 .write("Special: €£¥")
3555 .unwrap();
3556
3557 document.add_page(page);
3558
3559 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3560 writer.write_document(&mut document).unwrap();
3561
3562 // Content should be written (exact encoding depends on implementation)
3563 assert!(!buffer.is_empty());
3564 }
3565
3566 // Test 37: PDF version in header
3567 #[test]
3568 fn test_pdf_version_header() {
3569 let mut buffer = Vec::new();
3570 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3571
3572 writer.write_header().unwrap();
3573
3574 let content = &buffer;
3575
3576 // Verify PDF version
3577 assert!(content.starts_with(b"%PDF-1.7\n"));
3578
3579 // Verify binary marker
3580 assert_eq!(content[9], b'%');
3581 assert_eq!(content[10], 0xE2);
3582 assert_eq!(content[11], 0xE3);
3583 assert_eq!(content[12], 0xCF);
3584 assert_eq!(content[13], 0xD3);
3585 assert_eq!(content[14], b'\n');
3586 }
3587
3588 // Test 38: Page content operations order
3589 #[test]
3590 fn test_page_content_operations_order() {
3591 let mut buffer = Vec::new();
3592 let mut document = Document::new();
3593
3594 let mut page = Page::a4();
3595
3596 // Add operations in specific order
3597 page.graphics()
3598 .save_state()
3599 .set_fill_color(crate::graphics::Color::Rgb(1.0, 0.0, 0.0))
3600 .rect(50.0, 50.0, 100.0, 100.0)
3601 .fill()
3602 .restore_state();
3603
3604 document.add_page(page);
3605
3606 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3607 writer.write_document(&mut document).unwrap();
3608
3609 let content = String::from_utf8_lossy(&buffer);
3610
3611 // Operations should maintain order
3612 // Note: Exact content depends on compression
3613 assert!(content.contains("stream"));
3614 assert!(content.contains("endstream"));
3615 }
3616
3617 // Test 39: Invalid UTF-8 handling
3618 #[test]
3619 fn test_invalid_utf8_handling() {
3620 let mut buffer = Vec::new();
3621 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3622
3623 // Create string with invalid UTF-8
3624 let invalid_utf8 = vec![0xFF, 0xFE, 0xFD];
3625 let string = String::from_utf8_lossy(&invalid_utf8).to_string();
3626
3627 writer
3628 .write_object(ObjectId::new(1, 0), Object::String(string))
3629 .unwrap();
3630
3631 // Should not panic and should write something
3632 assert!(!buffer.is_empty());
3633 }
3634
3635 // Test 40: Round-trip write and parse
3636 #[test]
3637 fn test_roundtrip_write_parse() {
3638 use crate::parser::PdfReader;
3639 use std::io::Cursor;
3640
3641 let mut buffer = Vec::new();
3642 let mut document = Document::new();
3643
3644 document.set_title("Round-trip Test");
3645 document.add_page(Page::a4());
3646
3647 // Write document
3648 {
3649 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3650 writer.write_document(&mut document).unwrap();
3651 }
3652
3653 // Try to parse what we wrote
3654 let cursor = Cursor::new(buffer);
3655 let result = PdfReader::new(cursor);
3656
3657 // Even if parsing fails (due to simplified writer),
3658 // we should have written valid PDF structure
3659 assert!(result.is_ok() || result.is_err()); // Either outcome is acceptable for this test
3660 }
3661
3662 // Test to validate that all referenced ObjectIds exist in xref table
3663 #[test]
3664 fn test_pdf_object_references_are_valid() {
3665 let mut buffer = Vec::new();
3666 let mut document = Document::new();
3667 document.set_title("Object Reference Validation Test");
3668
3669 // Create a page with form fields (the problematic case)
3670 let mut page = Page::a4();
3671
3672 // Add some text content
3673 page.text()
3674 .set_font(Font::Helvetica, 12.0)
3675 .at(50.0, 700.0)
3676 .write("Form with validation:")
3677 .unwrap();
3678
3679 // Add form widgets that previously caused invalid references
3680 use crate::forms::{BorderStyle, TextField, Widget, WidgetAppearance};
3681 use crate::geometry::{Point, Rectangle};
3682 use crate::graphics::Color;
3683
3684 let text_appearance = WidgetAppearance {
3685 border_color: Some(Color::rgb(0.0, 0.0, 0.5)),
3686 background_color: Some(Color::rgb(0.95, 0.95, 1.0)),
3687 border_width: 1.0,
3688 border_style: BorderStyle::Solid,
3689 };
3690
3691 let name_widget = Widget::new(Rectangle::new(
3692 Point::new(150.0, 640.0),
3693 Point::new(400.0, 660.0),
3694 ))
3695 .with_appearance(text_appearance);
3696
3697 page.add_form_widget(name_widget.clone());
3698 document.add_page(page);
3699
3700 // Enable forms and add field
3701 let form_manager = document.enable_forms();
3702 let name_field = TextField::new("name_field").with_default_value("");
3703 form_manager
3704 .add_text_field(name_field, name_widget, None)
3705 .unwrap();
3706
3707 // Write the document
3708 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3709 writer.write_document(&mut document).unwrap();
3710
3711 // Parse the generated PDF to validate structure
3712 let content = String::from_utf8_lossy(&buffer);
3713
3714 // Extract xref section to find max object ID
3715 if let Some(xref_start) = content.find("xref\n") {
3716 let xref_section = &content[xref_start..];
3717 let lines: Vec<&str> = xref_section.lines().collect();
3718 if lines.len() > 1 {
3719 let first_line = lines[1]; // Second line after "xref"
3720 if let Some(space_pos) = first_line.find(' ') {
3721 let (start_str, count_str) = first_line.split_at(space_pos);
3722 let start_id: u32 = start_str.parse().unwrap_or(0);
3723 let count: u32 = count_str.trim().parse().unwrap_or(0);
3724 let max_valid_id = start_id + count - 1;
3725
3726 // Check that no references exceed the xref table size
3727 // Look for patterns like "1000 0 R" that shouldn't exist
3728 assert!(
3729 !content.contains("1000 0 R"),
3730 "Found invalid ObjectId reference 1000 0 R - max valid ID is {max_valid_id}"
3731 );
3732 assert!(
3733 !content.contains("1001 0 R"),
3734 "Found invalid ObjectId reference 1001 0 R - max valid ID is {max_valid_id}"
3735 );
3736 assert!(
3737 !content.contains("1002 0 R"),
3738 "Found invalid ObjectId reference 1002 0 R - max valid ID is {max_valid_id}"
3739 );
3740 assert!(
3741 !content.contains("1003 0 R"),
3742 "Found invalid ObjectId reference 1003 0 R - max valid ID is {max_valid_id}"
3743 );
3744
3745 // Verify all object references are within valid range
3746 for line in content.lines() {
3747 if line.contains(" 0 R") {
3748 // Extract object IDs from references
3749 let words: Vec<&str> = line.split_whitespace().collect();
3750 for i in 0..words.len().saturating_sub(2) {
3751 if words[i + 1] == "0" && words[i + 2] == "R" {
3752 if let Ok(obj_id) = words[i].parse::<u32>() {
3753 assert!(obj_id <= max_valid_id,
3754 "Object reference {obj_id} 0 R exceeds xref table size (max: {max_valid_id})");
3755 }
3756 }
3757 }
3758 }
3759 }
3760
3761 println!("✅ PDF structure validation passed: all {count} object references are valid (max ID: {max_valid_id})");
3762 }
3763 }
3764 } else {
3765 panic!("Could not find xref section in generated PDF");
3766 }
3767 }
3768
3769 #[test]
3770 fn test_xref_stream_generation() {
3771 let mut buffer = Vec::new();
3772 let mut document = Document::new();
3773 document.set_title("XRef Stream Test");
3774
3775 let page = Page::a4();
3776 document.add_page(page);
3777
3778 // Create writer with XRef stream configuration
3779 let config = WriterConfig {
3780 use_xref_streams: true,
3781 pdf_version: "1.5".to_string(),
3782 compress_streams: true,
3783 };
3784 let mut writer = PdfWriter::with_config(&mut buffer, config);
3785 writer.write_document(&mut document).unwrap();
3786
3787 let content = String::from_utf8_lossy(&buffer);
3788
3789 // Should have PDF 1.5 header
3790 assert!(content.starts_with("%PDF-1.5\n"));
3791
3792 // Should NOT have traditional xref table
3793 assert!(!content.contains("\nxref\n"));
3794 assert!(!content.contains("\ntrailer\n"));
3795
3796 // Should have XRef stream object
3797 assert!(content.contains("/Type /XRef"));
3798 assert!(content.contains("/Filter /FlateDecode"));
3799 assert!(content.contains("/W ["));
3800 assert!(content.contains("/Root "));
3801 assert!(content.contains("/Info "));
3802
3803 // Should have startxref pointing to XRef stream
3804 assert!(content.contains("\nstartxref\n"));
3805 assert!(content.contains("\n%%EOF\n"));
3806 }
3807
3808 #[test]
3809 fn test_writer_config_default() {
3810 let config = WriterConfig::default();
3811 assert!(!config.use_xref_streams);
3812 assert_eq!(config.pdf_version, "1.7");
3813 }
3814
3815 #[test]
3816 fn test_pdf_version_in_header() {
3817 let mut buffer = Vec::new();
3818 let mut document = Document::new();
3819
3820 let page = Page::a4();
3821 document.add_page(page);
3822
3823 // Test with custom version
3824 let config = WriterConfig {
3825 use_xref_streams: false,
3826 pdf_version: "1.4".to_string(),
3827 compress_streams: true,
3828 };
3829 let mut writer = PdfWriter::with_config(&mut buffer, config);
3830 writer.write_document(&mut document).unwrap();
3831
3832 let content = String::from_utf8_lossy(&buffer);
3833 assert!(content.starts_with("%PDF-1.4\n"));
3834 }
3835
3836 #[test]
3837 fn test_xref_stream_with_multiple_objects() {
3838 let mut buffer = Vec::new();
3839 let mut document = Document::new();
3840 document.set_title("Multi Object XRef Stream Test");
3841
3842 // Add multiple pages to create more objects
3843 for i in 0..3 {
3844 let mut page = Page::a4();
3845 page.text()
3846 .set_font(Font::Helvetica, 12.0)
3847 .at(100.0, 700.0)
3848 .write(&format!("Page {page}", page = i + 1))
3849 .unwrap();
3850 document.add_page(page);
3851 }
3852
3853 let config = WriterConfig {
3854 use_xref_streams: true,
3855 pdf_version: "1.5".to_string(),
3856 compress_streams: true,
3857 };
3858 let mut writer = PdfWriter::with_config(&mut buffer, config);
3859 writer.write_document(&mut document).unwrap();
3860 }
3861
3862 #[test]
3863 fn test_write_pdf_header() {
3864 let mut buffer = Vec::new();
3865 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3866 writer.write_header().unwrap();
3867
3868 let content = String::from_utf8_lossy(&buffer);
3869 assert!(content.starts_with("%PDF-"));
3870 assert!(content.contains("\n%"));
3871 }
3872
3873 #[test]
3874 fn test_write_empty_document() {
3875 let mut buffer = Vec::new();
3876 let mut document = Document::new();
3877
3878 // Empty document should still generate valid PDF
3879 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3880 let result = writer.write_document(&mut document);
3881 assert!(result.is_ok());
3882
3883 let content = String::from_utf8_lossy(&buffer);
3884 assert!(content.starts_with("%PDF-"));
3885 assert!(content.contains("%%EOF"));
3886 }
3887
3888 // Note: The following tests were removed as they use methods that don't exist
3889 // in the current PdfWriter API (write_string, write_name, write_real, etc.)
3890 // These would need to be reimplemented using the actual available methods.
3891
3892 /*
3893 #[test]
3894 fn test_write_string_escaping() {
3895 let mut buffer = Vec::new();
3896 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3897
3898 // Test various string escaping scenarios
3899 writer.write_string(b"Normal text").unwrap();
3900 assert!(buffer.contains(&b'('[0]));
3901
3902 buffer.clear();
3903 writer.write_string(b"Text with (parentheses)").unwrap();
3904 let content = String::from_utf8_lossy(&buffer);
3905 assert!(content.contains("\\(") || content.contains("\\)"));
3906
3907 buffer.clear();
3908 writer.write_string(b"Text with \\backslash").unwrap();
3909 let content = String::from_utf8_lossy(&buffer);
3910 assert!(content.contains("\\\\"));
3911 }
3912
3913 #[test]
3914 fn test_write_name_escaping() {
3915 let mut buffer = Vec::new();
3916 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3917
3918 // Normal name
3919 writer.write_name("Type").unwrap();
3920 assert_eq!(String::from_utf8_lossy(&buffer), "/Type");
3921
3922 buffer.clear();
3923 writer.write_name("Name With Spaces").unwrap();
3924 let content = String::from_utf8_lossy(&buffer);
3925 assert!(content.starts_with("/"));
3926 assert!(content.contains("#20")); // Space encoded as #20
3927
3928 buffer.clear();
3929 writer.write_name("Special#Characters").unwrap();
3930 let content = String::from_utf8_lossy(&buffer);
3931 assert!(content.contains("#23")); // # encoded as #23
3932 }
3933
3934 #[test]
3935 fn test_write_real_number() {
3936 let mut buffer = Vec::new();
3937 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3938
3939 writer.write_real(3.14159).unwrap();
3940 assert_eq!(String::from_utf8_lossy(&buffer), "3.14159");
3941
3942 buffer.clear();
3943 writer.write_real(0.0).unwrap();
3944 assert_eq!(String::from_utf8_lossy(&buffer), "0");
3945
3946 buffer.clear();
3947 writer.write_real(-123.456).unwrap();
3948 assert_eq!(String::from_utf8_lossy(&buffer), "-123.456");
3949
3950 buffer.clear();
3951 writer.write_real(1000.0).unwrap();
3952 assert_eq!(String::from_utf8_lossy(&buffer), "1000");
3953 }
3954
3955 #[test]
3956 fn test_write_array() {
3957 let mut buffer = Vec::new();
3958 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3959
3960 let array = vec![
3961 PdfObject::Integer(1),
3962 PdfObject::Real(2.5),
3963 PdfObject::Name(PdfName::new("Test".to_string())),
3964 PdfObject::Boolean(true),
3965 PdfObject::Null,
3966 ];
3967
3968 writer.write_array(&array).unwrap();
3969 let content = String::from_utf8_lossy(&buffer);
3970
3971 assert!(content.starts_with("["));
3972 assert!(content.ends_with("]"));
3973 assert!(content.contains("1"));
3974 assert!(content.contains("2.5"));
3975 assert!(content.contains("/Test"));
3976 assert!(content.contains("true"));
3977 assert!(content.contains("null"));
3978 }
3979
3980 #[test]
3981 fn test_write_dictionary() {
3982 let mut buffer = Vec::new();
3983 let mut writer = PdfWriter::new_with_writer(&mut buffer);
3984
3985 let mut dict = HashMap::new();
3986 dict.insert(PdfName::new("Type".to_string()),
3987 PdfObject::Name(PdfName::new("Page".to_string())));
3988 dict.insert(PdfName::new("Count".to_string()),
3989 PdfObject::Integer(10));
3990 dict.insert(PdfName::new("Kids".to_string()),
3991 PdfObject::Array(vec![PdfObject::Reference(1, 0)]));
3992
3993 writer.write_dictionary(&dict).unwrap();
3994 let content = String::from_utf8_lossy(&buffer);
3995
3996 assert!(content.starts_with("<<"));
3997 assert!(content.ends_with(">>"));
3998 assert!(content.contains("/Type /Page"));
3999 assert!(content.contains("/Count 10"));
4000 assert!(content.contains("/Kids [1 0 R]"));
4001 }
4002
4003 #[test]
4004 fn test_write_stream() {
4005 let mut buffer = Vec::new();
4006 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4007
4008 let mut dict = HashMap::new();
4009 dict.insert(PdfName::new("Length".to_string()),
4010 PdfObject::Integer(20));
4011
4012 let data = b"This is stream data.";
4013 writer.write_stream(&dict, data).unwrap();
4014
4015 let content = String::from_utf8_lossy(&buffer);
4016 assert!(content.contains("<<"));
4017 assert!(content.contains("/Length 20"));
4018 assert!(content.contains(">>"));
4019 assert!(content.contains("stream\n"));
4020 assert!(content.contains("This is stream data."));
4021 assert!(content.contains("\nendstream"));
4022 }
4023
4024 #[test]
4025 fn test_write_indirect_object() {
4026 let mut buffer = Vec::new();
4027 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4028
4029 let obj = PdfObject::Dictionary({
4030 let mut dict = HashMap::new();
4031 dict.insert(PdfName::new("Type".to_string()),
4032 PdfObject::Name(PdfName::new("Catalog".to_string())));
4033 dict
4034 });
4035
4036 writer.write_indirect_object(1, 0, &obj).unwrap();
4037 let content = String::from_utf8_lossy(&buffer);
4038
4039 assert!(content.starts_with("1 0 obj"));
4040 assert!(content.contains("<<"));
4041 assert!(content.contains("/Type /Catalog"));
4042 assert!(content.contains(">>"));
4043 assert!(content.ends_with("endobj\n"));
4044 }
4045
4046 #[test]
4047 fn test_write_xref_entry() {
4048 let mut buffer = Vec::new();
4049 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4050
4051 writer.write_xref_entry(0, 65535, 'f').unwrap();
4052 assert_eq!(String::from_utf8_lossy(&buffer), "0000000000 65535 f \n");
4053
4054 buffer.clear();
4055 writer.write_xref_entry(123456, 0, 'n').unwrap();
4056 assert_eq!(String::from_utf8_lossy(&buffer), "0000123456 00000 n \n");
4057
4058 buffer.clear();
4059 writer.write_xref_entry(9999999999, 99, 'n').unwrap();
4060 assert_eq!(String::from_utf8_lossy(&buffer), "9999999999 00099 n \n");
4061 }
4062
4063 #[test]
4064 fn test_write_trailer() {
4065 let mut buffer = Vec::new();
4066 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4067
4068 let mut trailer_dict = HashMap::new();
4069 trailer_dict.insert(PdfName::new("Size".to_string()),
4070 PdfObject::Integer(10));
4071 trailer_dict.insert(PdfName::new("Root".to_string()),
4072 PdfObject::Reference(1, 0));
4073 trailer_dict.insert(PdfName::new("Info".to_string()),
4074 PdfObject::Reference(2, 0));
4075
4076 writer.write_trailer(&trailer_dict, 12345).unwrap();
4077 let content = String::from_utf8_lossy(&buffer);
4078
4079 assert!(content.starts_with("trailer\n"));
4080 assert!(content.contains("<<"));
4081 assert!(content.contains("/Size 10"));
4082 assert!(content.contains("/Root 1 0 R"));
4083 assert!(content.contains("/Info 2 0 R"));
4084 assert!(content.contains(">>"));
4085 assert!(content.contains("startxref\n12345\n%%EOF"));
4086 }
4087
4088 #[test]
4089 fn test_compress_stream_data() {
4090 let mut writer = PdfWriter::new(&mut Vec::new());
4091
4092 let data = b"This is some text that should be compressed. It contains repeated patterns patterns patterns.";
4093 let compressed = writer.compress_stream(data).unwrap();
4094
4095 // Compressed data should have compression header
4096 assert!(compressed.len() > 0);
4097
4098 // Decompress to verify
4099 use flate2::read::ZlibDecoder;
4100 use std::io::Read;
4101 let mut decoder = ZlibDecoder::new(&compressed[..]);
4102 let mut decompressed = Vec::new();
4103 decoder.read_to_end(&mut decompressed).unwrap();
4104
4105 assert_eq!(decompressed, data);
4106 }
4107
4108 #[test]
4109 fn test_write_pages_tree() {
4110 let mut buffer = Vec::new();
4111 let mut document = Document::new();
4112
4113 // Add multiple pages with different sizes
4114 document.add_page(Page::a4());
4115 document.add_page(Page::a3());
4116 document.add_page(Page::letter());
4117
4118 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4119 writer.write_document(&mut document).unwrap();
4120
4121 let content = String::from_utf8_lossy(&buffer);
4122
4123 // Should have pages object
4124 assert!(content.contains("/Type /Pages"));
4125 assert!(content.contains("/Count 3"));
4126 assert!(content.contains("/Kids ["));
4127
4128 // Should have individual page objects
4129 assert!(content.contains("/Type /Page"));
4130 assert!(content.contains("/Parent "));
4131 assert!(content.contains("/MediaBox ["));
4132 }
4133
4134 #[test]
4135 fn test_write_font_resources() {
4136 let mut buffer = Vec::new();
4137 let mut document = Document::new();
4138
4139 let mut page = Page::a4();
4140 page.text()
4141 .set_font(Font::Helvetica, 12.0)
4142 .at(100.0, 700.0)
4143 .write("Helvetica")
4144 .unwrap();
4145 page.text()
4146 .set_font(Font::Times, 14.0)
4147 .at(100.0, 680.0)
4148 .write("Times")
4149 .unwrap();
4150 page.text()
4151 .set_font(Font::Courier, 10.0)
4152 .at(100.0, 660.0)
4153 .write("Courier")
4154 .unwrap();
4155
4156 document.add_page(page);
4157
4158 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4159 writer.write_document(&mut document).unwrap();
4160
4161 let content = String::from_utf8_lossy(&buffer);
4162
4163 // Should have font resources
4164 assert!(content.contains("/Font <<"));
4165 assert!(content.contains("/Type /Font"));
4166 assert!(content.contains("/Subtype /Type1"));
4167 assert!(content.contains("/BaseFont /Helvetica"));
4168 assert!(content.contains("/BaseFont /Times-Roman"));
4169 assert!(content.contains("/BaseFont /Courier"));
4170 }
4171
4172 #[test]
4173 fn test_write_image_xobject() {
4174 let mut buffer = Vec::new();
4175 let mut document = Document::new();
4176
4177 let mut page = Page::a4();
4178 // Simulate adding an image (would need actual image data in real usage)
4179 // This test verifies the structure is written correctly
4180
4181 document.add_page(page);
4182
4183 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4184 writer.write_document(&mut document).unwrap();
4185
4186 let content = String::from_utf8_lossy(&buffer);
4187
4188 // Basic structure should be present
4189 assert!(content.contains("/Resources"));
4190 }
4191
4192 #[test]
4193 fn test_write_document_with_metadata() {
4194 let mut buffer = Vec::new();
4195 let mut document = Document::new();
4196
4197 document.set_title("Test Document");
4198 document.set_author("Test Author");
4199 document.set_subject("Test Subject");
4200 document.set_keywords(vec!["test".to_string(), "pdf".to_string()]);
4201 document.set_creator("Test Creator");
4202 document.set_producer("oxidize-pdf");
4203
4204 document.add_page(Page::a4());
4205
4206 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4207 writer.write_document(&mut document).unwrap();
4208
4209 let content = String::from_utf8_lossy(&buffer);
4210
4211 // Should have info dictionary
4212 assert!(content.contains("/Title (Test Document)"));
4213 assert!(content.contains("/Author (Test Author)"));
4214 assert!(content.contains("/Subject (Test Subject)"));
4215 assert!(content.contains("/Keywords (test, pdf)"));
4216 assert!(content.contains("/Creator (Test Creator)"));
4217 assert!(content.contains("/Producer (oxidize-pdf)"));
4218 assert!(content.contains("/CreationDate"));
4219 assert!(content.contains("/ModDate"));
4220 }
4221
4222 #[test]
4223 fn test_write_cross_reference_stream() {
4224 let mut buffer = Vec::new();
4225 let config = WriterConfig {
4226 use_xref_streams: true,
4227 pdf_version: "1.5".to_string(),
4228 compress_streams: true,
4229 };
4230
4231 let mut writer = PdfWriter::with_config(&mut buffer, config);
4232 let mut document = Document::new();
4233 document.add_page(Page::a4());
4234
4235 writer.write_document(&mut document).unwrap();
4236
4237 let content = buffer.clone();
4238
4239 // Should contain compressed xref stream
4240 let content_str = String::from_utf8_lossy(&content);
4241 assert!(content_str.contains("/Type /XRef"));
4242 assert!(content_str.contains("/Filter /FlateDecode"));
4243 assert!(content_str.contains("/W ["));
4244 assert!(content_str.contains("/Index ["));
4245 }
4246
4247 #[test]
4248 fn test_write_linearized_hint() {
4249 // Test placeholder for linearized PDF support
4250 let mut buffer = Vec::new();
4251 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4252 let mut document = Document::new();
4253
4254 document.add_page(Page::a4());
4255 writer.write_document(&mut document).unwrap();
4256
4257 // Linearization would add specific markers
4258 let content = String::from_utf8_lossy(&buffer);
4259 assert!(content.starts_with("%PDF-"));
4260 }
4261
4262 #[test]
4263 fn test_write_encrypted_document() {
4264 // Test placeholder for encryption support
4265 let mut buffer = Vec::new();
4266 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4267 let mut document = Document::new();
4268
4269 document.add_page(Page::a4());
4270 writer.write_document(&mut document).unwrap();
4271
4272 let content = String::from_utf8_lossy(&buffer);
4273 // Would contain /Encrypt dictionary if implemented
4274 assert!(!content.contains("/Encrypt"));
4275 }
4276
4277 #[test]
4278 fn test_object_number_allocation() {
4279 let mut writer = PdfWriter::new(&mut Vec::new());
4280
4281 let obj1 = writer.allocate_object_number();
4282 let obj2 = writer.allocate_object_number();
4283 let obj3 = writer.allocate_object_number();
4284
4285 assert_eq!(obj1, 1);
4286 assert_eq!(obj2, 2);
4287 assert_eq!(obj3, 3);
4288
4289 // Object numbers should be sequential
4290 assert_eq!(obj2 - obj1, 1);
4291 assert_eq!(obj3 - obj2, 1);
4292 }
4293
4294 #[test]
4295 fn test_write_page_content_stream() {
4296 let mut buffer = Vec::new();
4297 let mut document = Document::new();
4298
4299 let mut page = Page::a4();
4300 page.text()
4301 .set_font(Font::Helvetica, 24.0)
4302 .at(100.0, 700.0)
4303 .write("Hello, PDF!")
4304 .unwrap();
4305
4306 page.graphics()
4307 .move_to(100.0, 600.0)
4308 .line_to(500.0, 600.0)
4309 .stroke();
4310
4311 document.add_page(page);
4312
4313 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4314 writer.write_document(&mut document).unwrap();
4315
4316 let content = String::from_utf8_lossy(&buffer);
4317
4318 // Should have content stream with text and graphics operations
4319 assert!(content.contains("BT")); // Begin text
4320 assert!(content.contains("ET")); // End text
4321 assert!(content.contains("Tf")); // Set font
4322 assert!(content.contains("Td")); // Position text
4323 assert!(content.contains("Tj")); // Show text
4324 assert!(content.contains(" m ")); // Move to
4325 assert!(content.contains(" l ")); // Line to
4326 assert!(content.contains(" S")); // Stroke
4327 }
4328 }
4329
4330 #[test]
4331 fn test_writer_config_default() {
4332 let config = WriterConfig::default();
4333 assert!(!config.use_xref_streams);
4334 assert_eq!(config.pdf_version, "1.7");
4335 assert!(config.compress_streams);
4336 }
4337
4338 #[test]
4339 fn test_writer_config_custom() {
4340 let config = WriterConfig {
4341 use_xref_streams: true,
4342 pdf_version: "2.0".to_string(),
4343 compress_streams: false,
4344 };
4345 assert!(config.use_xref_streams);
4346 assert_eq!(config.pdf_version, "2.0");
4347 assert!(!config.compress_streams);
4348 }
4349
4350 #[test]
4351 fn test_pdf_writer_new() {
4352 let buffer = Vec::new();
4353 let writer = PdfWriter::new_with_writer(buffer);
4354 assert_eq!(writer.current_position, 0);
4355 assert_eq!(writer.next_object_id, 1);
4356 assert!(writer.catalog_id.is_none());
4357 assert!(writer.pages_id.is_none());
4358 assert!(writer.info_id.is_none());
4359 }
4360
4361 #[test]
4362 fn test_pdf_writer_with_config() {
4363 let config = WriterConfig {
4364 use_xref_streams: true,
4365 pdf_version: "1.5".to_string(),
4366 compress_streams: false,
4367 };
4368 let buffer = Vec::new();
4369 let writer = PdfWriter::with_config(buffer, config.clone());
4370 assert_eq!(writer.config.pdf_version, "1.5");
4371 assert!(writer.config.use_xref_streams);
4372 assert!(!writer.config.compress_streams);
4373 }
4374
4375 #[test]
4376 fn test_allocate_object_id() {
4377 let buffer = Vec::new();
4378 let mut writer = PdfWriter::new_with_writer(buffer);
4379
4380 let id1 = writer.allocate_object_id();
4381 assert_eq!(id1, ObjectId::new(1, 0));
4382
4383 let id2 = writer.allocate_object_id();
4384 assert_eq!(id2, ObjectId::new(2, 0));
4385
4386 let id3 = writer.allocate_object_id();
4387 assert_eq!(id3, ObjectId::new(3, 0));
4388
4389 assert_eq!(writer.next_object_id, 4);
4390 }
4391
4392 #[test]
4393 fn test_write_header_version() {
4394 let mut buffer = Vec::new();
4395 {
4396 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4397 writer.write_header().unwrap();
4398 }
4399
4400 let content = String::from_utf8_lossy(&buffer);
4401 assert!(content.starts_with("%PDF-1.7\n"));
4402 // Binary comment should be present
4403 assert!(buffer.len() > 10);
4404 assert_eq!(buffer[9], b'%');
4405 }
4406
4407 #[test]
4408 fn test_write_header_custom_version() {
4409 let mut buffer = Vec::new();
4410 {
4411 let config = WriterConfig {
4412 pdf_version: "2.0".to_string(),
4413 ..Default::default()
4414 };
4415 let mut writer = PdfWriter::with_config(&mut buffer, config);
4416 writer.write_header().unwrap();
4417 }
4418
4419 let content = String::from_utf8_lossy(&buffer);
4420 assert!(content.starts_with("%PDF-2.0\n"));
4421 }
4422
4423 #[test]
4424 fn test_write_object_integer() {
4425 let mut buffer = Vec::new();
4426 {
4427 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4428 let obj_id = ObjectId::new(1, 0);
4429 let obj = Object::Integer(42);
4430 writer.write_object(obj_id, obj).unwrap();
4431 }
4432
4433 let content = String::from_utf8_lossy(&buffer);
4434 assert!(content.contains("1 0 obj"));
4435 assert!(content.contains("42"));
4436 assert!(content.contains("endobj"));
4437 }
4438
4439 #[test]
4440 fn test_write_dictionary_object() {
4441 let mut buffer = Vec::new();
4442 {
4443 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4444 let obj_id = ObjectId::new(1, 0);
4445
4446 let mut dict = Dictionary::new();
4447 dict.set("Type", Object::Name("Test".to_string()));
4448 dict.set("Count", Object::Integer(5));
4449
4450 writer
4451 .write_object(obj_id, Object::Dictionary(dict))
4452 .unwrap();
4453 }
4454
4455 let content = String::from_utf8_lossy(&buffer);
4456 assert!(content.contains("1 0 obj"));
4457 assert!(content.contains("/Type /Test"));
4458 assert!(content.contains("/Count 5"));
4459 assert!(content.contains("endobj"));
4460 }
4461
4462 #[test]
4463 fn test_write_array_object() {
4464 let mut buffer = Vec::new();
4465 {
4466 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4467 let obj_id = ObjectId::new(1, 0);
4468
4469 let array = vec![Object::Integer(1), Object::Integer(2), Object::Integer(3)];
4470
4471 writer.write_object(obj_id, Object::Array(array)).unwrap();
4472 }
4473
4474 let content = String::from_utf8_lossy(&buffer);
4475 assert!(content.contains("1 0 obj"));
4476 assert!(content.contains("[1 2 3]"));
4477 assert!(content.contains("endobj"));
4478 }
4479
4480 #[test]
4481 fn test_write_string_object() {
4482 let mut buffer = Vec::new();
4483 {
4484 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4485 let obj_id = ObjectId::new(1, 0);
4486
4487 writer
4488 .write_object(obj_id, Object::String("Hello PDF".to_string()))
4489 .unwrap();
4490 }
4491
4492 let content = String::from_utf8_lossy(&buffer);
4493 assert!(content.contains("1 0 obj"));
4494 assert!(content.contains("(Hello PDF)"));
4495 assert!(content.contains("endobj"));
4496 }
4497
4498 #[test]
4499 fn test_write_reference_object() {
4500 let mut buffer = Vec::new();
4501 {
4502 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4503
4504 let mut dict = Dictionary::new();
4505 dict.set("Parent", Object::Reference(ObjectId::new(2, 0)));
4506
4507 writer
4508 .write_object(ObjectId::new(1, 0), Object::Dictionary(dict))
4509 .unwrap();
4510 }
4511
4512 let content = String::from_utf8_lossy(&buffer);
4513 assert!(content.contains("/Parent 2 0 R"));
4514 }
4515
4516 // test_write_stream_object removed due to API differences
4517
4518 #[test]
4519 fn test_write_boolean_objects() {
4520 let mut buffer = Vec::new();
4521 {
4522 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4523
4524 writer
4525 .write_object(ObjectId::new(1, 0), Object::Boolean(true))
4526 .unwrap();
4527 writer
4528 .write_object(ObjectId::new(2, 0), Object::Boolean(false))
4529 .unwrap();
4530 }
4531
4532 let content = String::from_utf8_lossy(&buffer);
4533 assert!(content.contains("1 0 obj"));
4534 assert!(content.contains("true"));
4535 assert!(content.contains("2 0 obj"));
4536 assert!(content.contains("false"));
4537 }
4538
4539 #[test]
4540 fn test_write_real_object() {
4541 let mut buffer = Vec::new();
4542 {
4543 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4544
4545 writer
4546 .write_object(ObjectId::new(1, 0), Object::Real(3.14159))
4547 .unwrap();
4548 }
4549
4550 let content = String::from_utf8_lossy(&buffer);
4551 assert!(content.contains("1 0 obj"));
4552 assert!(content.contains("3.14159"));
4553 }
4554
4555 #[test]
4556 fn test_write_null_object() {
4557 let mut buffer = Vec::new();
4558 {
4559 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4560
4561 writer
4562 .write_object(ObjectId::new(1, 0), Object::Null)
4563 .unwrap();
4564 }
4565
4566 let content = String::from_utf8_lossy(&buffer);
4567 assert!(content.contains("1 0 obj"));
4568 assert!(content.contains("null"));
4569 }
4570
4571 #[test]
4572 fn test_write_nested_structures() {
4573 let mut buffer = Vec::new();
4574 {
4575 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4576
4577 let mut inner_dict = Dictionary::new();
4578 inner_dict.set("Key", Object::String("Value".to_string()));
4579
4580 let mut outer_dict = Dictionary::new();
4581 outer_dict.set("Inner", Object::Dictionary(inner_dict));
4582 outer_dict.set(
4583 "Array",
4584 Object::Array(vec![Object::Integer(1), Object::Name("Test".to_string())]),
4585 );
4586
4587 writer
4588 .write_object(ObjectId::new(1, 0), Object::Dictionary(outer_dict))
4589 .unwrap();
4590 }
4591
4592 let content = String::from_utf8_lossy(&buffer);
4593 assert!(content.contains("/Inner <<"));
4594 assert!(content.contains("/Key (Value)"));
4595 assert!(content.contains("/Array [1 /Test]"));
4596 }
4597
4598 #[test]
4599 fn test_xref_positions_tracking() {
4600 let mut buffer = Vec::new();
4601 {
4602 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4603
4604 let id1 = ObjectId::new(1, 0);
4605 let id2 = ObjectId::new(2, 0);
4606
4607 writer.write_object(id1, Object::Integer(1)).unwrap();
4608 let pos1 = writer.xref_positions.get(&id1).copied();
4609 assert!(pos1.is_some());
4610
4611 writer.write_object(id2, Object::Integer(2)).unwrap();
4612 let pos2 = writer.xref_positions.get(&id2).copied();
4613 assert!(pos2.is_some());
4614
4615 // Position 2 should be after position 1
4616 assert!(pos2.unwrap() > pos1.unwrap());
4617 }
4618 }
4619
4620 #[test]
4621 fn test_write_info_basic() {
4622 let mut buffer = Vec::new();
4623 {
4624 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4625 writer.info_id = Some(ObjectId::new(3, 0));
4626
4627 let mut document = Document::new();
4628 document.set_title("Test Document");
4629 document.set_author("Test Author");
4630
4631 writer.write_info(&document).unwrap();
4632 }
4633
4634 let content = String::from_utf8_lossy(&buffer);
4635 assert!(content.contains("3 0 obj"));
4636 assert!(content.contains("/Title (Test Document)"));
4637 assert!(content.contains("/Author (Test Author)"));
4638 assert!(content.contains("/Producer"));
4639 assert!(content.contains("/CreationDate"));
4640 }
4641
4642 #[test]
4643 fn test_write_info_with_all_metadata() {
4644 let mut buffer = Vec::new();
4645 {
4646 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4647 writer.info_id = Some(ObjectId::new(3, 0));
4648
4649 let mut document = Document::new();
4650 document.set_title("Title");
4651 document.set_author("Author");
4652 document.set_subject("Subject");
4653 document.set_keywords("keyword1, keyword2");
4654 document.set_creator("Creator");
4655
4656 writer.write_info(&document).unwrap();
4657 }
4658
4659 let content = String::from_utf8_lossy(&buffer);
4660 assert!(content.contains("/Title (Title)"));
4661 assert!(content.contains("/Author (Author)"));
4662 assert!(content.contains("/Subject (Subject)"));
4663 assert!(content.contains("/Keywords (keyword1, keyword2)"));
4664 assert!(content.contains("/Creator (Creator)"));
4665 }
4666
4667 #[test]
4668 fn test_write_catalog_basic() {
4669 let mut buffer = Vec::new();
4670 {
4671 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4672 writer.catalog_id = Some(ObjectId::new(1, 0));
4673 writer.pages_id = Some(ObjectId::new(2, 0));
4674
4675 let mut document = Document::new();
4676 writer.write_catalog(&mut document).unwrap();
4677 }
4678
4679 let content = String::from_utf8_lossy(&buffer);
4680 assert!(content.contains("1 0 obj"));
4681 assert!(content.contains("/Type /Catalog"));
4682 assert!(content.contains("/Pages 2 0 R"));
4683 }
4684
4685 #[test]
4686 fn test_write_catalog_with_outline() {
4687 let mut buffer = Vec::new();
4688 {
4689 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4690 writer.catalog_id = Some(ObjectId::new(1, 0));
4691 writer.pages_id = Some(ObjectId::new(2, 0));
4692
4693 let mut document = Document::new();
4694 let mut outline = crate::structure::OutlineTree::new();
4695 outline.add_item(crate::structure::OutlineItem::new("Chapter 1"));
4696 document.outline = Some(outline);
4697
4698 writer.write_catalog(&mut document).unwrap();
4699 }
4700
4701 let content = String::from_utf8_lossy(&buffer);
4702 assert!(content.contains("/Type /Catalog"));
4703 assert!(content.contains("/Outlines"));
4704 }
4705
4706 #[test]
4707 fn test_write_xref_basic() {
4708 let mut buffer = Vec::new();
4709 {
4710 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4711
4712 // Add some objects to xref
4713 writer.xref_positions.insert(ObjectId::new(0, 65535), 0);
4714 writer.xref_positions.insert(ObjectId::new(1, 0), 15);
4715 writer.xref_positions.insert(ObjectId::new(2, 0), 100);
4716
4717 writer.write_xref().unwrap();
4718 }
4719
4720 let content = String::from_utf8_lossy(&buffer);
4721 assert!(content.contains("xref"));
4722 assert!(content.contains("0 3")); // 3 objects starting at 0
4723 assert!(content.contains("0000000000 65535 f"));
4724 assert!(content.contains("0000000015 00000 n"));
4725 assert!(content.contains("0000000100 00000 n"));
4726 }
4727
4728 #[test]
4729 fn test_write_trailer_complete() {
4730 let mut buffer = Vec::new();
4731 {
4732 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4733 writer.catalog_id = Some(ObjectId::new(1, 0));
4734 writer.info_id = Some(ObjectId::new(2, 0));
4735
4736 // Add some objects
4737 writer.xref_positions.insert(ObjectId::new(0, 65535), 0);
4738 writer.xref_positions.insert(ObjectId::new(1, 0), 15);
4739 writer.xref_positions.insert(ObjectId::new(2, 0), 100);
4740
4741 writer.write_trailer(1000).unwrap();
4742 }
4743
4744 let content = String::from_utf8_lossy(&buffer);
4745 assert!(content.contains("trailer"));
4746 assert!(content.contains("/Size 3"));
4747 assert!(content.contains("/Root 1 0 R"));
4748 assert!(content.contains("/Info 2 0 R"));
4749 assert!(content.contains("startxref"));
4750 assert!(content.contains("1000"));
4751 assert!(content.contains("%%EOF"));
4752 }
4753
4754 // escape_string test removed - method is private
4755
4756 // format_date test removed - method is private
4757
4758 #[test]
4759 fn test_write_bytes_tracking() {
4760 let mut buffer = Vec::new();
4761 {
4762 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4763
4764 let data = b"Test data";
4765 writer.write_bytes(data).unwrap();
4766 assert_eq!(writer.current_position, data.len() as u64);
4767
4768 writer.write_bytes(b" more").unwrap();
4769 assert_eq!(writer.current_position, (data.len() + 5) as u64);
4770 }
4771
4772 assert_eq!(buffer, b"Test data more");
4773 }
4774
4775 #[test]
4776 fn test_complete_document_write() {
4777 let mut buffer = Vec::new();
4778 {
4779 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4780 let mut document = Document::new();
4781
4782 // Add a page
4783 let page = crate::page::Page::new(612.0, 792.0);
4784 document.add_page(page);
4785
4786 // Set metadata
4787 document.set_title("Test PDF");
4788 document.set_author("Test Suite");
4789
4790 // Write the document
4791 writer.write_document(&mut document).unwrap();
4792 }
4793
4794 let content = String::from_utf8_lossy(&buffer);
4795
4796 // Check PDF structure
4797 assert!(content.starts_with("%PDF-"));
4798 assert!(content.contains("/Type /Catalog"));
4799 assert!(content.contains("/Type /Pages"));
4800 assert!(content.contains("/Type /Page"));
4801 assert!(content.contains("/Title (Test PDF)"));
4802 assert!(content.contains("/Author (Test Suite)"));
4803 assert!(content.contains("xref") || content.contains("/Type /XRef"));
4804 assert!(content.ends_with("%%EOF\n"));
4805 }
4806
4807 // ========== NEW COMPREHENSIVE TESTS ==========
4808
4809 #[test]
4810 fn test_writer_resource_cleanup() {
4811 let mut buffer = Vec::new();
4812 {
4813 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4814
4815 // Allocate many object IDs to test cleanup
4816 let ids: Vec<_> = (0..100).map(|_| writer.allocate_object_id()).collect();
4817
4818 // Verify all IDs are unique and sequential
4819 for (i, &id) in ids.iter().enumerate() {
4820 assert_eq!(id, (i + 1) as u32);
4821 }
4822
4823 // Test that we can still allocate after cleanup
4824 let next_id = writer.allocate_object_id();
4825 assert_eq!(next_id, 101);
4826 }
4827 // Writer should be properly dropped here
4828 }
4829
4830 #[test]
4831 fn test_writer_concurrent_safety() {
4832 use std::sync::{Arc, Mutex};
4833 use std::thread;
4834
4835 let buffer = Arc::new(Mutex::new(Vec::new()));
4836 let buffer_clone = Arc::clone(&buffer);
4837
4838 let handle = thread::spawn(move || {
4839 let mut buf = buffer_clone.lock().unwrap();
4840 let mut writer = PdfWriter::new_with_writer(&mut *buf);
4841
4842 // Simulate concurrent operations
4843 for i in 0..10 {
4844 let id = writer.allocate_object_id();
4845 assert_eq!(id, (i + 1) as u32);
4846 }
4847
4848 // Write some data
4849 writer.write_bytes(b"Thread test").unwrap();
4850 });
4851
4852 handle.join().unwrap();
4853
4854 let buffer = buffer.lock().unwrap();
4855 assert_eq!(&*buffer, b"Thread test");
4856 }
4857
4858 #[test]
4859 fn test_writer_memory_efficiency() {
4860 let mut buffer = Vec::new();
4861 {
4862 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4863
4864 // Test that large objects don't cause excessive memory usage
4865 let large_data = vec![b'X'; 10_000];
4866 writer.write_bytes(&large_data).unwrap();
4867
4868 // Verify position tracking is accurate
4869 assert_eq!(writer.current_position, 10_000);
4870
4871 // Write more data
4872 writer.write_bytes(b"END").unwrap();
4873 assert_eq!(writer.current_position, 10_003);
4874 }
4875
4876 // Verify buffer contents
4877 assert_eq!(buffer.len(), 10_003);
4878 assert_eq!(&buffer[10_000..], b"END");
4879 }
4880
4881 #[test]
4882 fn test_writer_edge_case_handling() {
4883 let mut buffer = Vec::new();
4884 {
4885 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4886
4887 // Test empty writes
4888 writer.write_bytes(b"").unwrap();
4889 assert_eq!(writer.current_position, 0);
4890
4891 // Test single byte writes
4892 writer.write_bytes(b"A").unwrap();
4893 assert_eq!(writer.current_position, 1);
4894
4895 // Test null bytes
4896 writer.write_bytes(b"\0").unwrap();
4897 assert_eq!(writer.current_position, 2);
4898
4899 // Test high ASCII values
4900 writer.write_bytes(b"\xFF\xFE").unwrap();
4901 assert_eq!(writer.current_position, 4);
4902 }
4903
4904 assert_eq!(buffer, vec![b'A', 0, 0xFF, 0xFE]);
4905 }
4906
4907 #[test]
4908 fn test_writer_cross_reference_consistency() {
4909 let mut buffer = Vec::new();
4910 {
4911 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4912 let mut document = Document::new();
4913
4914 // Create a document with multiple objects
4915 for i in 0..5 {
4916 let page = crate::page::Page::new(612.0, 792.0);
4917 document.add_page(page);
4918 }
4919
4920 document.set_title(&format!("Test Document {}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()));
4921
4922 writer.write_document(&mut document).unwrap();
4923 }
4924
4925 let content = String::from_utf8_lossy(&buffer);
4926
4927 // Verify cross-reference structure
4928 if content.contains("xref") {
4929 // Traditional xref table
4930 assert!(content.contains("0000000000 65535 f"));
4931 assert!(content.contains("0000000000 00000 n") || content.contains("trailer"));
4932 } else {
4933 // XRef stream
4934 assert!(content.contains("/Type /XRef"));
4935 }
4936
4937 // Should have proper trailer
4938 assert!(content.contains("/Size"));
4939 assert!(content.contains("/Root"));
4940 }
4941
4942 #[test]
4943 fn test_writer_config_validation() {
4944 let mut config = WriterConfig::default();
4945 assert_eq!(config.pdf_version, "1.7");
4946 assert!(!config.use_xref_streams);
4947 assert!(config.compress_streams);
4948
4949 // Test custom configuration
4950 config.pdf_version = "1.4".to_string();
4951 config.use_xref_streams = true;
4952 config.compress_streams = false;
4953
4954 let buffer = Vec::new();
4955 let writer = PdfWriter::with_config(buffer, config.clone());
4956 assert_eq!(writer.config.pdf_version, "1.4");
4957 assert!(writer.config.use_xref_streams);
4958 assert!(!writer.config.compress_streams);
4959 }
4960
4961 #[test]
4962 fn test_pdf_version_validation() {
4963 let test_versions = ["1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "2.0"];
4964
4965 for version in &test_versions {
4966 let mut config = WriterConfig::default();
4967 config.pdf_version = version.to_string();
4968
4969 let mut buffer = Vec::new();
4970 {
4971 let mut writer = PdfWriter::with_config(&mut buffer, config);
4972 writer.write_header().unwrap();
4973 }
4974
4975 let content = String::from_utf8_lossy(&buffer);
4976 assert!(content.starts_with(&format!("%PDF-{}", version)));
4977 }
4978 }
4979
4980 #[test]
4981 fn test_object_id_allocation_sequence() {
4982 let mut buffer = Vec::new();
4983 let mut writer = PdfWriter::new_with_writer(&mut buffer);
4984
4985 // Test sequential allocation
4986 let id1 = writer.allocate_object_id();
4987 let id2 = writer.allocate_object_id();
4988 let id3 = writer.allocate_object_id();
4989
4990 assert_eq!(id1.number(), 1);
4991 assert_eq!(id2.number(), 2);
4992 assert_eq!(id3.number(), 3);
4993 assert_eq!(id1.generation(), 0);
4994 assert_eq!(id2.generation(), 0);
4995 assert_eq!(id3.generation(), 0);
4996 }
4997
4998 #[test]
4999 fn test_xref_position_tracking() {
5000 let mut buffer = Vec::new();
5001 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5002
5003 let id1 = ObjectId::new(1, 0);
5004 let id2 = ObjectId::new(2, 0);
5005
5006 // Write first object
5007 writer.write_header().unwrap();
5008 let pos1 = writer.current_position;
5009 writer.write_object(id1, Object::Integer(42)).unwrap();
5010
5011 // Write second object
5012 let pos2 = writer.current_position;
5013 writer.write_object(id2, Object::String("test".to_string())).unwrap();
5014
5015 // Verify positions are tracked
5016 assert_eq!(writer.xref_positions.get(&id1), Some(&pos1));
5017 assert_eq!(writer.xref_positions.get(&id2), Some(&pos2));
5018 assert!(pos2 > pos1);
5019 }
5020
5021 #[test]
5022 fn test_binary_header_generation() {
5023 let mut buffer = Vec::new();
5024 {
5025 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5026 writer.write_header().unwrap();
5027 }
5028
5029 // Check binary comment is present
5030 assert!(buffer.len() > 10);
5031 assert_eq!(&buffer[0..5], b"%PDF-");
5032
5033 // Find the binary comment line
5034 let content = buffer.as_slice();
5035 let mut found_binary = false;
5036 for i in 0..content.len() - 5 {
5037 if content[i] == b'%' && content[i + 1] == 0xE2 {
5038 found_binary = true;
5039 break;
5040 }
5041 }
5042 assert!(found_binary, "Binary comment marker not found");
5043 }
5044
5045 #[test]
5046 fn test_large_object_handling() {
5047 let mut buffer = Vec::new();
5048 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5049
5050 // Create a large string object
5051 let large_string = "A".repeat(10000);
5052 let id = ObjectId::new(1, 0);
5053
5054 writer.write_object(id, Object::String(large_string.clone())).unwrap();
5055
5056 let content = String::from_utf8_lossy(&buffer);
5057 assert!(content.contains("1 0 obj"));
5058 assert!(content.contains(&large_string));
5059 assert!(content.contains("endobj"));
5060 }
5061
5062 #[test]
5063 fn test_unicode_string_encoding() {
5064 let mut buffer = Vec::new();
5065 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5066
5067 let unicode_strings = vec![
5068 "Hello 世界",
5069 "café",
5070 "🎯 emoji test",
5071 "Ω α β γ δ",
5072 "\u{FEFF}BOM test",
5073 ];
5074
5075 for (i, s) in unicode_strings.iter().enumerate() {
5076 let id = ObjectId::new((i + 1) as u32, 0);
5077 writer.write_object(id, Object::String(s.to_string())).unwrap();
5078 }
5079
5080 let content = String::from_utf8_lossy(&buffer);
5081 // Verify objects are written properly
5082 assert!(content.contains("1 0 obj"));
5083 assert!(content.contains("2 0 obj"));
5084 }
5085
5086 #[test]
5087 fn test_special_characters_in_names() {
5088 let mut buffer = Vec::new();
5089 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5090
5091 let special_names = vec![
5092 "Name With Spaces",
5093 "Name#With#Hash",
5094 "Name/With/Slash",
5095 "Name(With)Parens",
5096 "Name[With]Brackets",
5097 "",
5098 ];
5099
5100 for (i, name) in special_names.iter().enumerate() {
5101 let id = ObjectId::new((i + 1) as u32, 0);
5102 writer.write_object(id, Object::Name(name.to_string())).unwrap();
5103 }
5104
5105 let content = String::from_utf8_lossy(&buffer);
5106 // Names should be properly escaped
5107 assert!(content.contains("Name#20With#20Spaces") || content.contains("Name With Spaces"));
5108 }
5109
5110 #[test]
5111 fn test_deep_nested_structures() {
5112 let mut buffer = Vec::new();
5113 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5114
5115 // Create deeply nested dictionary
5116 let mut current = Dictionary::new();
5117 current.set("Level", Object::Integer(0));
5118
5119 for i in 1..=10 {
5120 let mut next = Dictionary::new();
5121 next.set("Level", Object::Integer(i));
5122 next.set("Parent", Object::Dictionary(current));
5123 current = next;
5124 }
5125
5126 let id = ObjectId::new(1, 0);
5127 writer.write_object(id, Object::Dictionary(current)).unwrap();
5128
5129 let content = String::from_utf8_lossy(&buffer);
5130 assert!(content.contains("1 0 obj"));
5131 assert!(content.contains("/Level"));
5132 }
5133
5134 #[test]
5135 fn test_xref_stream_vs_table_consistency() {
5136 let mut document = Document::new();
5137 document.add_page(crate::page::Page::new(612.0, 792.0));
5138
5139 // Test with traditional xref table
5140 let mut buffer_table = Vec::new();
5141 {
5142 let config = WriterConfig {
5143 use_xref_streams: false,
5144 ..Default::default()
5145 };
5146 let mut writer = PdfWriter::with_config(&mut buffer_table, config);
5147 writer.write_document(&mut document.clone()).unwrap();
5148 }
5149
5150 // Test with xref stream
5151 let mut buffer_stream = Vec::new();
5152 {
5153 let config = WriterConfig {
5154 use_xref_streams: true,
5155 ..Default::default()
5156 };
5157 let mut writer = PdfWriter::with_config(&mut buffer_stream, config);
5158 writer.write_document(&mut document.clone()).unwrap();
5159 }
5160
5161 let content_table = String::from_utf8_lossy(&buffer_table);
5162 let content_stream = String::from_utf8_lossy(&buffer_stream);
5163
5164 // Both should be valid PDFs
5165 assert!(content_table.starts_with("%PDF-"));
5166 assert!(content_stream.starts_with("%PDF-"));
5167
5168 // Traditional should have xref table
5169 assert!(content_table.contains("xref"));
5170 assert!(content_table.contains("trailer"));
5171
5172 // Stream version should have XRef object
5173 assert!(content_stream.contains("/Type /XRef") || content_stream.contains("xref"));
5174 }
5175
5176 #[test]
5177 fn test_compression_flag_effects() {
5178 let mut document = Document::new();
5179 let mut page = crate::page::Page::new(612.0, 792.0);
5180 let mut gc = page.graphics();
5181 gc.show_text("Test content with compression").unwrap();
5182 document.add_page(page);
5183
5184 // Test with compression enabled
5185 let mut buffer_compressed = Vec::new();
5186 {
5187 let config = WriterConfig {
5188 compress_streams: true,
5189 ..Default::default()
5190 };
5191 let mut writer = PdfWriter::with_config(&mut buffer_compressed, config);
5192 writer.write_document(&mut document.clone()).unwrap();
5193 }
5194
5195 // Test with compression disabled
5196 let mut buffer_uncompressed = Vec::new();
5197 {
5198 let config = WriterConfig {
5199 compress_streams: false,
5200 ..Default::default()
5201 };
5202 let mut writer = PdfWriter::with_config(&mut buffer_uncompressed, config);
5203 writer.write_document(&mut document.clone()).unwrap();
5204 }
5205
5206 // Compressed version should be smaller (usually)
5207 // Note: For small content, overhead might make it larger
5208 assert!(buffer_compressed.len() > 0);
5209 assert!(buffer_uncompressed.len() > 0);
5210 }
5211
5212 #[test]
5213 fn test_empty_document_handling() {
5214 let mut buffer = Vec::new();
5215 let mut document = Document::new();
5216
5217 {
5218 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5219 writer.write_document(&mut document).unwrap();
5220 }
5221
5222 let content = String::from_utf8_lossy(&buffer);
5223 assert!(content.starts_with("%PDF-"));
5224 assert!(content.contains("/Type /Catalog"));
5225 assert!(content.contains("/Type /Pages"));
5226 assert!(content.contains("/Count 0"));
5227 assert!(content.ends_with("%%EOF\n"));
5228 }
5229
5230 #[test]
5231 fn test_object_reference_resolution() {
5232 let mut buffer = Vec::new();
5233 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5234
5235 let id1 = ObjectId::new(1, 0);
5236 let id2 = ObjectId::new(2, 0);
5237
5238 // Create objects that reference each other
5239 let mut dict1 = Dictionary::new();
5240 dict1.set("Type", Object::Name("Test".to_string()));
5241 dict1.set("Reference", Object::Reference(id2));
5242
5243 let mut dict2 = Dictionary::new();
5244 dict2.set("Type", Object::Name("Test2".to_string()));
5245 dict2.set("BackRef", Object::Reference(id1));
5246
5247 writer.write_object(id1, Object::Dictionary(dict1)).unwrap();
5248 writer.write_object(id2, Object::Dictionary(dict2)).unwrap();
5249
5250 let content = String::from_utf8_lossy(&buffer);
5251 assert!(content.contains("1 0 obj"));
5252 assert!(content.contains("2 0 obj"));
5253 assert!(content.contains("2 0 R"));
5254 assert!(content.contains("1 0 R"));
5255 }
5256
5257 #[test]
5258 fn test_metadata_field_encoding() {
5259 let mut buffer = Vec::new();
5260 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5261
5262 let mut document = Document::new();
5263 document.set_title("Test Title with Ümlauts");
5264 document.set_author("Authör Name");
5265 document.set_subject("Subject with 中文");
5266 document.set_keywords("keyword1, keyword2, ключевые слова");
5267
5268 writer.write_document(&mut document).unwrap();
5269
5270 let content = String::from_utf8_lossy(&buffer);
5271 assert!(content.contains("/Title"));
5272 assert!(content.contains("/Author"));
5273 assert!(content.contains("/Subject"));
5274 assert!(content.contains("/Keywords"));
5275 }
5276
5277 #[test]
5278 fn test_object_generation_numbers() {
5279 let mut buffer = Vec::new();
5280 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5281
5282 // Test different generation numbers
5283 let id_gen0 = ObjectId::new(1, 0);
5284 let id_gen1 = ObjectId::new(1, 1);
5285 let id_gen5 = ObjectId::new(2, 5);
5286
5287 writer.write_object(id_gen0, Object::Integer(0)).unwrap();
5288 writer.write_object(id_gen1, Object::Integer(1)).unwrap();
5289 writer.write_object(id_gen5, Object::Integer(5)).unwrap();
5290
5291 let content = String::from_utf8_lossy(&buffer);
5292 assert!(content.contains("1 0 obj"));
5293 assert!(content.contains("1 1 obj"));
5294 assert!(content.contains("2 5 obj"));
5295 }
5296
5297 #[test]
5298 fn test_array_serialization_edge_cases() {
5299 let mut buffer = Vec::new();
5300 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5301
5302 let test_arrays = vec![
5303 // Empty array
5304 vec![],
5305 // Single element
5306 vec![Object::Integer(42)],
5307 // Mixed types
5308 vec![
5309 Object::Integer(1),
5310 Object::Real(3.14),
5311 Object::String("test".to_string()),
5312 Object::Name("TestName".to_string()),
5313 Object::Boolean(true),
5314 Object::Null,
5315 ],
5316 // Nested arrays
5317 vec![
5318 Object::Array(vec![Object::Integer(1), Object::Integer(2)]),
5319 Object::Array(vec![Object::String("a".to_string()), Object::String("b".to_string())]),
5320 ],
5321 ];
5322
5323 for (i, array) in test_arrays.iter().enumerate() {
5324 let id = ObjectId::new((i + 1) as u32, 0);
5325 writer.write_object(id, Object::Array(array.clone())).unwrap();
5326 }
5327
5328 let content = String::from_utf8_lossy(&buffer);
5329 assert!(content.contains("[]")); // Empty array
5330 assert!(content.contains("[42]")); // Single element
5331 assert!(content.contains("true")); // Boolean
5332 assert!(content.contains("null")); // Null
5333 }
5334
5335 #[test]
5336 fn test_real_number_precision() {
5337 let mut buffer = Vec::new();
5338 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5339
5340 let test_reals = vec![
5341 0.0,
5342 1.0,
5343 -1.0,
5344 3.14159265359,
5345 0.000001,
5346 1000000.5,
5347 -0.123456789,
5348 std::f64::consts::E,
5349 std::f64::consts::PI,
5350 ];
5351
5352 for (i, real) in test_reals.iter().enumerate() {
5353 let id = ObjectId::new((i + 1) as u32, 0);
5354 writer.write_object(id, Object::Real(*real)).unwrap();
5355 }
5356
5357 let content = String::from_utf8_lossy(&buffer);
5358 assert!(content.contains("3.14159"));
5359 assert!(content.contains("0.000001"));
5360 assert!(content.contains("1000000.5"));
5361 }
5362
5363 #[test]
5364 fn test_circular_reference_detection() {
5365 let mut buffer = Vec::new();
5366 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5367
5368 let id1 = ObjectId::new(1, 0);
5369 let id2 = ObjectId::new(2, 0);
5370
5371 // Create circular reference (should not cause infinite loop)
5372 let mut dict1 = Dictionary::new();
5373 dict1.set("Ref", Object::Reference(id2));
5374
5375 let mut dict2 = Dictionary::new();
5376 dict2.set("Ref", Object::Reference(id1));
5377
5378 writer.write_object(id1, Object::Dictionary(dict1)).unwrap();
5379 writer.write_object(id2, Object::Dictionary(dict2)).unwrap();
5380
5381 let content = String::from_utf8_lossy(&buffer);
5382 assert!(content.contains("1 0 obj"));
5383 assert!(content.contains("2 0 obj"));
5384 }
5385
5386 #[test]
5387 fn test_document_structure_integrity() {
5388 let mut buffer = Vec::new();
5389 let mut document = Document::new();
5390
5391 // Add multiple pages with different sizes
5392 document.add_page(crate::page::Page::new(612.0, 792.0)); // Letter
5393 document.add_page(crate::page::Page::new(595.0, 842.0)); // A4
5394 document.add_page(crate::page::Page::new(720.0, 1008.0)); // Legal
5395
5396 {
5397 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5398 writer.write_document(&mut document).unwrap();
5399 }
5400
5401 let content = String::from_utf8_lossy(&buffer);
5402
5403 // Verify structure
5404 assert!(content.contains("/Count 3"));
5405 assert!(content.contains("/MediaBox [0 0 612 792]"));
5406 assert!(content.contains("/MediaBox [0 0 595 842]"));
5407 assert!(content.contains("/MediaBox [0 0 720 1008]"));
5408 }
5409
5410 #[test]
5411 fn test_xref_table_boundary_conditions() {
5412 let mut buffer = Vec::new();
5413 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5414
5415 // Test with object 0 (free object)
5416 writer.xref_positions.insert(ObjectId::new(0, 65535), 0);
5417
5418 // Test with high object numbers
5419 writer.xref_positions.insert(ObjectId::new(999999, 0), 1234567890);
5420
5421 // Test with high generation numbers
5422 writer.xref_positions.insert(ObjectId::new(1, 65534), 100);
5423
5424 writer.write_xref().unwrap();
5425
5426 let content = String::from_utf8_lossy(&buffer);
5427 assert!(content.contains("0000000000 65535 f"));
5428 assert!(content.contains("1234567890 00000 n"));
5429 assert!(content.contains("0000000100 65534 n"));
5430 }
5431
5432 #[test]
5433 fn test_trailer_completeness() {
5434 let mut buffer = Vec::new();
5435 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5436
5437 writer.catalog_id = Some(ObjectId::new(1, 0));
5438 writer.info_id = Some(ObjectId::new(2, 0));
5439
5440 // Add multiple objects to ensure proper size calculation
5441 for i in 0..10 {
5442 writer.xref_positions.insert(ObjectId::new(i, 0), (i * 100) as u64);
5443 }
5444
5445 writer.write_trailer(5000).unwrap();
5446
5447 let content = String::from_utf8_lossy(&buffer);
5448 assert!(content.contains("trailer"));
5449 assert!(content.contains("/Size 10"));
5450 assert!(content.contains("/Root 1 0 R"));
5451 assert!(content.contains("/Info 2 0 R"));
5452 assert!(content.contains("startxref"));
5453 assert!(content.contains("5000"));
5454 assert!(content.contains("%%EOF"));
5455 }
5456
5457 #[test]
5458 fn test_position_tracking_accuracy() {
5459 let mut buffer = Vec::new();
5460 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5461
5462 let initial_pos = writer.current_position;
5463 assert_eq!(initial_pos, 0);
5464
5465 writer.write_bytes(b"Hello").unwrap();
5466 assert_eq!(writer.current_position, 5);
5467
5468 writer.write_bytes(b" World").unwrap();
5469 assert_eq!(writer.current_position, 11);
5470
5471 writer.write_bytes(b"!").unwrap();
5472 assert_eq!(writer.current_position, 12);
5473
5474 assert_eq!(buffer, b"Hello World!");
5475 }
5476
5477 #[test]
5478 fn test_error_handling_write_failures() {
5479 // Test with a mock writer that fails
5480 struct FailingWriter {
5481 fail_after: usize,
5482 written: usize,
5483 }
5484
5485 impl Write for FailingWriter {
5486 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
5487 if self.written + buf.len() > self.fail_after {
5488 Err(std::io::Error::new(std::io::ErrorKind::Other, "Mock failure"))
5489 } else {
5490 self.written += buf.len();
5491 Ok(buf.len())
5492 }
5493 }
5494
5495 fn flush(&mut self) -> std::io::Result<()> {
5496 Ok(())
5497 }
5498 }
5499
5500 let failing_writer = FailingWriter { fail_after: 10, written: 0 };
5501 let mut writer = PdfWriter::new_with_writer(failing_writer);
5502
5503 // This should fail when trying to write more than 10 bytes
5504 let result = writer.write_bytes(b"This is a long string that will fail");
5505 assert!(result.is_err());
5506 }
5507
5508 #[test]
5509 fn test_object_serialization_consistency() {
5510 let mut buffer = Vec::new();
5511 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5512
5513 // Test consistent serialization of the same object
5514 let test_obj = Object::Dictionary({
5515 let mut dict = Dictionary::new();
5516 dict.set("Type", Object::Name("Test".to_string()));
5517 dict.set("Value", Object::Integer(42));
5518 dict
5519 });
5520
5521 let id1 = ObjectId::new(1, 0);
5522 let id2 = ObjectId::new(2, 0);
5523
5524 writer.write_object(id1, test_obj.clone()).unwrap();
5525 writer.write_object(id2, test_obj.clone()).unwrap();
5526
5527 let content = String::from_utf8_lossy(&buffer);
5528
5529 // Both objects should have identical content except for object ID
5530 let lines: Vec<&str> = content.lines().collect();
5531 let obj1_content: Vec<&str> = lines.iter()
5532 .skip_while(|line| !line.contains("1 0 obj"))
5533 .take_while(|line| !line.contains("endobj"))
5534 .skip(1) // Skip the "1 0 obj" line
5535 .copied()
5536 .collect();
5537
5538 let obj2_content: Vec<&str> = lines.iter()
5539 .skip_while(|line| !line.contains("2 0 obj"))
5540 .take_while(|line| !line.contains("endobj"))
5541 .skip(1) // Skip the "2 0 obj" line
5542 .copied()
5543 .collect();
5544
5545 assert_eq!(obj1_content, obj2_content);
5546 }
5547
5548 #[test]
5549 fn test_font_subsetting_integration() {
5550 let mut buffer = Vec::new();
5551 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5552
5553 // Simulate used characters for font subsetting
5554 let mut used_chars = std::collections::HashSet::new();
5555 used_chars.insert('A');
5556 used_chars.insert('B');
5557 used_chars.insert('C');
5558 used_chars.insert(' ');
5559
5560 writer.document_used_chars = Some(used_chars.clone());
5561
5562 // Verify the used characters are stored
5563 assert!(writer.document_used_chars.is_some());
5564 let stored_chars = writer.document_used_chars.as_ref().unwrap();
5565 assert!(stored_chars.contains(&'A'));
5566 assert!(stored_chars.contains(&'B'));
5567 assert!(stored_chars.contains(&'C'));
5568 assert!(stored_chars.contains(&' '));
5569 assert!(!stored_chars.contains(&'Z'));
5570 }
5571
5572 #[test]
5573 fn test_form_field_tracking() {
5574 let mut buffer = Vec::new();
5575 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5576
5577 // Test form field ID tracking
5578 let field_id = ObjectId::new(10, 0);
5579 let widget_id1 = ObjectId::new(11, 0);
5580 let widget_id2 = ObjectId::new(12, 0);
5581
5582 writer.field_id_map.insert("test_field".to_string(), field_id);
5583 writer.field_widget_map.insert(
5584 "test_field".to_string(),
5585 vec![widget_id1, widget_id2]
5586 );
5587 writer.form_field_ids.push(field_id);
5588
5589 // Verify tracking
5590 assert_eq!(writer.field_id_map.get("test_field"), Some(&field_id));
5591 assert_eq!(writer.field_widget_map.get("test_field"), Some(&vec![widget_id1, widget_id2]));
5592 assert!(writer.form_field_ids.contains(&field_id));
5593 }
5594
5595 #[test]
5596 fn test_page_id_tracking() {
5597 let mut buffer = Vec::new();
5598 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5599
5600 let page_ids = vec![
5601 ObjectId::new(5, 0),
5602 ObjectId::new(6, 0),
5603 ObjectId::new(7, 0),
5604 ];
5605
5606 writer.page_ids = page_ids.clone();
5607
5608 assert_eq!(writer.page_ids.len(), 3);
5609 assert_eq!(writer.page_ids[0].number(), 5);
5610 assert_eq!(writer.page_ids[1].number(), 6);
5611 assert_eq!(writer.page_ids[2].number(), 7);
5612 }
5613
5614 #[test]
5615 fn test_catalog_pages_info_id_allocation() {
5616 let mut buffer = Vec::new();
5617 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5618
5619 // Test that required IDs are properly allocated
5620 writer.catalog_id = Some(writer.allocate_object_id());
5621 writer.pages_id = Some(writer.allocate_object_id());
5622 writer.info_id = Some(writer.allocate_object_id());
5623
5624 assert!(writer.catalog_id.is_some());
5625 assert!(writer.pages_id.is_some());
5626 assert!(writer.info_id.is_some());
5627
5628 // IDs should be sequential
5629 assert_eq!(writer.catalog_id.unwrap().number(), 1);
5630 assert_eq!(writer.pages_id.unwrap().number(), 2);
5631 assert_eq!(writer.info_id.unwrap().number(), 3);
5632 }
5633
5634 #[test]
5635 fn test_boolean_object_serialization() {
5636 let mut buffer = Vec::new();
5637 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5638
5639 writer.write_object(ObjectId::new(1, 0), Object::Boolean(true)).unwrap();
5640 writer.write_object(ObjectId::new(2, 0), Object::Boolean(false)).unwrap();
5641
5642 let content = String::from_utf8_lossy(&buffer);
5643 assert!(content.contains("true"));
5644 assert!(content.contains("false"));
5645 }
5646
5647 #[test]
5648 fn test_null_object_serialization() {
5649 let mut buffer = Vec::new();
5650 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5651
5652 writer.write_object(ObjectId::new(1, 0), Object::Null).unwrap();
5653
5654 let content = String::from_utf8_lossy(&buffer);
5655 assert!(content.contains("1 0 obj"));
5656 assert!(content.contains("null"));
5657 assert!(content.contains("endobj"));
5658 }
5659
5660 #[test]
5661 fn test_stream_object_handling() {
5662 let mut buffer = Vec::new();
5663 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5664
5665 let stream_data = b"This is stream content";
5666 let mut stream_dict = Dictionary::new();
5667 stream_dict.set("Length", Object::Integer(stream_data.len() as i64));
5668
5669 let stream = crate::objects::Stream {
5670 dict: stream_dict,
5671 data: stream_data.to_vec(),
5672 };
5673
5674 writer.write_object(ObjectId::new(1, 0), Object::Stream(stream)).unwrap();
5675
5676 let content = String::from_utf8_lossy(&buffer);
5677 assert!(content.contains("1 0 obj"));
5678 assert!(content.contains("/Length"));
5679 assert!(content.contains("stream"));
5680 assert!(content.contains("This is stream content"));
5681 assert!(content.contains("endstream"));
5682 assert!(content.contains("endobj"));
5683 }
5684
5685 #[test]
5686 fn test_integer_boundary_values() {
5687 let mut buffer = Vec::new();
5688 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5689
5690 let test_integers = vec![
5691 i64::MIN,
5692 -1000000,
5693 -1,
5694 0,
5695 1,
5696 1000000,
5697 i64::MAX,
5698 ];
5699
5700 for (i, int_val) in test_integers.iter().enumerate() {
5701 let id = ObjectId::new((i + 1) as u32, 0);
5702 writer.write_object(id, Object::Integer(*int_val)).unwrap();
5703 }
5704
5705 let content = String::from_utf8_lossy(&buffer);
5706 assert!(content.contains(&i64::MIN.to_string()));
5707 assert!(content.contains(&i64::MAX.to_string()));
5708 }
5709
5710 #[test]
5711 fn test_real_number_special_values() {
5712 let mut buffer = Vec::new();
5713 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5714
5715 let test_reals = vec![
5716 0.0,
5717 -0.0,
5718 f64::MIN,
5719 f64::MAX,
5720 1.0 / 3.0, // Repeating decimal
5721 f64::EPSILON,
5722 ];
5723
5724 for (i, real_val) in test_reals.iter().enumerate() {
5725 if real_val.is_finite() {
5726 let id = ObjectId::new((i + 1) as u32, 0);
5727 writer.write_object(id, Object::Real(*real_val)).unwrap();
5728 }
5729 }
5730
5731 let content = String::from_utf8_lossy(&buffer);
5732 // Should contain some real numbers
5733 assert!(content.contains("0.33333") || content.contains("0.3"));
5734 }
5735
5736 #[test]
5737 fn test_empty_containers() {
5738 let mut buffer = Vec::new();
5739 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5740
5741 // Empty array
5742 writer.write_object(ObjectId::new(1, 0), Object::Array(vec![])).unwrap();
5743
5744 // Empty dictionary
5745 writer.write_object(ObjectId::new(2, 0), Object::Dictionary(Dictionary::new())).unwrap();
5746
5747 let content = String::from_utf8_lossy(&buffer);
5748 assert!(content.contains("[]"));
5749 assert!(content.contains("<<>>") || content.contains("<< >>"));
5750 }
5751
5752 #[test]
5753 fn test_write_document_with_forms() {
5754 let mut buffer = Vec::new();
5755 let mut document = Document::new();
5756
5757 // Add a page
5758 document.add_page(crate::page::Page::new(612.0, 792.0));
5759
5760 // Add form manager to trigger AcroForm creation
5761 document.form_manager = Some(crate::forms::FormManager::new());
5762
5763 {
5764 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5765 writer.write_document(&mut document).unwrap();
5766 }
5767
5768 let content = String::from_utf8_lossy(&buffer);
5769 assert!(content.contains("/AcroForm") || content.contains("AcroForm"));
5770 }
5771
5772 #[test]
5773 fn test_write_document_with_outlines() {
5774 let mut buffer = Vec::new();
5775 let mut document = Document::new();
5776
5777 // Add a page
5778 document.add_page(crate::page::Page::new(612.0, 792.0));
5779
5780 // Add outline tree
5781 let mut outline_tree = crate::document::OutlineTree::new();
5782 outline_tree.add_item(crate::document::OutlineItem {
5783 title: "Chapter 1".to_string(),
5784 ..Default::default()
5785 });
5786 document.outline = Some(outline_tree);
5787
5788 {
5789 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5790 writer.write_document(&mut document).unwrap();
5791 }
5792
5793 let content = String::from_utf8_lossy(&buffer);
5794 assert!(content.contains("/Outlines") || content.contains("Chapter 1"));
5795 }
5796
5797 #[test]
5798 fn test_string_escaping_edge_cases() {
5799 let mut buffer = Vec::new();
5800 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5801
5802 let test_strings = vec![
5803 "Simple string",
5804 "String with \\backslash",
5805 "String with (parentheses)",
5806 "String with \nnewline",
5807 "String with \ttab",
5808 "String with \rcarriage return",
5809 "Unicode: café",
5810 "Emoji: 🎯",
5811 "", // Empty string
5812 ];
5813
5814 for (i, s) in test_strings.iter().enumerate() {
5815 let id = ObjectId::new((i + 1) as u32, 0);
5816 writer.write_object(id, Object::String(s.to_string())).unwrap();
5817 }
5818
5819 let content = String::from_utf8_lossy(&buffer);
5820 // Should contain escaped or encoded strings
5821 assert!(content.contains("Simple string"));
5822 }
5823
5824 #[test]
5825 fn test_name_escaping_edge_cases() {
5826 let mut buffer = Vec::new();
5827 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5828
5829 let test_names = vec![
5830 "SimpleName",
5831 "Name With Spaces",
5832 "Name#With#Hash",
5833 "Name/With/Slash",
5834 "Name(With)Parens",
5835 "Name[With]Brackets",
5836 "", // Empty name
5837 ];
5838
5839 for (i, name) in test_names.iter().enumerate() {
5840 let id = ObjectId::new((i + 1) as u32, 0);
5841 writer.write_object(id, Object::Name(name.to_string())).unwrap();
5842 }
5843
5844 let content = String::from_utf8_lossy(&buffer);
5845 // Names should be properly escaped or handled
5846 assert!(content.contains("/SimpleName"));
5847 }
5848
5849 #[test]
5850 fn test_maximum_nesting_depth() {
5851 let mut buffer = Vec::new();
5852 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5853
5854 // Create maximum reasonable nesting
5855 let mut current = Object::Integer(0);
5856 for i in 1..=100 {
5857 let mut dict = Dictionary::new();
5858 dict.set(&format!("Level{}", i), current);
5859 current = Object::Dictionary(dict);
5860 }
5861
5862 writer.write_object(ObjectId::new(1, 0), current).unwrap();
5863
5864 let content = String::from_utf8_lossy(&buffer);
5865 assert!(content.contains("1 0 obj"));
5866 assert!(content.contains("/Level"));
5867 }
5868
5869 #[test]
5870 fn test_writer_state_isolation() {
5871 // Test that different writers don't interfere with each other
5872 let mut buffer1 = Vec::new();
5873 let mut buffer2 = Vec::new();
5874
5875 let mut writer1 = PdfWriter::new_with_writer(&mut buffer1);
5876 let mut writer2 = PdfWriter::new_with_writer(&mut buffer2);
5877
5878 // Write different objects to each writer
5879 writer1.write_object(ObjectId::new(1, 0), Object::Integer(111)).unwrap();
5880 writer2.write_object(ObjectId::new(1, 0), Object::Integer(222)).unwrap();
5881
5882 let content1 = String::from_utf8_lossy(&buffer1);
5883 let content2 = String::from_utf8_lossy(&buffer2);
5884
5885 assert!(content1.contains("111"));
5886 assert!(content2.contains("222"));
5887 assert!(!content1.contains("222"));
5888 assert!(!content2.contains("111"));
5889 }
5890 */
5891
5892 /* Temporarily disabled for coverage measurement
5893 #[test]
5894 fn test_font_embedding() {
5895 let mut buffer = Vec::new();
5896 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5897
5898 // Test font dictionary creation
5899 let mut font_dict = Dictionary::new();
5900 font_dict.insert("Type".to_string(), PdfObject::Name(PdfName::new("Font")));
5901 font_dict.insert("Subtype".to_string(), PdfObject::Name(PdfName::new("Type1")));
5902 font_dict.insert("BaseFont".to_string(), PdfObject::Name(PdfName::new("Helvetica")));
5903
5904 writer.write_object(ObjectId::new(1, 0), Object::Dictionary(font_dict)).unwrap();
5905
5906 let content = String::from_utf8_lossy(&buffer);
5907 assert!(content.contains("/Type /Font"));
5908 assert!(content.contains("/Subtype /Type1"));
5909 assert!(content.contains("/BaseFont /Helvetica"));
5910 }
5911
5912 #[test]
5913 fn test_form_field_writing() {
5914 let mut buffer = Vec::new();
5915 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5916
5917 // Create a form field dictionary
5918 let field_dict = Dictionary::new()
5919 .set("FT", Name::new("Tx")) // Text field
5920 .set("T", String::from("Name".as_bytes().to_vec()))
5921 .set("V", String::from("John Doe".as_bytes().to_vec()));
5922
5923 writer.write_object(ObjectId::new(1, 0), Object::Dictionary(field_dict)).unwrap();
5924
5925 let content = String::from_utf8_lossy(&buffer);
5926 assert!(content.contains("/FT /Tx"));
5927 assert!(content.contains("(Name)"));
5928 assert!(content.contains("(John Doe)"));
5929 }
5930
5931 #[test]
5932 fn test_write_binary_data() {
5933 let mut buffer = Vec::new();
5934 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5935
5936 // Test binary stream data
5937 let binary_data = vec![0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10]; // JPEG header
5938 let stream = Object::Stream(
5939 Dictionary::new()
5940 .set("Length", Object::Integer(binary_data.len() as i64))
5941 .set("Filter", Object::Name("DCTDecode".to_string())),
5942 binary_data.clone(),
5943 );
5944
5945 writer.write_object(ObjectId::new(1, 0), stream).unwrap();
5946
5947 let content = buffer.clone();
5948 // Verify stream structure
5949 let content_str = String::from_utf8_lossy(&content);
5950 assert!(content_str.contains("/Length 6"));
5951 assert!(content_str.contains("/Filter /DCTDecode"));
5952 // Binary data should be present
5953 assert!(content.windows(6).any(|window| window == &binary_data[..]));
5954 }
5955
5956 #[test]
5957 fn test_write_large_dictionary() {
5958 let mut buffer = Vec::new();
5959 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5960
5961 // Create a dictionary with many entries
5962 let mut dict = Dictionary::new();
5963 for i in 0..50 {
5964 dict = dict.set(format!("Key{}", i), Object::Integer(i));
5965 }
5966
5967 writer.write_object(ObjectId::new(1, 0), Object::Dictionary(dict)).unwrap();
5968
5969 let content = String::from_utf8_lossy(&buffer);
5970 assert!(content.contains("/Key0 0"));
5971 assert!(content.contains("/Key49 49"));
5972 assert!(content.contains("<<") && content.contains(">>"));
5973 }
5974
5975 #[test]
5976 fn test_write_nested_arrays() {
5977 let mut buffer = Vec::new();
5978 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5979
5980 // Create nested arrays
5981 let inner_array = Object::Array(vec![Object::Integer(1), Object::Integer(2), Object::Integer(3)]);
5982 let outer_array = Object::Array(vec![
5983 Object::Integer(0),
5984 inner_array,
5985 Object::String("test".to_string()),
5986 ]);
5987
5988 writer.write_object(ObjectId::new(1, 0), outer_array).unwrap();
5989
5990 let content = String::from_utf8_lossy(&buffer);
5991 assert!(content.contains("[0 [1 2 3] (test)]"));
5992 }
5993
5994 #[test]
5995 fn test_write_object_with_generation() {
5996 let mut buffer = Vec::new();
5997 let mut writer = PdfWriter::new_with_writer(&mut buffer);
5998
5999 // Test non-zero generation number
6000 writer.write_object(ObjectId::new(5, 3), Object::Boolean(true)).unwrap();
6001
6002 let content = String::from_utf8_lossy(&buffer);
6003 assert!(content.contains("5 3 obj"));
6004 assert!(content.contains("true"));
6005 assert!(content.contains("endobj"));
6006 }
6007
6008 #[test]
6009 fn test_write_empty_objects() {
6010 let mut buffer = Vec::new();
6011 let mut writer = PdfWriter::new_with_writer(&mut buffer);
6012
6013 // Test empty dictionary
6014 writer.write_object(ObjectId::new(1, 0), Object::Dictionary(Dictionary::new())).unwrap();
6015 // Test empty array
6016 writer.write_object(ObjectId::new(2, 0), Object::Array(vec![])).unwrap();
6017 // Test empty string
6018 writer.write_object(ObjectId::new(3, 0), Object::String(String::new())).unwrap();
6019
6020 let content = String::from_utf8_lossy(&buffer);
6021 assert!(content.contains("1 0 obj\n<<>>"));
6022 assert!(content.contains("2 0 obj\n[]"));
6023 assert!(content.contains("3 0 obj\n()"));
6024 }
6025
6026 #[test]
6027 fn test_escape_special_chars_in_strings() {
6028 let mut buffer = Vec::new();
6029 let mut writer = PdfWriter::new_with_writer(&mut buffer);
6030
6031 // Test string with special characters
6032 let special_string = String::from("Test (with) \\backslash\\ and )parens(".as_bytes().to_vec());
6033 writer.write_object(ObjectId::new(1, 0), special_string).unwrap();
6034
6035 let content = String::from_utf8_lossy(&buffer);
6036 // Should escape parentheses and backslashes
6037 assert!(content.contains("(Test \\(with\\) \\\\backslash\\\\ and \\)parens\\()"));
6038 }
6039
6040 // #[test]
6041 // fn test_write_hex_string() {
6042 // let mut buffer = Vec::new();
6043 // let mut writer = PdfWriter::new_with_writer(&mut buffer);
6044 //
6045 // // Create hex string (high bit bytes)
6046 // let hex_data = vec![0xFF, 0xAB, 0xCD, 0xEF];
6047 // let hex_string = Object::String(format!("{:02X}", hex_data.iter().map(|b| format!("{:02X}", b)).collect::<String>()));
6048 //
6049 // writer.write_object(ObjectId::new(1, 0), hex_string).unwrap();
6050 //
6051 // let content = String::from_utf8_lossy(&buffer);
6052 // assert!(content.contains("FFABCDEF"));
6053 // }
6054
6055 #[test]
6056 fn test_null_object() {
6057 let mut buffer = Vec::new();
6058 let mut writer = PdfWriter::new_with_writer(&mut buffer);
6059
6060 writer.write_object(ObjectId::new(1, 0), Object::Null).unwrap();
6061
6062 let content = String::from_utf8_lossy(&buffer);
6063 assert!(content.contains("1 0 obj\nnull\nendobj"));
6064 }
6065 */
6066 }
6067}