Skip to main content

oxidize_pdf/writer/pdf_writer/
mod.rs

1use crate::document::Document;
2use crate::error::{PdfError, Result};
3use crate::objects::{Dictionary, Object, ObjectId};
4use crate::text::fonts::embedding::CjkFontType;
5use crate::text::fonts::truetype::CmapSubtable;
6use crate::writer::{ObjectStreamConfig, ObjectStreamWriter, XRefStreamWriter};
7use chrono::{DateTime, Utc};
8use std::collections::HashMap;
9use std::io::{BufWriter, Write};
10use std::path::Path;
11
12/// Configuration for PDF writer
13#[derive(Debug, Clone)]
14pub struct WriterConfig {
15    /// Use XRef streams instead of traditional XRef tables (PDF 1.5+)
16    pub use_xref_streams: bool,
17    /// Use Object Streams for compressing multiple objects together (PDF 1.5+)
18    pub use_object_streams: bool,
19    /// PDF version to write (default: 1.7)
20    pub pdf_version: String,
21    /// Enable compression for streams (default: true)
22    pub compress_streams: bool,
23    /// Enable incremental updates mode (ISO 32000-1 §7.5.6)
24    pub incremental_update: bool,
25}
26
27impl Default for WriterConfig {
28    fn default() -> Self {
29        Self {
30            use_xref_streams: false,
31            use_object_streams: false,
32            pdf_version: "1.7".to_string(),
33            compress_streams: true,
34            incremental_update: false,
35        }
36    }
37}
38
39impl WriterConfig {
40    /// Create a modern PDF 1.5+ configuration with all compression features enabled
41    pub fn modern() -> Self {
42        Self {
43            use_xref_streams: true,
44            use_object_streams: true,
45            pdf_version: "1.5".to_string(),
46            compress_streams: true,
47            incremental_update: false,
48        }
49    }
50
51    /// Create a legacy PDF 1.4 configuration without modern compression
52    pub fn legacy() -> Self {
53        Self {
54            use_xref_streams: false,
55            use_object_streams: false,
56            pdf_version: "1.4".to_string(),
57            compress_streams: true,
58            incremental_update: false,
59        }
60    }
61
62    /// Create configuration for incremental updates (ISO 32000-1 §7.5.6)
63    pub fn incremental() -> Self {
64        Self {
65            use_xref_streams: false,
66            use_object_streams: false,
67            pdf_version: "1.4".to_string(),
68            compress_streams: true,
69            incremental_update: true,
70        }
71    }
72}
73
74/// Escape the three characters that are meaningful inside a PDF literal
75/// string (ISO 32000-1 §7.3.4.2): backslash introduces escape sequences
76/// and MUST be doubled; parentheses delimit the string and MUST be
77/// prefixed with a backslash when they appear in the payload.
78///
79/// Other control characters (CR, LF, HT, BS, FF) are legal inside a
80/// literal string *unescaped*, so we leave them alone — the parser is
81/// required to accept them verbatim per §7.3.4.2 Table 3. Octal
82/// escapes are a valid alternative encoding but not required here.
83///
84/// Correct ordering is essential: `\` MUST be escaped first (otherwise
85/// the `\` we insert to escape a `(` would itself get doubled). This
86/// helper walks the input exactly once and emits the escaped form.
87///
88/// **Scope clarification (issue #240 follow-up):** this helper serves
89/// only `Object::String` payloads (metadata, dict entries, array
90/// elements). The show-text `(text) Tj` payloads inside content
91/// streams take an independent path (`Op::ShowText` bytes are produced
92/// by `text::encoding::escape_show_text_literal_bytes`, which DOES
93/// escape the high byte range `0x80..=0xFF` as `\NNN` octal because
94/// those payloads carry WinAnsi-encoded text whose bytes must survive
95/// 7-bit-safe intermediaries). The two helpers solve different
96/// problems and intentionally have different coverage; they are not
97/// coordinated and one is not "downstream" of the other.
98fn escape_pdf_string_bytes(input: &[u8]) -> Vec<u8> {
99    let mut out = Vec::with_capacity(input.len());
100    for &byte in input {
101        match byte {
102            b'\\' => out.extend_from_slice(b"\\\\"),
103            b'(' => out.extend_from_slice(b"\\("),
104            b')' => out.extend_from_slice(b"\\)"),
105            other => out.push(other),
106        }
107    }
108    out
109}
110
111pub struct PdfWriter<W: Write> {
112    writer: W,
113    xref_positions: HashMap<ObjectId, u64>,
114    current_position: u64,
115    next_object_id: u32,
116    // Maps for tracking object IDs during writing
117    catalog_id: Option<ObjectId>,
118    pages_id: Option<ObjectId>,
119    info_id: Option<ObjectId>,
120    // Maps for tracking form fields and their widgets
121    #[allow(dead_code)]
122    field_widget_map: HashMap<String, Vec<ObjectId>>, // field name -> widget IDs
123    #[allow(dead_code)]
124    field_id_map: HashMap<String, ObjectId>, // field name -> field ID
125    form_field_ids: Vec<ObjectId>, // form field IDs to add to page annotations
126    page_ids: Vec<ObjectId>,       // page IDs for form field references
127    // Configuration
128    config: WriterConfig,
129    // Characters used in document, bucketed by font name (issue #204).
130    // The writer uses this to subset each custom font with only its
131    // own characters — a single global set caused unused fonts to be
132    // embedded with the active fonts' character coverage, doubling
133    // emitted size when two fonts shared a family.
134    document_used_chars_by_font: std::collections::HashMap<String, std::collections::HashSet<char>>,
135    // Object stream buffering (when use_object_streams is enabled)
136    buffered_objects: HashMap<ObjectId, Vec<u8>>,
137    compressed_object_map: HashMap<ObjectId, (ObjectId, u32)>, // obj_id -> (stream_id, index)
138    // Incremental update support (ISO 32000-1 §7.5.6)
139    prev_xref_offset: Option<u64>,
140    base_pdf_size: Option<u64>,
141    // Encryption support
142    encrypt_obj_id: Option<ObjectId>,
143    file_id: Option<Vec<u8>>,
144    encryption_state: Option<WriterEncryptionState>,
145    pending_encrypt_dict: Option<Dictionary>,
146    // FormManager field tracking:
147    //  * `form_field_placeholder_map` translates the placeholder
148    //    `ObjectReference` returned by `FormManager::add_text_field` et al.
149    //    (those use a local counter unaware of writer-side allocation) into
150    //    the real `ObjectId` chosen by `allocate_object_id`. Widgets created
151    //    via `Page::add_form_widget_with_ref` store the placeholder in
152    //    `Annotation::field_parent`; when the annotation dict is written we
153    //    remap it through this table so `/Parent` points at the real field.
154    //  * `form_manager_field_refs` is the ordered (alphabetical by field
155    //    name) list of real refs; it's appended to `document.acro_form.fields`
156    //    during `write_catalog` and is what ends up in
157    //    `/AcroForm/Fields`.
158    form_field_placeholder_map: HashMap<crate::objects::ObjectReference, ObjectId>,
159    form_manager_field_refs: Vec<crate::objects::ObjectReference>,
160}
161
162/// Holds the encryption key and encryptor for encrypting objects during write
163struct WriterEncryptionState {
164    encryptor: crate::encryption::ObjectEncryptor,
165}
166
167impl<W: Write> PdfWriter<W> {
168    pub fn new_with_writer(writer: W) -> Self {
169        Self::with_config(writer, WriterConfig::default())
170    }
171
172    pub fn with_config(writer: W, config: WriterConfig) -> Self {
173        Self {
174            writer,
175            xref_positions: HashMap::new(),
176            current_position: 0,
177            next_object_id: 1, // Start at 1 for sequential numbering
178            catalog_id: None,
179            pages_id: None,
180            info_id: None,
181            field_widget_map: HashMap::new(),
182            field_id_map: HashMap::new(),
183            form_field_ids: Vec::new(),
184            page_ids: Vec::new(),
185            config,
186            document_used_chars_by_font: std::collections::HashMap::new(),
187            buffered_objects: HashMap::new(),
188            compressed_object_map: HashMap::new(),
189            prev_xref_offset: None,
190            base_pdf_size: None,
191            encrypt_obj_id: None,
192            file_id: None,
193            encryption_state: None,
194            pending_encrypt_dict: None,
195            form_field_placeholder_map: HashMap::new(),
196            form_manager_field_refs: Vec::new(),
197        }
198    }
199
200    pub fn write_document(&mut self, document: &mut Document) -> Result<()> {
201        // Store used characters for font subsetting
202        if !document.used_characters_by_font.is_empty() {
203            self.document_used_chars_by_font = document.used_characters_by_font.clone();
204        }
205
206        self.write_header()?;
207
208        // Reserve object IDs for fixed objects (written in order)
209        self.catalog_id = Some(self.allocate_object_id());
210        self.pages_id = Some(self.allocate_object_id());
211        self.info_id = Some(self.allocate_object_id());
212
213        // Initialize encryption state BEFORE writing objects
214        // (objects need to be encrypted as they are written)
215        if let Some(ref encryption) = document.encryption {
216            self.init_encryption(encryption)?;
217        }
218
219        // Write custom fonts first (so pages can reference them)
220        let font_refs = self.write_fonts(document)?;
221
222        // Pre-allocate object IDs for every field owned by the FormManager
223        // BEFORE writing pages, so widget annotations on those pages can
224        // emit `/Parent <real_id>` instead of pointing at the placeholder
225        // refs returned by `FormManager::add_text_field`. This is the piece
226        // that bridges the FormManager's local id counter and the writer's
227        // global id allocator. See `form_field_placeholder_map` for details.
228        self.preallocate_form_manager_fields(document)?;
229
230        // Write pages (they contain widget annotations and font references)
231        self.write_pages(document, &font_refs)?;
232
233        // Write form fields (must be after pages so we can track widgets)
234        self.write_form_fields(document)?;
235
236        // Write catalog (must be after forms so AcroForm has correct field references)
237        self.write_catalog(document)?;
238
239        // Write document info
240        self.write_info(document)?;
241
242        // Write /Encrypt dict AFTER all objects (it must NOT be encrypted itself)
243        self.write_encryption_dict()?;
244
245        // Flush buffered objects as object streams (if enabled)
246        if self.config.use_object_streams {
247            self.flush_object_streams()?;
248        }
249
250        // Write xref table or stream
251        let xref_position = self.current_position;
252        if self.config.use_xref_streams {
253            self.write_xref_stream()?;
254        } else {
255            self.write_xref()?;
256        }
257
258        // Write trailer (only for traditional xref)
259        if !self.config.use_xref_streams {
260            self.write_trailer(xref_position)?;
261        }
262
263        if let Ok(()) = self.writer.flush() {
264            // Flush succeeded
265        }
266        Ok(())
267    }
268
269    /// Write an incremental update to an existing PDF (ISO 32000-1 §7.5.6)
270    ///
271    /// This appends new/modified objects to the end of an existing PDF file
272    /// without modifying the original content. The base PDF is copied first,
273    /// then new pages are ADDED to the end of the document.
274    ///
275    /// For REPLACING specific pages (e.g., form filling), use `write_incremental_with_page_replacement`.
276    ///
277    /// # Arguments
278    ///
279    /// * `base_pdf_path` - Path to the existing PDF file
280    /// * `document` - Document containing NEW pages to add
281    ///
282    /// # Returns
283    ///
284    /// Returns Ok(()) if the incremental update was written successfully
285    ///
286    /// # Example - Adding Pages
287    ///
288    /// ```no_run
289    /// use oxidize_pdf::{Document, Page, writer::{PdfWriter, WriterConfig}};
290    /// use std::fs::File;
291    /// use std::io::BufWriter;
292    ///
293    /// let mut doc = Document::new();
294    /// doc.add_page(Page::a4()); // This will be added as a NEW page
295    ///
296    /// let file = File::create("output.pdf").unwrap();
297    /// let writer = BufWriter::new(file);
298    /// let config = WriterConfig::incremental();
299    /// let mut pdf_writer = PdfWriter::with_config(writer, config);
300    /// pdf_writer.write_incremental_update("base.pdf", &mut doc).unwrap();
301    /// ```
302    pub fn write_incremental_update(
303        &mut self,
304        base_pdf_path: impl AsRef<std::path::Path>,
305        document: &mut Document,
306    ) -> Result<()> {
307        use std::io::{BufReader, Read, Seek, SeekFrom};
308
309        // Step 1: Parse the base PDF to get catalog and page information
310        let base_pdf_file = std::fs::File::open(base_pdf_path.as_ref())?;
311        let mut pdf_reader = crate::parser::PdfReader::new(BufReader::new(base_pdf_file))?;
312
313        // Get catalog from base PDF
314        let base_catalog = pdf_reader.catalog()?;
315
316        // Extract Pages reference from base catalog
317        let (base_pages_id, base_pages_gen) = base_catalog
318            .get("Pages")
319            .and_then(|obj| {
320                if let crate::parser::objects::PdfObject::Reference(id, gen) = obj {
321                    Some((*id, *gen))
322                } else {
323                    None
324                }
325            })
326            .ok_or_else(|| {
327                crate::error::PdfError::InvalidStructure(
328                    "Base PDF catalog missing /Pages reference".to_string(),
329                )
330            })?;
331
332        // Get the pages dictionary from the base PDF using the reference
333        let base_pages_obj = pdf_reader.get_object(base_pages_id, base_pages_gen)?;
334        let base_pages_kids = if let crate::parser::objects::PdfObject::Dictionary(dict) =
335            base_pages_obj
336        {
337            dict.get("Kids")
338                .and_then(|obj| {
339                    if let crate::parser::objects::PdfObject::Array(arr) = obj {
340                        // Convert PdfObject::Reference to writer::Object::Reference
341                        // PdfArray.0 gives access to the internal Vec<PdfObject>
342                        Some(
343                            arr.0
344                                .iter()
345                                .filter_map(|item| {
346                                    if let crate::parser::objects::PdfObject::Reference(id, gen) =
347                                        item
348                                    {
349                                        Some(crate::objects::Object::Reference(
350                                            crate::objects::ObjectId::new(*id, *gen),
351                                        ))
352                                    } else {
353                                        None
354                                    }
355                                })
356                                .collect::<Vec<_>>(),
357                        )
358                    } else {
359                        None
360                    }
361                })
362                .unwrap_or_default()
363        } else {
364            Vec::new()
365        };
366
367        // Count existing pages
368        let base_page_count = base_pages_kids.len();
369
370        // Step 2: Copy the base PDF content
371        let base_pdf = std::fs::File::open(base_pdf_path.as_ref())?;
372        let mut base_reader = BufReader::new(base_pdf);
373
374        // Find the startxref offset in the base PDF
375        base_reader.seek(SeekFrom::End(-100))?;
376        let mut end_buffer = vec![0u8; 100];
377        let bytes_read = base_reader.read(&mut end_buffer)?;
378        end_buffer.truncate(bytes_read);
379
380        let end_str = String::from_utf8_lossy(&end_buffer);
381        let prev_xref = if let Some(startxref_pos) = end_str.find("startxref") {
382            let after_startxref = &end_str[startxref_pos + 9..];
383
384            let number_str: String = after_startxref
385                .chars()
386                .skip_while(|c| c.is_whitespace())
387                .take_while(|c| c.is_ascii_digit())
388                .collect();
389
390            number_str.parse::<u64>().map_err(|_| {
391                crate::error::PdfError::InvalidStructure(
392                    "Could not parse startxref offset".to_string(),
393                )
394            })?
395        } else {
396            return Err(crate::error::PdfError::InvalidStructure(
397                "startxref not found in base PDF".to_string(),
398            ));
399        };
400
401        // Copy entire base PDF
402        base_reader.seek(SeekFrom::Start(0))?;
403        let base_size = std::io::copy(&mut base_reader, &mut self.writer)? as u64;
404
405        // Store base PDF info for trailer
406        self.prev_xref_offset = Some(prev_xref);
407        self.base_pdf_size = Some(base_size);
408        self.current_position = base_size;
409
410        // Step 3: Write new/modified objects only
411        if !document.used_characters_by_font.is_empty() {
412            self.document_used_chars_by_font = document.used_characters_by_font.clone();
413        }
414
415        // Allocate IDs for new objects
416        self.catalog_id = Some(self.allocate_object_id());
417        self.pages_id = Some(self.allocate_object_id());
418        self.info_id = Some(self.allocate_object_id());
419
420        // Write custom fonts first
421        let font_refs = self.write_fonts(document)?;
422
423        // Write NEW pages only (not rewriting all pages)
424        self.write_pages(document, &font_refs)?;
425
426        // Write form fields
427        self.write_form_fields(document)?;
428
429        // Step 4: Write modified catalog that references BOTH old and new pages
430        let catalog_id = self.get_catalog_id()?;
431        let new_pages_id = self.get_pages_id()?;
432
433        let mut catalog = crate::objects::Dictionary::new();
434        catalog.set("Type", crate::objects::Object::Name("Catalog".to_string()));
435        catalog.set("Pages", crate::objects::Object::Reference(new_pages_id));
436
437        // Note: For now, we only preserve the Pages reference.
438        // Full catalog preservation (Outlines, AcroForm, etc.) would require
439        // converting parser::PdfObject to writer::Object, which is a future enhancement.
440
441        self.write_object(catalog_id, crate::objects::Object::Dictionary(catalog))?;
442
443        // Step 5: Write new Pages tree that includes BOTH base pages and new pages
444        let mut all_pages_kids = base_pages_kids;
445
446        // Add references to new pages
447        for page_id in &self.page_ids {
448            all_pages_kids.push(crate::objects::Object::Reference(*page_id));
449        }
450
451        let mut pages_dict = crate::objects::Dictionary::new();
452        pages_dict.set("Type", crate::objects::Object::Name("Pages".to_string()));
453        pages_dict.set("Kids", crate::objects::Object::Array(all_pages_kids));
454        pages_dict.set(
455            "Count",
456            crate::objects::Object::Integer((base_page_count + self.page_ids.len()) as i64),
457        );
458
459        self.write_object(new_pages_id, crate::objects::Object::Dictionary(pages_dict))?;
460
461        // Write document info
462        self.write_info(document)?;
463
464        // Step 6: Write new XRef table with /Prev pointer
465        let xref_position = self.current_position;
466        self.write_xref()?;
467
468        // Step 7: Write trailer with /Prev
469        self.write_trailer(xref_position)?;
470
471        self.writer.flush()?;
472        Ok(())
473    }
474
475    /// Replaces pages in an existing PDF using incremental update structure (ISO 32000-1 §7.5.6).
476    ///
477    /// # Use Cases
478    /// This API is ideal for:
479    /// - **Dynamic page generation**: You have logic to generate complete pages from data
480    /// - **Template variants**: Switching between multiple pre-generated page versions
481    /// - **Page repair**: Regenerating corrupted or problematic pages from scratch
482    ///
483    /// # Manual Content Recreation Required
484    /// **IMPORTANT**: This API requires you to **manually recreate** the entire page content.
485    /// The replaced page will contain ONLY what you provide in `document.pages`.
486    ///
487    /// If you need to modify existing content (e.g., fill form fields on an existing page),
488    /// you must recreate the base content AND add your modifications.
489    ///
490    /// # Example: Form Filling with Manual Recreation
491    /// ```rust,no_run
492    /// use oxidize_pdf::{Document, Page, text::Font, writer::{PdfWriter, WriterConfig}};
493    /// use std::fs::File;
494    /// use std::io::BufWriter;
495    ///
496    /// let mut filled_doc = Document::new();
497    /// let mut page = Page::a4();
498    ///
499    /// // Step 1: Recreate the template content (REQUIRED - you must know this)
500    /// page.text()
501    ///     .set_font(Font::Helvetica, 12.0)
502    ///     .at(50.0, 700.0)
503    ///     .write("Name: _______________________________")?;
504    ///
505    /// // Step 2: Add your filled data at the appropriate position
506    /// page.text()
507    ///     .set_font(Font::Helvetica, 12.0)
508    ///     .at(110.0, 700.0)
509    ///     .write("John Smith")?;
510    ///
511    /// filled_doc.add_page(page);
512    ///
513    /// let file = File::create("filled.pdf")?;
514    /// let writer = BufWriter::new(file);
515    /// let mut pdf_writer = PdfWriter::with_config(writer, WriterConfig::incremental());
516    ///
517    /// pdf_writer.write_incremental_with_page_replacement("template.pdf", &mut filled_doc)?;
518    /// # Ok::<(), Box<dyn std::error::Error>>(())
519    /// ```
520    ///
521    /// # ISO Compliance
522    /// This function implements ISO 32000-1 §7.5.6 incremental updates:
523    /// - Preserves original PDF bytes (append-only)
524    /// - Uses /Prev pointer in trailer
525    /// - Maintains cross-reference chain
526    /// - Compatible with digital signatures on base PDF
527    ///
528    /// # Future: Automatic Overlay API
529    /// For automatic form filling (load + modify + save) without manual recreation,
530    /// a future `write_incremental_with_overlay()` API is planned. This will require
531    /// implementation of `Document::load()` and content overlay system.
532    ///
533    /// # Parameters
534    /// - `base_pdf_path`: Path to the existing PDF to modify
535    /// - `document`: Document containing replacement pages (first N pages will replace base pages 0..N-1)
536    ///
537    /// # Returns
538    /// - `Ok(())` if incremental update was written successfully
539    /// - `Err(PdfError)` if base PDF cannot be read, parsed, or structure is invalid
540    pub fn write_incremental_with_page_replacement(
541        &mut self,
542        base_pdf_path: impl AsRef<std::path::Path>,
543        document: &mut Document,
544    ) -> Result<()> {
545        use std::io::Cursor;
546
547        // Step 1: Read the entire base PDF into memory (avoids double file open)
548        let base_pdf_bytes = std::fs::read(base_pdf_path.as_ref())?;
549        let base_size = base_pdf_bytes.len() as u64;
550
551        // Step 2: Parse from memory to get page information
552        let mut pdf_reader = crate::parser::PdfReader::new(Cursor::new(&base_pdf_bytes))?;
553
554        let base_catalog = pdf_reader.catalog()?;
555
556        let (base_pages_id, base_pages_gen) = base_catalog
557            .get("Pages")
558            .and_then(|obj| {
559                if let crate::parser::objects::PdfObject::Reference(id, gen) = obj {
560                    Some((*id, *gen))
561                } else {
562                    None
563                }
564            })
565            .ok_or_else(|| {
566                crate::error::PdfError::InvalidStructure(
567                    "Base PDF catalog missing /Pages reference".to_string(),
568                )
569            })?;
570
571        let base_pages_obj = pdf_reader.get_object(base_pages_id, base_pages_gen)?;
572        let base_pages_kids = if let crate::parser::objects::PdfObject::Dictionary(dict) =
573            base_pages_obj
574        {
575            dict.get("Kids")
576                .and_then(|obj| {
577                    if let crate::parser::objects::PdfObject::Array(arr) = obj {
578                        Some(
579                            arr.0
580                                .iter()
581                                .filter_map(|item| {
582                                    if let crate::parser::objects::PdfObject::Reference(id, gen) =
583                                        item
584                                    {
585                                        Some(crate::objects::Object::Reference(
586                                            crate::objects::ObjectId::new(*id, *gen),
587                                        ))
588                                    } else {
589                                        None
590                                    }
591                                })
592                                .collect::<Vec<_>>(),
593                        )
594                    } else {
595                        None
596                    }
597                })
598                .unwrap_or_default()
599        } else {
600            Vec::new()
601        };
602
603        let base_page_count = base_pages_kids.len();
604
605        // Step 3: Find startxref offset from the bytes
606        let start_search = if base_size > 100 { base_size - 100 } else { 0 } as usize;
607        let end_bytes = &base_pdf_bytes[start_search..];
608        let end_str = String::from_utf8_lossy(end_bytes);
609
610        let prev_xref = if let Some(startxref_pos) = end_str.find("startxref") {
611            let after_startxref = &end_str[startxref_pos + 9..];
612            let number_str: String = after_startxref
613                .chars()
614                .skip_while(|c| c.is_whitespace())
615                .take_while(|c| c.is_ascii_digit())
616                .collect();
617
618            number_str.parse::<u64>().map_err(|_| {
619                crate::error::PdfError::InvalidStructure(
620                    "Could not parse startxref offset".to_string(),
621                )
622            })?
623        } else {
624            return Err(crate::error::PdfError::InvalidStructure(
625                "startxref not found in base PDF".to_string(),
626            ));
627        };
628
629        // Step 4: Copy base PDF bytes to output
630        self.writer.write_all(&base_pdf_bytes)?;
631
632        self.prev_xref_offset = Some(prev_xref);
633        self.base_pdf_size = Some(base_size);
634        self.current_position = base_size;
635
636        // Step 3: Write replacement pages
637        if !document.used_characters_by_font.is_empty() {
638            self.document_used_chars_by_font = document.used_characters_by_font.clone();
639        }
640
641        self.catalog_id = Some(self.allocate_object_id());
642        self.pages_id = Some(self.allocate_object_id());
643        self.info_id = Some(self.allocate_object_id());
644
645        let font_refs = self.write_fonts(document)?;
646        self.write_pages(document, &font_refs)?;
647        self.write_form_fields(document)?;
648
649        // Step 4: Create Pages tree with REPLACEMENTS
650        let catalog_id = self.get_catalog_id()?;
651        let new_pages_id = self.get_pages_id()?;
652
653        let mut catalog = crate::objects::Dictionary::new();
654        catalog.set("Type", crate::objects::Object::Name("Catalog".to_string()));
655        catalog.set("Pages", crate::objects::Object::Reference(new_pages_id));
656        self.write_object(catalog_id, crate::objects::Object::Dictionary(catalog))?;
657
658        // Build new Kids array: replace first N pages, keep rest from base
659        let mut all_pages_kids = Vec::new();
660        let replacement_count = document.pages.len();
661
662        // Add replacement pages (these override base pages at same indices)
663        for page_id in &self.page_ids {
664            all_pages_kids.push(crate::objects::Object::Reference(*page_id));
665        }
666
667        // Add remaining base pages that weren't replaced
668        if replacement_count < base_page_count {
669            for i in replacement_count..base_page_count {
670                if let Some(page_ref) = base_pages_kids.get(i) {
671                    all_pages_kids.push(page_ref.clone());
672                }
673            }
674        }
675
676        let mut pages_dict = crate::objects::Dictionary::new();
677        pages_dict.set("Type", crate::objects::Object::Name("Pages".to_string()));
678        pages_dict.set(
679            "Kids",
680            crate::objects::Object::Array(all_pages_kids.clone()),
681        );
682        pages_dict.set(
683            "Count",
684            crate::objects::Object::Integer(all_pages_kids.len() as i64),
685        );
686
687        self.write_object(new_pages_id, crate::objects::Object::Dictionary(pages_dict))?;
688        self.write_info(document)?;
689
690        let xref_position = self.current_position;
691        self.write_xref()?;
692        self.write_trailer(xref_position)?;
693
694        self.writer.flush()?;
695        Ok(())
696    }
697
698    /// Overlays content onto existing PDF pages using incremental updates (PLANNED).
699    ///
700    /// **STATUS**: Not yet implemented. This API is planned for a future release.
701    ///
702    /// # What This Will Do
703    /// When implemented, this function will allow you to:
704    /// - Load an existing PDF
705    /// - Modify specific elements (fill form fields, add annotations, watermarks)
706    /// - Save incrementally without recreating entire pages
707    ///
708    /// # Difference from Page Replacement
709    /// - **Page Replacement** (`write_incremental_with_page_replacement`): Replaces entire pages with manually recreated content
710    /// - **Overlay** (this function): Modifies existing pages by adding/changing specific elements
711    ///
712    /// # Planned Usage (Future)
713    /// ```rust,ignore
714    /// // This code will work in a future release
715    /// let mut pdf_writer = PdfWriter::with_config(writer, WriterConfig::incremental());
716    ///
717    /// let overlays = vec![
718    ///     PageOverlay::new(0)
719    ///         .add_text(110.0, 700.0, "John Smith")
720    ///         .add_annotation(Annotation::text(200.0, 500.0, "Review this")),
721    /// ];
722    ///
723    /// pdf_writer.write_incremental_with_overlay("form.pdf", overlays)?;
724    /// ```
725    ///
726    /// # Implementation Requirements
727    /// This function requires:
728    /// 1. `Document::load()` - Load existing PDF into Document structure
729    /// 2. `Page::from_parsed()` - Convert parsed pages to writable format
730    /// 3. Content stream overlay system - Append to existing content streams
731    /// 4. Resource merging - Combine new resources with existing ones
732    ///
733    /// Estimated implementation effort: 6-7 days
734    ///
735    /// # Current Workaround
736    /// Until this is implemented, use `write_incremental_with_page_replacement()` with manual
737    /// page recreation. See that function's documentation for examples.
738    ///
739    /// # Parameters
740    /// - `base_pdf_path`: Path to the existing PDF to modify (future)
741    /// - `overlays`: Content to overlay on existing pages (future)
742    ///
743    /// # Returns
744    /// Currently always returns `PdfError::NotImplemented`
745    pub fn write_incremental_with_overlay<P: AsRef<std::path::Path>>(
746        &mut self,
747        base_pdf_path: P,
748        mut overlay_fn: impl FnMut(&mut crate::Page) -> Result<()>,
749    ) -> Result<()> {
750        use std::io::Cursor;
751
752        // Step 1: Read the entire base PDF into memory
753        let base_pdf_bytes = std::fs::read(base_pdf_path.as_ref())?;
754        let base_size = base_pdf_bytes.len() as u64;
755
756        // Step 2: Parse from memory to get page information
757        let pdf_reader = crate::parser::PdfReader::new(Cursor::new(&base_pdf_bytes))?;
758        let parsed_doc = crate::parser::PdfDocument::new(pdf_reader);
759
760        // Get all pages from base PDF
761        let page_count = parsed_doc.page_count()?;
762
763        // Step 3: Find startxref offset from the bytes
764        let start_search = if base_size > 100 { base_size - 100 } else { 0 } as usize;
765        let end_bytes = &base_pdf_bytes[start_search..];
766        let end_str = String::from_utf8_lossy(end_bytes);
767
768        let prev_xref = if let Some(startxref_pos) = end_str.find("startxref") {
769            let after_startxref = &end_str[startxref_pos + 9..];
770            let number_str: String = after_startxref
771                .chars()
772                .skip_while(|c| c.is_whitespace())
773                .take_while(|c| c.is_ascii_digit())
774                .collect();
775
776            number_str.parse::<u64>().map_err(|_| {
777                crate::error::PdfError::InvalidStructure(
778                    "Could not parse startxref offset".to_string(),
779                )
780            })?
781        } else {
782            return Err(crate::error::PdfError::InvalidStructure(
783                "startxref not found in base PDF".to_string(),
784            ));
785        };
786
787        // Step 5: Copy base PDF bytes to output
788        self.writer.write_all(&base_pdf_bytes)?;
789
790        self.prev_xref_offset = Some(prev_xref);
791        self.base_pdf_size = Some(base_size);
792        self.current_position = base_size;
793
794        // Step 6: Build temporary document with overlaid pages
795        let mut temp_doc = crate::Document::new();
796
797        for page_idx in 0..page_count {
798            // Convert parsed page to writable with content preservation
799            let parsed_page = parsed_doc.get_page(page_idx)?;
800            let mut writable_page =
801                crate::Page::from_parsed_with_content(&parsed_page, &parsed_doc)?;
802
803            // Apply overlay function
804            overlay_fn(&mut writable_page)?;
805
806            // Add to temporary document
807            temp_doc.add_page(writable_page);
808        }
809
810        // Step 7: Write document with standard writer methods
811        // This ensures consistent object numbering
812        if !temp_doc.used_characters_by_font.is_empty() {
813            self.document_used_chars_by_font = temp_doc.used_characters_by_font.clone();
814        }
815
816        self.catalog_id = Some(self.allocate_object_id());
817        self.pages_id = Some(self.allocate_object_id());
818        self.info_id = Some(self.allocate_object_id());
819
820        let font_refs = self.write_fonts(&temp_doc)?;
821        self.write_pages(&temp_doc, &font_refs)?;
822        self.write_form_fields(&mut temp_doc)?;
823
824        // Step 8: Create new catalog and pages tree
825        let catalog_id = self.get_catalog_id()?;
826        let new_pages_id = self.get_pages_id()?;
827
828        let mut catalog = crate::objects::Dictionary::new();
829        catalog.set("Type", crate::objects::Object::Name("Catalog".to_string()));
830        catalog.set("Pages", crate::objects::Object::Reference(new_pages_id));
831        self.write_object(catalog_id, crate::objects::Object::Dictionary(catalog))?;
832
833        // Build new Kids array with ALL overlaid pages
834        let mut all_pages_kids = Vec::new();
835        for page_id in &self.page_ids {
836            all_pages_kids.push(crate::objects::Object::Reference(*page_id));
837        }
838
839        let mut pages_dict = crate::objects::Dictionary::new();
840        pages_dict.set("Type", crate::objects::Object::Name("Pages".to_string()));
841        pages_dict.set(
842            "Kids",
843            crate::objects::Object::Array(all_pages_kids.clone()),
844        );
845        pages_dict.set(
846            "Count",
847            crate::objects::Object::Integer(all_pages_kids.len() as i64),
848        );
849
850        self.write_object(new_pages_id, crate::objects::Object::Dictionary(pages_dict))?;
851        self.write_info(&temp_doc)?;
852
853        let xref_position = self.current_position;
854        self.write_xref()?;
855        self.write_trailer(xref_position)?;
856
857        self.writer.flush()?;
858        Ok(())
859    }
860
861    fn write_header(&mut self) -> Result<()> {
862        let header = format!("%PDF-{}\n", self.config.pdf_version);
863        self.write_bytes(header.as_bytes())?;
864        // Binary comment to ensure file is treated as binary
865        self.write_bytes(&[b'%', 0xE2, 0xE3, 0xCF, 0xD3, b'\n'])?;
866        Ok(())
867    }
868
869    /// Convert pdf_objects types to writer objects types
870    /// This is a temporary bridge until type unification is complete
871    fn convert_pdf_objects_dict_to_writer(
872        &self,
873        pdf_dict: &crate::pdf_objects::Dictionary,
874    ) -> crate::objects::Dictionary {
875        let mut writer_dict = crate::objects::Dictionary::new();
876
877        for (key, value) in pdf_dict.iter() {
878            let writer_obj = self.convert_pdf_object_to_writer(value);
879            writer_dict.set(key.as_str(), writer_obj);
880        }
881
882        writer_dict
883    }
884
885    fn convert_pdf_object_to_writer(
886        &self,
887        obj: &crate::pdf_objects::Object,
888    ) -> crate::objects::Object {
889        use crate::objects::Object as WriterObj;
890        use crate::pdf_objects::Object as PdfObj;
891
892        match obj {
893            PdfObj::Null => WriterObj::Null,
894            PdfObj::Boolean(b) => WriterObj::Boolean(*b),
895            PdfObj::Integer(i) => WriterObj::Integer(*i),
896            PdfObj::Real(f) => WriterObj::Real(*f),
897            // As in `overlay`: the writer's string is a Rust `String`, so a
898            // binary string cannot survive; decoding as text keeps the text
899            // strings of a copied object readable (issue #459).
900            PdfObj::String(s) => {
901                WriterObj::String(crate::parser::objects::decode_text_string(s.as_bytes()))
902            }
903            PdfObj::Name(n) => WriterObj::Name(n.as_str().to_string()),
904            PdfObj::Array(arr) => {
905                let items: Vec<WriterObj> = arr
906                    .iter()
907                    .map(|item| self.convert_pdf_object_to_writer(item))
908                    .collect();
909                WriterObj::Array(items)
910            }
911            PdfObj::Dictionary(dict) => {
912                WriterObj::Dictionary(self.convert_pdf_objects_dict_to_writer(dict))
913            }
914            PdfObj::Stream(stream) => {
915                let dict = self.convert_pdf_objects_dict_to_writer(&stream.dict);
916                WriterObj::Stream(dict, stream.data.clone())
917            }
918            PdfObj::Reference(id) => {
919                WriterObj::Reference(crate::objects::ObjectId::new(id.number(), id.generation()))
920            }
921        }
922    }
923
924    fn write_catalog(&mut self, document: &mut Document) -> Result<()> {
925        let catalog_id = self.get_catalog_id()?;
926        let pages_id = self.get_pages_id()?;
927
928        let mut catalog = Dictionary::new();
929        catalog.set("Type", Object::Name("Catalog".to_string()));
930        catalog.set("Pages", Object::Reference(pages_id));
931
932        // Serialize fields owned by the FormManager (ISO 32000-1 §12.7.3).
933        //
934        // Before v2.5.6 this block did nothing: it bound `_form_manager`
935        // but never read its `fields` map, so only fields appended manually
936        // to `document.acro_form.fields` ever reached the output PDF. Any
937        // field created via `FormManager::add_text_field` / `add_combo_box`
938        // / etc. was silently dropped — exactly the gap the .NET wrapper
939        // hit.
940        //
941        // Object IDs for these fields were pre-allocated in
942        // `preallocate_form_manager_fields` (called before `write_pages`
943        // so widget `/Parent` refs could resolve). Here we only have to:
944        //   (a) write the field-body dict into each pre-allocated id, and
945        //   (b) append those ids to `document.acro_form.fields` so the
946        //       /AcroForm write block below emits
947        //       `/AcroForm/Fields [N 0 R ...]`.
948        //
949        // Iteration follows the same deterministic order used at
950        // pre-allocation time, so the order-vs-id pairing is stable.
951        if let Some(form_manager) = &document.form_manager {
952            if document.acro_form.is_none() {
953                document.acro_form = Some(crate::forms::AcroForm::new());
954            }
955
956            // Write each field dict into its reserved id.
957            // Surface a clean `PdfError` if the placeholder-ref → real-id
958            // map is missing any entry — a "can't happen" breach of the
959            // invariant established by `preallocate_form_manager_fields`,
960            // which must run before this function.
961            let mut sorted: Vec<(Dictionary, crate::objects::ObjectReference)> = Vec::new();
962            for (name, form_field, placeholder) in form_manager.iter_fields_sorted() {
963                let real_id = *self.form_field_placeholder_map.get(&placeholder).ok_or_else(
964                    || {
965                        PdfError::Internal(format!(
966                            "AcroForm writer internal invariant broken: field '{name}' (placeholder {placeholder}) has no pre-allocated real object id — preallocate_form_manager_fields must run before write_catalog"
967                        ))
968                    },
969                )?;
970                sorted.push((form_field.field_dict.clone(), real_id));
971            }
972            for (field_dict, real_id) in sorted {
973                self.write_object(real_id, Object::Dictionary(field_dict))?;
974            }
975
976            if let Some(acro) = document.acro_form.as_mut() {
977                for r in &self.form_manager_field_refs {
978                    if !acro.fields.contains(r) {
979                        acro.fields.push(*r);
980                    }
981                }
982            }
983        }
984
985        // Add AcroForm if present
986        if let Some(acro_form) = &document.acro_form {
987            // Reserve object ID for AcroForm
988            let acro_form_id = self.allocate_object_id();
989
990            // Write AcroForm object
991            self.write_object(acro_form_id, Object::Dictionary(acro_form.to_dict()))?;
992
993            // Reference it in catalog
994            catalog.set("AcroForm", Object::Reference(acro_form_id));
995        }
996
997        // Add Outlines if present
998        if let Some(outline_tree) = &document.outline {
999            if !outline_tree.items.is_empty() {
1000                let outline_root_id = self.write_outline_tree(outline_tree)?;
1001                catalog.set("Outlines", Object::Reference(outline_root_id));
1002            }
1003        }
1004
1005        // Add StructTreeRoot if present (Tagged PDF - ISO 32000-1 §14.8)
1006        if let Some(struct_tree) = &document.struct_tree {
1007            if !struct_tree.is_empty() {
1008                let struct_tree_root_id = self.write_struct_tree(struct_tree)?;
1009                catalog.set("StructTreeRoot", Object::Reference(struct_tree_root_id));
1010                // Mark as Tagged PDF
1011                catalog.set("MarkInfo", {
1012                    let mut mark_info = Dictionary::new();
1013                    mark_info.set("Marked", Object::Boolean(true));
1014                    Object::Dictionary(mark_info)
1015                });
1016            }
1017        }
1018
1019        // Add XMP Metadata stream (ISO 32000-1 §14.3.2)
1020        // Generate XMP from document metadata and embed as stream
1021        let xmp_metadata = document.create_xmp_metadata();
1022        let xmp_packet = xmp_metadata.to_xmp_packet();
1023        let metadata_id = self.allocate_object_id();
1024
1025        // Create metadata stream dictionary
1026        let mut metadata_dict = Dictionary::new();
1027        metadata_dict.set("Type", Object::Name("Metadata".to_string()));
1028        metadata_dict.set("Subtype", Object::Name("XML".to_string()));
1029        metadata_dict.set("Length", Object::Integer(xmp_packet.len() as i64));
1030
1031        // Write XMP metadata stream
1032        self.write_object(
1033            metadata_id,
1034            Object::Stream(metadata_dict, xmp_packet.into_bytes()),
1035        )?;
1036
1037        // Reference it in catalog
1038        catalog.set("Metadata", Object::Reference(metadata_id));
1039
1040        // /OpenAction — ISO 32000-1 §7.7.2 Table 28
1041        if let Some(action) = &document.open_action {
1042            catalog.set("OpenAction", Object::Dictionary(action.to_dict()));
1043        }
1044
1045        // /ViewerPreferences — ISO 32000-1 §7.7.2 Table 28, detailed in §12.2
1046        if let Some(prefs) = &document.viewer_preferences {
1047            catalog.set("ViewerPreferences", Object::Dictionary(prefs.to_dict()));
1048        }
1049
1050        // /Names — ISO 32000-1 §7.7.4 Table 31 (Name Dictionary).
1051        // The /Dests sub-entry is the name tree for named destinations
1052        // (§12.3.2.3). Both the name tree and the Name Dictionary are
1053        // written as indirect objects.
1054        if let Some(named_dests) = &document.named_destinations {
1055            let dests_tree_id = self.allocate_object_id();
1056            self.write_object(dests_tree_id, Object::Dictionary(named_dests.to_dict()))?;
1057
1058            let mut names_dict = Dictionary::new();
1059            names_dict.set("Dests", Object::Reference(dests_tree_id));
1060            let names_dict_id = self.allocate_object_id();
1061            self.write_object(names_dict_id, Object::Dictionary(names_dict))?;
1062
1063            catalog.set("Names", Object::Reference(names_dict_id));
1064        }
1065
1066        // /PageLabels — ISO 32000-1 §7.7.2 Table 28, §12.4.2.
1067        // The value is a number tree; we emit it as an indirect object so
1068        // large documents can grow without reshuffling the catalog.
1069        if let Some(page_labels) = &document.page_labels {
1070            let labels_id = self.allocate_object_id();
1071            self.write_object(labels_id, Object::Dictionary(page_labels.to_dict()))?;
1072            catalog.set("PageLabels", Object::Reference(labels_id));
1073        }
1074
1075        self.write_object(catalog_id, Object::Dictionary(catalog))?;
1076        Ok(())
1077    }
1078
1079    /// Issue #395: build the collision-only rename map for a page's preserved
1080    /// fonts. A preserved `/Font` key is disambiguated only when it collides
1081    /// with a key already present in the page `/Font` dict the writer builds —
1082    /// i.e. one of the unconditionally injected base fonts
1083    /// ([`INJECTED_BASE_FONT_KEYS`]) or an overlay/custom font (`font_refs`).
1084    /// Returns an empty map when the page has no preserved fonts or none
1085    /// collide, so no content rewrite happens in the common case.
1086    fn preserved_font_disambiguation_map(
1087        page: &crate::page::Page,
1088        font_refs: &HashMap<String, ObjectId>,
1089    ) -> HashMap<String, String> {
1090        let preserved_fonts = match page
1091            .get_preserved_resources()
1092            .and_then(|res| res.get("Font"))
1093        {
1094            Some(crate::pdf_objects::Object::Dictionary(fonts)) => fonts,
1095            _ => return HashMap::new(),
1096        };
1097
1098        let mut reserved: std::collections::HashSet<String> =
1099            crate::writer::INJECTED_BASE_FONT_KEYS
1100                .iter()
1101                .map(|s| s.to_string())
1102                .collect();
1103        reserved.extend(font_refs.keys().cloned());
1104
1105        let preserved_keys: Vec<String> = preserved_fonts
1106            .keys()
1107            .map(|k| k.as_str().to_string())
1108            .collect();
1109        crate::writer::collision_font_mapping(preserved_keys.iter().map(|s| s.as_str()), &reserved)
1110    }
1111
1112    fn write_page_content(
1113        &mut self,
1114        content_id: ObjectId,
1115        page: &crate::page::Page,
1116        preserved_font_map: &HashMap<String, String>,
1117    ) -> Result<()> {
1118        let mut page_copy = page.clone();
1119        // Issue #395: drive the preserved-content font rewrite from the same
1120        // collision-only map used for the resource-dict rename.
1121        page_copy.preserved_font_rewrite_map = preserved_font_map.clone();
1122        let content = page_copy.generate_content()?;
1123
1124        // Create stream with compression if enabled
1125        #[cfg(feature = "compression")]
1126        {
1127            use crate::objects::Stream;
1128            let mut stream = Stream::new(content);
1129            // Only compress if config allows it
1130            if self.config.compress_streams {
1131                stream.compress_flate()?;
1132            }
1133
1134            self.write_object(
1135                content_id,
1136                Object::Stream(stream.dictionary().clone(), stream.data().to_vec()),
1137            )?;
1138        }
1139
1140        #[cfg(not(feature = "compression"))]
1141        {
1142            let mut stream_dict = Dictionary::new();
1143            stream_dict.set("Length", Object::Integer(content.len() as i64));
1144
1145            self.write_object(content_id, Object::Stream(stream_dict, content))?;
1146        }
1147
1148        Ok(())
1149    }
1150
1151    fn write_outline_tree(
1152        &mut self,
1153        outline_tree: &crate::structure::OutlineTree,
1154    ) -> Result<ObjectId> {
1155        // Create root outline dictionary
1156        let outline_root_id = self.allocate_object_id();
1157
1158        let mut outline_root = Dictionary::new();
1159        outline_root.set("Type", Object::Name("Outlines".to_string()));
1160
1161        if !outline_tree.items.is_empty() {
1162            // Reserve IDs for all outline items
1163            let mut item_ids = Vec::new();
1164
1165            // Count all items and assign IDs
1166            fn count_items(items: &[crate::structure::OutlineItem]) -> usize {
1167                let mut count = items.len();
1168                for item in items {
1169                    count += count_items(&item.children);
1170                }
1171                count
1172            }
1173
1174            let total_items = count_items(&outline_tree.items);
1175
1176            // Reserve IDs for all items
1177            for _ in 0..total_items {
1178                item_ids.push(self.allocate_object_id());
1179            }
1180
1181            let mut id_index = 0;
1182
1183            // Write root items
1184            let first_id = item_ids[0];
1185            let last_id = item_ids[outline_tree.items.len() - 1];
1186
1187            outline_root.set("First", Object::Reference(first_id));
1188            outline_root.set("Last", Object::Reference(last_id));
1189
1190            // Visible count
1191            let visible_count = outline_tree.visible_count();
1192            outline_root.set("Count", Object::Integer(visible_count));
1193
1194            // Write all items recursively
1195            let mut written_items = Vec::new();
1196
1197            for (i, item) in outline_tree.items.iter().enumerate() {
1198                let item_id = item_ids[id_index];
1199                id_index += 1;
1200
1201                let prev_id = if i > 0 { Some(item_ids[i - 1]) } else { None };
1202                let next_id = if i < outline_tree.items.len() - 1 {
1203                    Some(item_ids[i + 1])
1204                } else {
1205                    None
1206                };
1207
1208                // Write this item and its children
1209                let children_ids = self.write_outline_item(
1210                    item,
1211                    item_id,
1212                    outline_root_id,
1213                    prev_id,
1214                    next_id,
1215                    &mut item_ids,
1216                    &mut id_index,
1217                )?;
1218
1219                written_items.extend(children_ids);
1220            }
1221        }
1222
1223        self.write_object(outline_root_id, Object::Dictionary(outline_root))?;
1224        Ok(outline_root_id)
1225    }
1226
1227    #[allow(clippy::too_many_arguments)]
1228    fn write_outline_item(
1229        &mut self,
1230        item: &crate::structure::OutlineItem,
1231        item_id: ObjectId,
1232        parent_id: ObjectId,
1233        prev_id: Option<ObjectId>,
1234        next_id: Option<ObjectId>,
1235        all_ids: &mut Vec<ObjectId>,
1236        id_index: &mut usize,
1237    ) -> Result<Vec<ObjectId>> {
1238        let mut written_ids = vec![item_id];
1239
1240        // Handle children if any
1241        let (first_child_id, last_child_id) = if !item.children.is_empty() {
1242            let first_idx = *id_index;
1243            let first_id = all_ids[first_idx];
1244            let last_idx = first_idx + item.children.len() - 1;
1245            let last_id = all_ids[last_idx];
1246
1247            // Write children
1248            for (i, child) in item.children.iter().enumerate() {
1249                let child_id = all_ids[*id_index];
1250                *id_index += 1;
1251
1252                let child_prev = if i > 0 {
1253                    Some(all_ids[first_idx + i - 1])
1254                } else {
1255                    None
1256                };
1257                let child_next = if i < item.children.len() - 1 {
1258                    Some(all_ids[first_idx + i + 1])
1259                } else {
1260                    None
1261                };
1262
1263                let child_ids = self.write_outline_item(
1264                    child, child_id, item_id, // This item is the parent
1265                    child_prev, child_next, all_ids, id_index,
1266                )?;
1267
1268                written_ids.extend(child_ids);
1269            }
1270
1271            (Some(first_id), Some(last_id))
1272        } else {
1273            (None, None)
1274        };
1275
1276        // Create item dictionary
1277        let item_dict = crate::structure::outline_item_to_dict(
1278            item,
1279            parent_id,
1280            first_child_id,
1281            last_child_id,
1282            prev_id,
1283            next_id,
1284        );
1285
1286        self.write_object(item_id, Object::Dictionary(item_dict))?;
1287
1288        Ok(written_ids)
1289    }
1290
1291    /// Writes the structure tree for Tagged PDF (ISO 32000-1 §14.8)
1292    fn write_struct_tree(
1293        &mut self,
1294        struct_tree: &crate::structure::StructTree,
1295    ) -> Result<ObjectId> {
1296        // Allocate IDs for StructTreeRoot and all elements
1297        let struct_tree_root_id = self.allocate_object_id();
1298        let mut element_ids = Vec::new();
1299        for _ in 0..struct_tree.len() {
1300            element_ids.push(self.allocate_object_id());
1301        }
1302
1303        // Build parent map: element_index -> parent_id
1304        let mut parent_map: std::collections::HashMap<usize, ObjectId> =
1305            std::collections::HashMap::new();
1306
1307        // Root element's parent is StructTreeRoot
1308        if let Some(root_index) = struct_tree.root_index() {
1309            parent_map.insert(root_index, struct_tree_root_id);
1310
1311            // Recursively map all children to their parents
1312            fn map_children_parents(
1313                tree: &crate::structure::StructTree,
1314                parent_index: usize,
1315                parent_id: ObjectId,
1316                element_ids: &[ObjectId],
1317                parent_map: &mut std::collections::HashMap<usize, ObjectId>,
1318            ) {
1319                if let Some(parent_elem) = tree.get(parent_index) {
1320                    for &child_index in &parent_elem.children {
1321                        parent_map.insert(child_index, parent_id);
1322                        map_children_parents(
1323                            tree,
1324                            child_index,
1325                            element_ids[child_index],
1326                            element_ids,
1327                            parent_map,
1328                        );
1329                    }
1330                }
1331            }
1332
1333            map_children_parents(
1334                struct_tree,
1335                root_index,
1336                element_ids[root_index],
1337                &element_ids,
1338                &mut parent_map,
1339            );
1340        }
1341
1342        // Write all structure elements with parent references
1343        for (index, element) in struct_tree.iter().enumerate() {
1344            let element_id = element_ids[index];
1345            let mut element_dict = Dictionary::new();
1346
1347            element_dict.set("Type", Object::Name("StructElem".to_string()));
1348            element_dict.set("S", Object::Name(element.structure_type.as_pdf_name()));
1349
1350            // Parent reference (ISO 32000-1 §14.7.2 - required)
1351            if let Some(&parent_id) = parent_map.get(&index) {
1352                element_dict.set("P", Object::Reference(parent_id));
1353            }
1354
1355            // Element ID (optional)
1356            if let Some(ref id) = element.id {
1357                element_dict.set("ID", Object::String(id.clone()));
1358            }
1359
1360            // Attributes
1361            if let Some(ref lang) = element.attributes.lang {
1362                element_dict.set("Lang", Object::String(lang.clone()));
1363            }
1364            if let Some(ref alt) = element.attributes.alt {
1365                element_dict.set("Alt", Object::String(alt.clone()));
1366            }
1367            if let Some(ref actual_text) = element.attributes.actual_text {
1368                element_dict.set("ActualText", Object::String(actual_text.clone()));
1369            }
1370            if let Some(ref title) = element.attributes.title {
1371                element_dict.set("T", Object::String(title.clone()));
1372            }
1373            if let Some(bbox) = element.attributes.bbox {
1374                element_dict.set(
1375                    "BBox",
1376                    Object::Array(vec![
1377                        Object::Real(bbox[0]),
1378                        Object::Real(bbox[1]),
1379                        Object::Real(bbox[2]),
1380                        Object::Real(bbox[3]),
1381                    ]),
1382                );
1383            }
1384
1385            // Kids (children elements + marked content references)
1386            let mut kids = Vec::new();
1387
1388            // Add child element references
1389            for &child_index in &element.children {
1390                kids.push(Object::Reference(element_ids[child_index]));
1391            }
1392
1393            // Add marked content references (MCIDs)
1394            for mcid_ref in &element.mcids {
1395                let mut mcr = Dictionary::new();
1396                mcr.set("Type", Object::Name("MCR".to_string()));
1397                mcr.set("Pg", Object::Integer(mcid_ref.page_index as i64));
1398                mcr.set("MCID", Object::Integer(mcid_ref.mcid as i64));
1399                kids.push(Object::Dictionary(mcr));
1400            }
1401
1402            if !kids.is_empty() {
1403                element_dict.set("K", Object::Array(kids));
1404            }
1405
1406            self.write_object(element_id, Object::Dictionary(element_dict))?;
1407        }
1408
1409        // Create StructTreeRoot dictionary
1410        let mut struct_tree_root = Dictionary::new();
1411        struct_tree_root.set("Type", Object::Name("StructTreeRoot".to_string()));
1412
1413        // Add root element(s) as K entry
1414        if let Some(root_index) = struct_tree.root_index() {
1415            struct_tree_root.set("K", Object::Reference(element_ids[root_index]));
1416        }
1417
1418        // Add RoleMap if not empty
1419        if !struct_tree.role_map.mappings().is_empty() {
1420            let mut role_map = Dictionary::new();
1421            for (custom_type, standard_type) in struct_tree.role_map.mappings() {
1422                role_map.set(
1423                    custom_type.as_str(),
1424                    Object::Name(standard_type.as_pdf_name().to_string()),
1425                );
1426            }
1427            struct_tree_root.set("RoleMap", Object::Dictionary(role_map));
1428        }
1429
1430        self.write_object(struct_tree_root_id, Object::Dictionary(struct_tree_root))?;
1431        Ok(struct_tree_root_id)
1432    }
1433
1434    /// Reserve an `ObjectId` for every field owned by `document.form_manager`
1435    /// and build the placeholder → real mapping used when widget annotations
1436    /// are serialised (see `Annotation::field_parent`).
1437    ///
1438    /// Called once from `write_document` before `write_pages`, so widget
1439    /// `/Parent` refs on pages resolve to real indirect objects. The field
1440    /// bodies themselves are written later, in `write_catalog`, reusing
1441    /// these pre-allocated IDs.
1442    ///
1443    /// Iteration order is deterministic (alphabetical by field name) via
1444    /// `FormManager::iter_fields_sorted` so object-ID allocation — and
1445    /// therefore the byte-for-byte output — is reproducible across builds.
1446    fn preallocate_form_manager_fields(&mut self, document: &Document) -> Result<()> {
1447        let Some(form_manager) = &document.form_manager else {
1448            return Ok(());
1449        };
1450
1451        for (_name, _form_field, placeholder) in form_manager.iter_fields_sorted() {
1452            let real_id = self.allocate_object_id();
1453            self.form_field_placeholder_map.insert(placeholder, real_id);
1454            self.form_manager_field_refs.push(real_id);
1455        }
1456        Ok(())
1457    }
1458
1459    fn write_form_fields(&mut self, document: &mut Document) -> Result<()> {
1460        // Add collected form field IDs to AcroForm
1461        if !self.form_field_ids.is_empty() {
1462            if let Some(acro_form) = &mut document.acro_form {
1463                // Clear any existing fields and add the ones we found
1464                acro_form.fields.clear();
1465                for field_id in &self.form_field_ids {
1466                    acro_form.add_field(*field_id);
1467                }
1468
1469                // Ensure AcroForm has the right properties
1470                acro_form.need_appearances = true;
1471                if acro_form.da.is_none() {
1472                    acro_form.da = Some("/Helv 12 Tf 0 g".to_string());
1473                }
1474            }
1475        }
1476        Ok(())
1477    }
1478
1479    fn write_info(&mut self, document: &Document) -> Result<()> {
1480        let info_id = self.get_info_id()?;
1481        let mut info_dict = Dictionary::new();
1482
1483        if let Some(ref title) = document.metadata.title {
1484            info_dict.set("Title", Object::String(title.clone()));
1485        }
1486        if let Some(ref author) = document.metadata.author {
1487            info_dict.set("Author", Object::String(author.clone()));
1488        }
1489        if let Some(ref subject) = document.metadata.subject {
1490            info_dict.set("Subject", Object::String(subject.clone()));
1491        }
1492        if let Some(ref keywords) = document.metadata.keywords {
1493            info_dict.set("Keywords", Object::String(keywords.clone()));
1494        }
1495        if let Some(ref creator) = document.metadata.creator {
1496            info_dict.set("Creator", Object::String(creator.clone()));
1497        }
1498        if let Some(ref producer) = document.metadata.producer {
1499            info_dict.set("Producer", Object::String(producer.clone()));
1500        }
1501
1502        // Add creation date
1503        if let Some(creation_date) = document.metadata.creation_date {
1504            let date_string = format_pdf_date(creation_date);
1505            info_dict.set("CreationDate", Object::String(date_string));
1506        }
1507
1508        // Add modification date
1509        if let Some(mod_date) = document.metadata.modification_date {
1510            let date_string = format_pdf_date(mod_date);
1511            info_dict.set("ModDate", Object::String(date_string));
1512        }
1513
1514        // Add PDF signature (anti-spoofing and licensing)
1515        // This is written AFTER user-configurable metadata so it cannot be overridden
1516        let edition = super::Edition::OpenSource;
1517
1518        let signature = super::PdfSignature::new(document, edition);
1519        signature.write_to_info_dict(&mut info_dict);
1520
1521        self.write_object(info_id, Object::Dictionary(info_dict))?;
1522        Ok(())
1523    }
1524
1525    fn write_fonts(&mut self, document: &Document) -> Result<HashMap<String, ObjectId>> {
1526        let mut font_refs = HashMap::new();
1527
1528        // Write custom fonts from the document. Fonts registered via
1529        // `add_font_from_bytes` but never referenced from any content
1530        // stream (i.e. never `set_font`'d on any page) are skipped —
1531        // embedding them waste space and was the direct cause of
1532        // issue #204 (two fonts in the same family both getting
1533        // subsetted with the active font's character set). The
1534        // per-font map is built during tracking by
1535        // `GraphicsContext::record_used_chars` / its `TextContext`
1536        // counterpart.
1537        for font_name in document.custom_font_names() {
1538            let has_usage = self
1539                .document_used_chars_by_font
1540                .get(&font_name)
1541                .map(|chars| !chars.is_empty())
1542                .unwrap_or(false);
1543            if !has_usage {
1544                continue;
1545            }
1546            if let Some(font) = document.get_custom_font(&font_name) {
1547                // For now, write all custom fonts as TrueType with Identity-H for Unicode support
1548                // The font from document is Arc<fonts::Font>, not text::font_manager::CustomFont
1549                let font_id = self.write_font_with_unicode_support(&font_name, &font)?;
1550                font_refs.insert(font_name.clone(), font_id);
1551            }
1552        }
1553
1554        // CID-keyed fonts (issue #358): registered explicitly for positioned
1555        // glyph-run drawing. Emitted unconditionally (registration is the intent
1556        // to use) and embedded whole; the CID semantics come from the caller's
1557        // `CidMapping`. Kept separate from the Unicode-keyed fonts above.
1558        // Deterministic order so output is reproducible (BTreeMap-style sort).
1559        let mut cid_fonts: Vec<(&String, &(Vec<u8>, crate::fonts::CidMapping))> =
1560            document.cid_keyed_fonts().iter().collect();
1561        cid_fonts.sort_by(|a, b| a.0.cmp(b.0));
1562        for (font_name, (data, mapping)) in cid_fonts {
1563            let font_id = self.write_cid_keyed_font(font_name, data, mapping)?;
1564            font_refs.insert(font_name.clone(), font_id);
1565        }
1566
1567        Ok(font_refs)
1568    }
1569
1570    /// Write font with automatic Unicode support detection
1571    fn write_font_with_unicode_support(
1572        &mut self,
1573        font_name: &str,
1574        font: &crate::fonts::Font,
1575    ) -> Result<ObjectId> {
1576        // Check if any text in the document needs Unicode
1577        // For simplicity, always use Type0 for full Unicode support
1578        self.write_type0_font_from_font(font_name, font)
1579    }
1580
1581    /// Write a Type0 font with CID support from fonts::Font
1582    fn write_type0_font_from_font(
1583        &mut self,
1584        font_name: &str,
1585        font: &crate::fonts::Font,
1586    ) -> Result<ObjectId> {
1587        // Per-font character set for subsetting (issue #204). Falls
1588        // back to a small ASCII/digit set only when the document
1589        // tracked no characters at all for this font — the ancient
1590        // code path pre-dating char tracking. Post-fix this fallback
1591        // shouldn't fire for any font reached through `write_fonts`
1592        // because that path already filters unused fonts out.
1593        let used_chars = self
1594            .document_used_chars_by_font
1595            .get(font_name)
1596            .cloned()
1597            .unwrap_or_else(|| {
1598                let mut chars = std::collections::HashSet::new();
1599                for ch in
1600                    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,!?".chars()
1601                {
1602                    chars.insert(ch);
1603                }
1604                chars
1605            });
1606
1607        // Diagnose characters the embedded font has no glyph for: they render
1608        // as .notdef (empty boxes). This is correct when the font genuinely
1609        // lacks the glyph, but warn so it is not a silent failure — the most
1610        // common cause of "my ✓/✗ show as boxes" reports (issue #287).
1611        // Fires once per font per save; a document saved repeatedly logs it
1612        // each time. `missing_glyphs` returns nothing when coverage is unknown
1613        // (e.g. an unparseable cmap), so this never produces false positives.
1614        let used_text: String = used_chars.iter().copied().collect();
1615        let mut missing = font.missing_glyphs(&used_text);
1616        if !missing.is_empty() {
1617            missing.sort_unstable();
1618            let list = missing
1619                .iter()
1620                .map(|c| format!("U+{:04X} {:?}", *c as u32, c))
1621                .collect::<Vec<_>>()
1622                .join(", ");
1623            tracing::warn!(
1624                "Custom font '{}' has no glyph for {} character(s): {}. \
1625                 They will render as .notdef (empty boxes); the embedded font \
1626                 does not contain these glyphs.",
1627                font_name,
1628                missing.len(),
1629                list
1630            );
1631        }
1632
1633        // Allocate IDs for all font objects
1634        let font_id = self.allocate_object_id();
1635        let descendant_font_id = self.allocate_object_id();
1636        let descriptor_id = self.allocate_object_id();
1637        let font_file_id = self.allocate_object_id();
1638        let to_unicode_id = self.allocate_object_id();
1639
1640        // Write font file. Large fonts are subsetted; the subsetter always
1641        // emits raw CFF for OpenType/CFF fonts, so OpenType font files are
1642        // embedded with /CIDFontType0C. TrueType fonts keep the SFNT wrapper.
1643        // IMPORTANT: We need the ORIGINAL font for width calculations, not the subset.
1644        let (font_data_to_embed, subset_glyph_mapping, original_font_for_widths) =
1645            if font.data.len() > 100_000 && !used_chars.is_empty() {
1646                match crate::text::fonts::truetype_subsetter::subset_font(
1647                    font.data.clone(),
1648                    &used_chars,
1649                ) {
1650                    Ok(subset_result) => (
1651                        subset_result.font_data,
1652                        Some(subset_result.glyph_mapping),
1653                        font.clone(),
1654                    ),
1655                    Err(_) => {
1656                        if font.data.len() < 25_000_000 {
1657                            (font.data.clone(), None, font.clone())
1658                        } else {
1659                            (Vec::new(), None, font.clone())
1660                        }
1661                    }
1662                }
1663            } else {
1664                (font.data.clone(), None, font.clone())
1665            };
1666
1667        if !font_data_to_embed.is_empty() {
1668            // Build the initial font-file dictionary carrying the format-specific
1669            // metadata. `/Length1` (uncompressed byte count) is required for
1670            // TrueType FontFile2 streams per ISO 32000-1 §9.9. `/Subtype
1671            // /CIDFontType0C` marks raw CFF bytes for OpenType FontFile3 streams.
1672            let mut font_file_dict = Dictionary::new();
1673            match font.format {
1674                crate::fonts::FontFormat::OpenType => {
1675                    font_file_dict.set("Subtype", Object::Name("CIDFontType0C".to_string()));
1676                }
1677                crate::fonts::FontFormat::TrueType => {
1678                    font_file_dict.set("Length1", Object::Integer(font_data_to_embed.len() as i64));
1679                }
1680            }
1681
1682            // Compress the font-file stream when the `compression` feature is
1683            // active and the writer config permits it. Uncompressed TTF glyf
1684            // data in particular compresses 60-70% with zlib — a 666 KB
1685            // subset PDF drops to under 200 KB after compression.
1686            #[cfg(feature = "compression")]
1687            {
1688                let font_stream_obj = if self.config.compress_streams {
1689                    let mut stream =
1690                        crate::objects::Stream::with_dictionary(font_file_dict, font_data_to_embed);
1691                    stream.compress_flate()?;
1692                    Object::Stream(stream.dictionary().clone(), stream.data().to_vec())
1693                } else {
1694                    Object::Stream(font_file_dict, font_data_to_embed)
1695                };
1696                self.write_object(font_file_id, font_stream_obj)?;
1697            }
1698            #[cfg(not(feature = "compression"))]
1699            {
1700                let font_stream_obj = Object::Stream(font_file_dict, font_data_to_embed);
1701                self.write_object(font_file_id, font_stream_obj)?;
1702            }
1703        } else {
1704            // No font data to embed
1705            let font_file_dict = Dictionary::new();
1706            let font_stream_obj = Object::Stream(font_file_dict, Vec::new());
1707            self.write_object(font_file_id, font_stream_obj)?;
1708        }
1709
1710        // Write font descriptor
1711        let mut descriptor = Dictionary::new();
1712        descriptor.set("Type", Object::Name("FontDescriptor".to_string()));
1713        descriptor.set("FontName", Object::Name(font_name.to_string()));
1714        descriptor.set("Flags", Object::Integer(4)); // Symbolic font
1715        descriptor.set(
1716            "FontBBox",
1717            Object::Array(vec![
1718                Object::Integer(font.descriptor.font_bbox[0] as i64),
1719                Object::Integer(font.descriptor.font_bbox[1] as i64),
1720                Object::Integer(font.descriptor.font_bbox[2] as i64),
1721                Object::Integer(font.descriptor.font_bbox[3] as i64),
1722            ]),
1723        );
1724        descriptor.set(
1725            "ItalicAngle",
1726            Object::Real(font.descriptor.italic_angle as f64),
1727        );
1728        descriptor.set("Ascent", Object::Real(font.descriptor.ascent as f64));
1729        descriptor.set("Descent", Object::Real(font.descriptor.descent as f64));
1730        descriptor.set("CapHeight", Object::Real(font.descriptor.cap_height as f64));
1731        descriptor.set("StemV", Object::Real(font.descriptor.stem_v as f64));
1732        // Use appropriate FontFile type based on font format
1733        let font_file_key = match font.format {
1734            crate::fonts::FontFormat::OpenType => "FontFile3", // CFF/OpenType fonts
1735            crate::fonts::FontFormat::TrueType => "FontFile2", // TrueType fonts
1736        };
1737        descriptor.set(font_file_key, Object::Reference(font_file_id));
1738        self.write_object(descriptor_id, Object::Dictionary(descriptor))?;
1739
1740        // Write CIDFont (descendant font)
1741        let mut cid_font = Dictionary::new();
1742        cid_font.set("Type", Object::Name("Font".to_string()));
1743        // ISO 32000-1 §9.7.4: CIDFontType0 for CFF/OpenType, CIDFontType2 for TrueType.
1744        let cid_font_subtype = match font.format {
1745            crate::fonts::FontFormat::OpenType => "CIDFontType0",
1746            crate::fonts::FontFormat::TrueType => "CIDFontType2",
1747        };
1748        cid_font.set("Subtype", Object::Name(cid_font_subtype.to_string()));
1749        cid_font.set("BaseFont", Object::Name(font_name.to_string()));
1750
1751        // CIDSystemInfo - Use appropriate values for CJK fonts
1752        let mut cid_system_info = Dictionary::new();
1753        let (registry, ordering, supplement) =
1754            if let Some(cjk_type) = CjkFontType::detect_from_name(font_name) {
1755                cjk_type.cid_system_info()
1756            } else {
1757                ("Adobe", "Identity", 0)
1758            };
1759
1760        cid_system_info.set("Registry", Object::String(registry.to_string()));
1761        cid_system_info.set("Ordering", Object::String(ordering.to_string()));
1762        cid_system_info.set("Supplement", Object::Integer(supplement as i64));
1763        cid_font.set("CIDSystemInfo", Object::Dictionary(cid_system_info));
1764
1765        cid_font.set("FontDescriptor", Object::Reference(descriptor_id));
1766
1767        // Calculate a better default width based on font metrics
1768        let default_width = self.calculate_default_width(font);
1769        cid_font.set("DW", Object::Integer(default_width));
1770
1771        // Generate proper width array from font metrics
1772        // IMPORTANT: Use the ORIGINAL font for width calculations, not the subset
1773        // But pass the subset mapping to know which characters we're using
1774        let w_array = self.generate_width_array(
1775            &original_font_for_widths,
1776            default_width,
1777            subset_glyph_mapping.as_ref(),
1778        );
1779        cid_font.set("W", Object::Array(w_array));
1780
1781        // CIDToGIDMap - Only required for CIDFontType2 (TrueType)
1782        // For CIDFontType0 (CFF/OpenType), CIDToGIDMap should NOT be present per ISO 32000-1:2008 §9.7.4.2
1783        // CFF fonts use CIDs directly as glyph identifiers, so no mapping is needed
1784        if cid_font_subtype == "CIDFontType2" {
1785            // TrueType fonts need CIDToGIDMap to map CIDs (Unicode code points) to Glyph IDs
1786            let cid_to_gid_map =
1787                self.generate_cid_to_gid_map(font_name, font, subset_glyph_mapping.as_ref())?;
1788            if !cid_to_gid_map.is_empty() {
1789                // Write the CIDToGIDMap as a stream, FlateDecode-compressed
1790                // when possible. The raw map is dimensioned to the highest
1791                // codepoint in use and is mostly zeros (only mapped code
1792                // points carry a 2-byte GID), so Flate compression typically
1793                // crushes it by 95-99%. For CJK-heavy documents this is the
1794                // difference between a 130 KB map (Issue #165) and a ~1 KB
1795                // stream.
1796                let cid_to_gid_map_id = self.allocate_object_id();
1797                let map_dict = Dictionary::new();
1798                #[cfg(feature = "compression")]
1799                let map_stream = if self.config.compress_streams {
1800                    let mut stream =
1801                        crate::objects::Stream::with_dictionary(map_dict, cid_to_gid_map);
1802                    stream.compress_flate()?;
1803                    Object::Stream(stream.dictionary().clone(), stream.data().to_vec())
1804                } else {
1805                    let mut d = map_dict;
1806                    d.set("Length", Object::Integer(cid_to_gid_map.len() as i64));
1807                    Object::Stream(d, cid_to_gid_map)
1808                };
1809                #[cfg(not(feature = "compression"))]
1810                let map_stream = {
1811                    let mut d = map_dict;
1812                    d.set("Length", Object::Integer(cid_to_gid_map.len() as i64));
1813                    Object::Stream(d, cid_to_gid_map)
1814                };
1815                self.write_object(cid_to_gid_map_id, map_stream)?;
1816                cid_font.set("CIDToGIDMap", Object::Reference(cid_to_gid_map_id));
1817            } else {
1818                cid_font.set("CIDToGIDMap", Object::Name("Identity".to_string()));
1819            }
1820        }
1821        // Note: For CIDFontType0 (CFF), we intentionally omit CIDToGIDMap
1822
1823        self.write_object(descendant_font_id, Object::Dictionary(cid_font))?;
1824
1825        // Write ToUnicode CMap. The CMap is filtered to the characters that
1826        // actually appear in the document (via `document_used_chars`) and the
1827        // stream is FlateDecode-compressed when the `compression` feature and
1828        // writer config allow it. The unfiltered, uncompressed version used to
1829        // dominate PDF output (~14 KB for a 2-char Latin document).
1830        let cmap_data = self.generate_tounicode_cmap_from_font(font_name, font);
1831        let cmap_dict = Dictionary::new();
1832        #[cfg(feature = "compression")]
1833        let cmap_stream = if self.config.compress_streams {
1834            let mut stream = crate::objects::Stream::with_dictionary(cmap_dict, cmap_data);
1835            stream.compress_flate()?;
1836            Object::Stream(stream.dictionary().clone(), stream.data().to_vec())
1837        } else {
1838            Object::Stream(cmap_dict, cmap_data)
1839        };
1840        #[cfg(not(feature = "compression"))]
1841        let cmap_stream = Object::Stream(cmap_dict, cmap_data);
1842        self.write_object(to_unicode_id, cmap_stream)?;
1843
1844        // Write Type0 font (main font)
1845        let mut type0_font = Dictionary::new();
1846        type0_font.set("Type", Object::Name("Font".to_string()));
1847        type0_font.set("Subtype", Object::Name("Type0".to_string()));
1848        type0_font.set("BaseFont", Object::Name(font_name.to_string()));
1849        type0_font.set("Encoding", Object::Name("Identity-H".to_string()));
1850        type0_font.set(
1851            "DescendantFonts",
1852            Object::Array(vec![Object::Reference(descendant_font_id)]),
1853        );
1854        type0_font.set("ToUnicode", Object::Reference(to_unicode_id));
1855
1856        self.write_object(font_id, Object::Dictionary(type0_font))?;
1857
1858        Ok(font_id)
1859    }
1860
1861    /// Write a CID-keyed Type0 font (issue #358) from explicit font bytes and a
1862    /// caller-supplied [`CidMapping`](crate::fonts::CidMapping).
1863    ///
1864    /// Unlike [`write_type0_font_from_font`](Self::write_type0_font_from_font),
1865    /// which is Unicode-keyed (CID = Unicode code point, subset by characters),
1866    /// this emits a `CIDFontType2` whose content-stream codes are the CIDs in
1867    /// `mapping`. `CIDToGIDMap` / `ToUnicode` / `/W` all come from the mapping's
1868    /// generators, so the run is drawn by glyph id and stays extractable. The
1869    /// font is embedded whole (no subsetting in this iteration).
1870    fn write_cid_keyed_font(
1871        &mut self,
1872        font_name: &str,
1873        data: &[u8],
1874        mapping: &crate::fonts::CidMapping,
1875    ) -> Result<ObjectId> {
1876        use crate::text::fonts::truetype::TrueTypeFont;
1877
1878        // Parse once for the descriptor/metrics and once for glyph advances.
1879        // Both use the ORIGINAL font: descriptor metrics are font-global and /W
1880        // advances are looked up by original GID (invariant under renumbering).
1881        let font = crate::fonts::Font::from_bytes(font_name, data.to_vec())?;
1882        let tt_font = TrueTypeFont::parse(data.to_vec())?;
1883
1884        // #358 Fase 2: subset the embedded font to exactly the glyphs drawn via
1885        // `show_cid_array`. The used GIDs are the values of `cid_to_gid` (the
1886        // consumer registers exactly the run's glyphs). The content stream keeps
1887        // using original GIDs as CIDs; `CIDToGIDMap` (below) bridges them to the
1888        // subset's compacted GID space via `gid_remap`. On any failure, fall back
1889        // to embedding the full font with an unchanged CIDToGIDMap.
1890        let used_gids: std::collections::HashSet<u16> =
1891            mapping.cid_to_gid.values().copied().collect();
1892        let (embed_bytes, gid_remap): (Vec<u8>, Option<std::collections::HashMap<u16, u16>>) =
1893            match crate::text::fonts::truetype_subsetter::subset_font_by_gids(
1894                data.to_vec(),
1895                &used_gids,
1896            ) {
1897                Ok(subset) => (subset.font_data, Some(subset.old_to_new)),
1898                Err(e) => {
1899                    tracing::debug!("CID-keyed subsetting failed ({e:?}); embedding full font");
1900                    (data.to_vec(), None)
1901                }
1902            };
1903
1904        let font_id = self.allocate_object_id();
1905        let descendant_font_id = self.allocate_object_id();
1906        let descriptor_id = self.allocate_object_id();
1907        let font_file_id = self.allocate_object_id();
1908        let to_unicode_id = self.allocate_object_id();
1909
1910        // FontFile2 stream — subset font, /Length1 = uncompressed byte count
1911        // (ISO 32000-1 §9.9), FlateDecode-compressed when configured.
1912        let mut font_file_dict = Dictionary::new();
1913        font_file_dict.set("Length1", Object::Integer(embed_bytes.len() as i64));
1914        #[cfg(feature = "compression")]
1915        {
1916            let font_stream_obj = if self.config.compress_streams {
1917                let mut stream =
1918                    crate::objects::Stream::with_dictionary(font_file_dict, embed_bytes);
1919                stream.compress_flate()?;
1920                Object::Stream(stream.dictionary().clone(), stream.data().to_vec())
1921            } else {
1922                let mut d = font_file_dict;
1923                d.set("Length", Object::Integer(embed_bytes.len() as i64));
1924                Object::Stream(d, embed_bytes)
1925            };
1926            self.write_object(font_file_id, font_stream_obj)?;
1927        }
1928        #[cfg(not(feature = "compression"))]
1929        {
1930            let mut d = font_file_dict;
1931            d.set("Length", Object::Integer(embed_bytes.len() as i64));
1932            self.write_object(font_file_id, Object::Stream(d, embed_bytes))?;
1933        }
1934
1935        // FontDescriptor — reuse the parsed font's metrics.
1936        let mut descriptor = Dictionary::new();
1937        descriptor.set("Type", Object::Name("FontDescriptor".to_string()));
1938        descriptor.set("FontName", Object::Name(font_name.to_string()));
1939        descriptor.set("Flags", Object::Integer(4)); // Symbolic
1940        descriptor.set(
1941            "FontBBox",
1942            Object::Array(vec![
1943                Object::Integer(font.descriptor.font_bbox[0] as i64),
1944                Object::Integer(font.descriptor.font_bbox[1] as i64),
1945                Object::Integer(font.descriptor.font_bbox[2] as i64),
1946                Object::Integer(font.descriptor.font_bbox[3] as i64),
1947            ]),
1948        );
1949        descriptor.set(
1950            "ItalicAngle",
1951            Object::Real(font.descriptor.italic_angle as f64),
1952        );
1953        descriptor.set("Ascent", Object::Real(font.descriptor.ascent as f64));
1954        descriptor.set("Descent", Object::Real(font.descriptor.descent as f64));
1955        descriptor.set("CapHeight", Object::Real(font.descriptor.cap_height as f64));
1956        descriptor.set("StemV", Object::Real(font.descriptor.stem_v as f64));
1957        descriptor.set("FontFile2", Object::Reference(font_file_id));
1958        self.write_object(descriptor_id, Object::Dictionary(descriptor))?;
1959
1960        // CIDFont (descendant). CIDFontType2 — TrueType only (validated at
1961        // registration time in `Document::add_cid_keyed_font`).
1962        let mut cid_font = Dictionary::new();
1963        cid_font.set("Type", Object::Name("Font".to_string()));
1964        cid_font.set("Subtype", Object::Name("CIDFontType2".to_string()));
1965        cid_font.set("BaseFont", Object::Name(font_name.to_string()));
1966        let mut cid_system_info = Dictionary::new();
1967        cid_system_info.set("Registry", Object::String("Adobe".to_string()));
1968        cid_system_info.set("Ordering", Object::String("Identity".to_string()));
1969        cid_system_info.set("Supplement", Object::Integer(0));
1970        cid_font.set("CIDSystemInfo", Object::Dictionary(cid_system_info));
1971        cid_font.set("FontDescriptor", Object::Reference(descriptor_id));
1972        cid_font.set("DW", Object::Integer(self.calculate_default_width(&font)));
1973
1974        // /W widths array from the mapping's per-CID advances (units already
1975        // normalised to 1000/em by the generator). Each entry is a singleton
1976        // `cid [ width ]`.
1977        let w_tuples = mapping.generate_width_array(&tt_font)?;
1978        let mut w_array: Vec<Object> = Vec::new();
1979        for (cid, _cid_last, width) in w_tuples {
1980            w_array.push(Object::Integer(cid as i64));
1981            w_array.push(Object::Array(vec![Object::Integer(width as i64)]));
1982        }
1983        cid_font.set("W", Object::Array(w_array));
1984
1985        // CIDToGIDMap: when the font was subsetted, remap each CID's GID to its
1986        // compacted id in the subset (so a CID = original GID resolves to the
1987        // right glyph in the smaller font). An empty generator output means
1988        // CID == GID for all entries → the `/Identity` name; otherwise a stream.
1989        let cid_to_gid_map = match &gid_remap {
1990            Some(remap) => {
1991                let mut remapped = mapping.clone();
1992                remapped.cid_to_gid = mapping
1993                    .cid_to_gid
1994                    .iter()
1995                    .map(|(&cid, &old_gid)| (cid, remap.get(&old_gid).copied().unwrap_or(0)))
1996                    .collect();
1997                remapped.generate_cid_to_gid_map()
1998            }
1999            None => mapping.generate_cid_to_gid_map(),
2000        };
2001        if cid_to_gid_map.is_empty() {
2002            cid_font.set("CIDToGIDMap", Object::Name("Identity".to_string()));
2003        } else {
2004            let cid_to_gid_map_id = self.allocate_object_id();
2005            let map_dict = Dictionary::new();
2006            #[cfg(feature = "compression")]
2007            let map_stream = if self.config.compress_streams {
2008                let mut stream = crate::objects::Stream::with_dictionary(map_dict, cid_to_gid_map);
2009                stream.compress_flate()?;
2010                Object::Stream(stream.dictionary().clone(), stream.data().to_vec())
2011            } else {
2012                let mut d = map_dict;
2013                d.set("Length", Object::Integer(cid_to_gid_map.len() as i64));
2014                Object::Stream(d, cid_to_gid_map)
2015            };
2016            #[cfg(not(feature = "compression"))]
2017            let map_stream = {
2018                let mut d = map_dict;
2019                d.set("Length", Object::Integer(cid_to_gid_map.len() as i64));
2020                Object::Stream(d, cid_to_gid_map)
2021            };
2022            self.write_object(cid_to_gid_map_id, map_stream)?;
2023            cid_font.set("CIDToGIDMap", Object::Reference(cid_to_gid_map_id));
2024        }
2025        self.write_object(descendant_font_id, Object::Dictionary(cid_font))?;
2026
2027        // ToUnicode CMap from the mapping (CID → Unicode), so extraction works.
2028        let cmap_data = mapping.generate_tounicode_cmap();
2029        let cmap_dict = Dictionary::new();
2030        #[cfg(feature = "compression")]
2031        let cmap_stream = if self.config.compress_streams {
2032            let mut stream = crate::objects::Stream::with_dictionary(cmap_dict, cmap_data);
2033            stream.compress_flate()?;
2034            Object::Stream(stream.dictionary().clone(), stream.data().to_vec())
2035        } else {
2036            let mut d = cmap_dict;
2037            d.set("Length", Object::Integer(cmap_data.len() as i64));
2038            Object::Stream(d, cmap_data)
2039        };
2040        #[cfg(not(feature = "compression"))]
2041        let cmap_stream = {
2042            let mut d = cmap_dict;
2043            d.set("Length", Object::Integer(cmap_data.len() as i64));
2044            Object::Stream(d, cmap_data)
2045        };
2046        self.write_object(to_unicode_id, cmap_stream)?;
2047
2048        // Type0 wrapper.
2049        let mut type0_font = Dictionary::new();
2050        type0_font.set("Type", Object::Name("Font".to_string()));
2051        type0_font.set("Subtype", Object::Name("Type0".to_string()));
2052        type0_font.set("BaseFont", Object::Name(font_name.to_string()));
2053        type0_font.set("Encoding", Object::Name("Identity-H".to_string()));
2054        type0_font.set(
2055            "DescendantFonts",
2056            Object::Array(vec![Object::Reference(descendant_font_id)]),
2057        );
2058        type0_font.set("ToUnicode", Object::Reference(to_unicode_id));
2059        self.write_object(font_id, Object::Dictionary(type0_font))?;
2060
2061        Ok(font_id)
2062    }
2063
2064    /// Calculate default width based on common characters
2065    fn calculate_default_width(&self, font: &crate::fonts::Font) -> i64 {
2066        use crate::text::fonts::truetype::TrueTypeFont;
2067
2068        // Try to calculate from actual font metrics
2069        if let Ok(tt_font) = TrueTypeFont::parse(font.data.clone()) {
2070            if let Ok(cmap_tables) = tt_font.parse_cmap() {
2071                if let Some(cmap) = CmapSubtable::select_best_or_first(&cmap_tables) {
2072                    if let Ok(widths) = tt_font.get_glyph_widths(&cmap.mappings) {
2073                        // NOTE: get_glyph_widths already returns widths in PDF units (1000 per em)
2074
2075                        // Calculate average width of common Latin characters
2076                        let common_chars =
2077                            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ";
2078                        let mut total_width = 0;
2079                        let mut count = 0;
2080
2081                        for ch in common_chars.chars() {
2082                            let unicode = ch as u32;
2083                            if let Some(&pdf_width) = widths.get(&unicode) {
2084                                total_width += pdf_width as i64;
2085                                count += 1;
2086                            }
2087                        }
2088
2089                        if count > 0 {
2090                            return total_width / count;
2091                        }
2092                    }
2093                }
2094            }
2095        }
2096
2097        // Fallback default if we can't calculate
2098        500
2099    }
2100
2101    /// Generate width array for CID font
2102    fn generate_width_array(
2103        &self,
2104        font: &crate::fonts::Font,
2105        _default_width: i64,
2106        subset_mapping: Option<&HashMap<u32, u16>>,
2107    ) -> Vec<Object> {
2108        use crate::text::fonts::truetype::TrueTypeFont;
2109
2110        let mut w_array = Vec::new();
2111
2112        // Try to get actual glyph widths from the font
2113        if let Ok(tt_font) = TrueTypeFont::parse(font.data.clone()) {
2114            // IMPORTANT: Always use ORIGINAL mappings for width calculation
2115            // The subset_mapping has NEW GlyphIDs which don't correspond to the right glyphs
2116            // in the original font's width table
2117            let char_to_glyph = {
2118                // Parse cmap to get original mappings
2119                if let Ok(cmap_tables) = tt_font.parse_cmap() {
2120                    if let Some(cmap) = CmapSubtable::select_best_or_first(&cmap_tables) {
2121                        // If we have subset_mapping, filter to only include used characters
2122                        if let Some(subset_map) = subset_mapping {
2123                            let mut filtered = HashMap::new();
2124                            for unicode in subset_map.keys() {
2125                                // Get the ORIGINAL GlyphID for this Unicode
2126                                if let Some(&orig_glyph) = cmap.mappings.get(unicode) {
2127                                    filtered.insert(*unicode, orig_glyph);
2128                                }
2129                            }
2130                            filtered
2131                        } else {
2132                            cmap.mappings.clone()
2133                        }
2134                    } else {
2135                        HashMap::new()
2136                    }
2137                } else {
2138                    HashMap::new()
2139                }
2140            };
2141
2142            if !char_to_glyph.is_empty() {
2143                // Get actual widths from the font
2144                if let Ok(widths) = tt_font.get_glyph_widths(&char_to_glyph) {
2145                    // NOTE: get_glyph_widths already returns widths scaled to PDF units (1000 per em)
2146                    // So we DON'T need to scale them again here
2147
2148                    // Group consecutive characters with same width for efficiency
2149                    let mut sorted_chars: Vec<_> = widths.iter().collect();
2150                    sorted_chars.sort_by_key(|(unicode, _)| *unicode);
2151
2152                    let mut i = 0;
2153                    while i < sorted_chars.len() {
2154                        let start_unicode = *sorted_chars[i].0;
2155                        // Width is already in PDF units from get_glyph_widths
2156                        let pdf_width = *sorted_chars[i].1 as i64;
2157
2158                        // Find consecutive characters with same width
2159                        let mut end_unicode = start_unicode;
2160                        let mut j = i + 1;
2161                        while j < sorted_chars.len() && *sorted_chars[j].0 == end_unicode + 1 {
2162                            let next_pdf_width = *sorted_chars[j].1 as i64;
2163                            if next_pdf_width == pdf_width {
2164                                end_unicode = *sorted_chars[j].0;
2165                                j += 1;
2166                            } else {
2167                                break;
2168                            }
2169                        }
2170
2171                        // Add to W array
2172                        if start_unicode == end_unicode {
2173                            // Single character
2174                            w_array.push(Object::Integer(start_unicode as i64));
2175                            w_array.push(Object::Array(vec![Object::Integer(pdf_width)]));
2176                        } else {
2177                            // Range of characters
2178                            w_array.push(Object::Integer(start_unicode as i64));
2179                            w_array.push(Object::Integer(end_unicode as i64));
2180                            w_array.push(Object::Integer(pdf_width));
2181                        }
2182
2183                        i = j;
2184                    }
2185
2186                    return w_array;
2187                }
2188            }
2189        }
2190
2191        // Fallback to reasonable default widths if we can't parse the font
2192        let ranges = vec![
2193            // Space character should be narrower
2194            (0x20, 0x20, 250), // Space
2195            (0x21, 0x2F, 333), // Punctuation
2196            (0x30, 0x39, 500), // Numbers (0-9)
2197            (0x3A, 0x40, 333), // More punctuation
2198            (0x41, 0x5A, 667), // Uppercase letters (A-Z)
2199            (0x5B, 0x60, 333), // Brackets
2200            (0x61, 0x7A, 500), // Lowercase letters (a-z)
2201            (0x7B, 0x7E, 333), // More brackets
2202            // Extended Latin
2203            (0xA0, 0xA0, 250), // Non-breaking space
2204            (0xA1, 0xBF, 333), // Latin-1 punctuation
2205            (0xC0, 0xD6, 667), // Latin-1 uppercase
2206            (0xD7, 0xD7, 564), // Multiplication sign
2207            (0xD8, 0xDE, 667), // More Latin-1 uppercase
2208            (0xDF, 0xF6, 500), // Latin-1 lowercase
2209            (0xF7, 0xF7, 564), // Division sign
2210            (0xF8, 0xFF, 500), // More Latin-1 lowercase
2211            // Latin Extended-A
2212            (0x100, 0x17F, 500), // Latin Extended-A
2213            // Symbols and special characters
2214            (0x2000, 0x200F, 250), // Various spaces
2215            (0x2010, 0x2027, 333), // Hyphens and dashes
2216            (0x2028, 0x202F, 250), // More spaces
2217            (0x2030, 0x206F, 500), // General Punctuation
2218            (0x2070, 0x209F, 400), // Superscripts
2219            (0x20A0, 0x20CF, 600), // Currency symbols
2220            (0x2100, 0x214F, 700), // Letterlike symbols
2221            (0x2190, 0x21FF, 600), // Arrows
2222            (0x2200, 0x22FF, 600), // Mathematical operators
2223            (0x2300, 0x23FF, 600), // Miscellaneous technical
2224            (0x2500, 0x257F, 500), // Box drawing
2225            (0x2580, 0x259F, 500), // Block elements
2226            (0x25A0, 0x25FF, 600), // Geometric shapes
2227            (0x2600, 0x26FF, 600), // Miscellaneous symbols
2228            (0x2700, 0x27BF, 600), // Dingbats
2229        ];
2230
2231        // Convert ranges to W array format
2232        for (start, end, width) in ranges {
2233            if start == end {
2234                // Single character
2235                w_array.push(Object::Integer(start));
2236                w_array.push(Object::Array(vec![Object::Integer(width)]));
2237            } else {
2238                // Range of characters
2239                w_array.push(Object::Integer(start));
2240                w_array.push(Object::Integer(end));
2241                w_array.push(Object::Integer(width));
2242            }
2243        }
2244
2245        w_array
2246    }
2247
2248    /// Generate CIDToGIDMap for Type0 font
2249    fn generate_cid_to_gid_map(
2250        &mut self,
2251        font_name: &str,
2252        font: &crate::fonts::Font,
2253        subset_mapping: Option<&HashMap<u32, u16>>,
2254    ) -> Result<Vec<u8>> {
2255        use crate::text::fonts::truetype::TrueTypeFont;
2256
2257        // If we have a subset mapping, use it directly
2258        // Otherwise, parse the font to get the original cmap table
2259        let cmap_mappings = if let Some(subset_map) = subset_mapping {
2260            // Use the subset mapping directly
2261            subset_map.clone()
2262        } else {
2263            // Parse the font to get the original cmap table
2264            let tt_font = TrueTypeFont::parse(font.data.clone())?;
2265            let cmap_tables = tt_font.parse_cmap()?;
2266
2267            // Find the best cmap table (prefer Format 12 for CJK)
2268            let cmap = CmapSubtable::select_best_or_first(&cmap_tables).ok_or_else(|| {
2269                crate::error::PdfError::FontError("No Unicode cmap table found".to_string())
2270            })?;
2271
2272            cmap.mappings.clone()
2273        };
2274
2275        // Build the CIDToGIDMap
2276        // Since we use Unicode code points as CIDs, we need to map Unicode → GlyphID
2277        // The map is a binary array where index = CID (Unicode) * 2, value = GlyphID (big-endian)
2278
2279        // OPTIMIZATION: Only create map for characters actually used in the document
2280        // Get used characters from document tracking
2281        let used_chars = self
2282            .document_used_chars_by_font
2283            .get(font_name)
2284            .cloned()
2285            .unwrap_or_default();
2286
2287        // Find the maximum Unicode value from used characters or full font
2288        let max_unicode = if !used_chars.is_empty() {
2289            // If we have used chars tracking, only map up to the highest used character
2290            used_chars
2291                .iter()
2292                .map(|ch| *ch as u32)
2293                .max()
2294                .unwrap_or(0x00FF) // At least Basic Latin
2295                .min(0xFFFF) as usize
2296        } else {
2297            // Fallback to original behavior if no tracking
2298            cmap_mappings
2299                .keys()
2300                .max()
2301                .copied()
2302                .unwrap_or(0xFFFF)
2303                .min(0xFFFF) as usize
2304        };
2305
2306        // Create the map: 2 bytes per entry
2307        let mut map = vec![0u8; (max_unicode + 1) * 2];
2308
2309        // Fill in the mappings
2310        let mut sample_mappings = Vec::new();
2311        for (&unicode, &glyph_id) in &cmap_mappings {
2312            if unicode <= max_unicode as u32 {
2313                let idx = (unicode as usize) * 2;
2314                // Write glyph_id in big-endian format
2315                map[idx] = (glyph_id >> 8) as u8;
2316                map[idx + 1] = (glyph_id & 0xFF) as u8;
2317
2318                // Collect some sample mappings for debugging
2319                if unicode == 0x0041 || unicode == 0x0061 || unicode == 0x00E1 || unicode == 0x00F1
2320                {
2321                    sample_mappings.push((unicode, glyph_id));
2322                }
2323            }
2324        }
2325
2326        Ok(map)
2327    }
2328
2329    /// Generate ToUnicode CMap for Type0 font from fonts::Font
2330    fn generate_tounicode_cmap_from_font(
2331        &self,
2332        font_name: &str,
2333        font: &crate::fonts::Font,
2334    ) -> Vec<u8> {
2335        use crate::text::fonts::truetype::TrueTypeFont;
2336
2337        let mut cmap = String::new();
2338
2339        // CMap header
2340        cmap.push_str("/CIDInit /ProcSet findresource begin\n");
2341        cmap.push_str("12 dict begin\n");
2342        cmap.push_str("begincmap\n");
2343        cmap.push_str("/CIDSystemInfo\n");
2344        cmap.push_str("<< /Registry (Adobe)\n");
2345        cmap.push_str("   /Ordering (UCS)\n");
2346        cmap.push_str("   /Supplement 0\n");
2347        cmap.push_str(">> def\n");
2348        cmap.push_str("/CMapName /Adobe-Identity-UCS def\n");
2349        cmap.push_str("/CMapType 2 def\n");
2350        cmap.push_str("1 begincodespacerange\n");
2351        cmap.push_str("<0000> <FFFF>\n");
2352        cmap.push_str("endcodespacerange\n");
2353
2354        // Build the set of code points that must appear in the ToUnicode CMap.
2355        // With Identity-H encoding, CID == Unicode, so each used character
2356        // produces a single `<CID> <unicode>` entry. If the document tracked
2357        // no used characters (legacy path), fall back to the font's full cmap
2358        // filtered to the BMP — but that path is a backstop, not the norm.
2359        let used_codepoints: Option<std::collections::HashSet<u32>> = self
2360            .document_used_chars_by_font
2361            .get(font_name)
2362            .map(|chars| {
2363                chars
2364                    .iter()
2365                    .map(|c| *c as u32)
2366                    .filter(|cp| *cp <= 0xFFFF)
2367                    .collect()
2368            });
2369
2370        let mut mappings: Vec<(u32, u32)> = Vec::new();
2371
2372        if let Some(used) = &used_codepoints {
2373            // Fast path: every used codepoint maps to itself under Identity-H.
2374            for cp in used {
2375                mappings.push((*cp, *cp));
2376            }
2377        } else if let Ok(tt_font) = TrueTypeFont::parse(font.data.clone()) {
2378            // Legacy backstop: no used-char tracking, emit every font mapping.
2379            if let Ok(cmap_tables) = tt_font.parse_cmap() {
2380                if let Some(cmap_table) = CmapSubtable::select_best_or_first(&cmap_tables) {
2381                    for (&unicode, &glyph_id) in &cmap_table.mappings {
2382                        if glyph_id > 0 && unicode <= 0xFFFF {
2383                            mappings.push((unicode, unicode));
2384                        }
2385                    }
2386                }
2387            }
2388        }
2389
2390        // Sort mappings by CID for better organization
2391        mappings.sort_by_key(|&(cid, _)| cid);
2392
2393        // Use more efficient bfrange where possible
2394        let mut i = 0;
2395        while i < mappings.len() {
2396            // Check if we can use a range
2397            let start_cid = mappings[i].0;
2398            let start_unicode = mappings[i].1;
2399            let mut end_idx = i;
2400
2401            // Find consecutive mappings
2402            while end_idx + 1 < mappings.len()
2403                && mappings[end_idx + 1].0 == mappings[end_idx].0 + 1
2404                && mappings[end_idx + 1].1 == mappings[end_idx].1 + 1
2405                && end_idx - i < 99
2406            // Max 100 per block
2407            {
2408                end_idx += 1;
2409            }
2410
2411            if end_idx > i {
2412                // Use bfrange for consecutive mappings
2413                cmap.push_str("1 beginbfrange\n");
2414                cmap.push_str(&format!(
2415                    "<{:04X}> <{:04X}> <{:04X}>\n",
2416                    start_cid, mappings[end_idx].0, start_unicode
2417                ));
2418                cmap.push_str("endbfrange\n");
2419                i = end_idx + 1;
2420            } else {
2421                // Use bfchar for individual mappings
2422                let mut chars = Vec::new();
2423                let chunk_end = (i + 100).min(mappings.len());
2424
2425                for item in &mappings[i..chunk_end] {
2426                    chars.push(*item);
2427                }
2428
2429                if !chars.is_empty() {
2430                    cmap.push_str(&format!("{} beginbfchar\n", chars.len()));
2431                    for (cid, unicode) in chars {
2432                        cmap.push_str(&format!("<{:04X}> <{:04X}>\n", cid, unicode));
2433                    }
2434                    cmap.push_str("endbfchar\n");
2435                }
2436
2437                i = chunk_end;
2438            }
2439        }
2440
2441        // CMap footer
2442        cmap.push_str("endcmap\n");
2443        cmap.push_str("CMapName currentdict /CMap defineresource pop\n");
2444        cmap.push_str("end\n");
2445        cmap.push_str("end\n");
2446
2447        cmap.into_bytes()
2448    }
2449
2450    /// Write a regular TrueType font
2451    #[allow(dead_code)]
2452    fn write_truetype_font(
2453        &mut self,
2454        font_name: &str,
2455        font: &crate::text::font_manager::CustomFont,
2456    ) -> Result<ObjectId> {
2457        // Allocate IDs for font objects
2458        let font_id = self.allocate_object_id();
2459        let descriptor_id = self.allocate_object_id();
2460        let font_file_id = self.allocate_object_id();
2461
2462        // Write font file (embedded TTF data)
2463        if let Some(ref data) = font.font_data {
2464            let mut font_file_dict = Dictionary::new();
2465            font_file_dict.set("Length1", Object::Integer(data.len() as i64));
2466            let font_stream_obj = Object::Stream(font_file_dict, data.clone());
2467            self.write_object(font_file_id, font_stream_obj)?;
2468        }
2469
2470        // Write font descriptor
2471        let mut descriptor = Dictionary::new();
2472        descriptor.set("Type", Object::Name("FontDescriptor".to_string()));
2473        descriptor.set("FontName", Object::Name(font_name.to_string()));
2474        descriptor.set("Flags", Object::Integer(32)); // Non-symbolic font
2475        descriptor.set(
2476            "FontBBox",
2477            Object::Array(vec![
2478                Object::Integer(-1000),
2479                Object::Integer(-1000),
2480                Object::Integer(2000),
2481                Object::Integer(2000),
2482            ]),
2483        );
2484        descriptor.set("ItalicAngle", Object::Integer(0));
2485        descriptor.set("Ascent", Object::Integer(font.descriptor.ascent as i64));
2486        descriptor.set("Descent", Object::Integer(font.descriptor.descent as i64));
2487        descriptor.set(
2488            "CapHeight",
2489            Object::Integer(font.descriptor.cap_height as i64),
2490        );
2491        descriptor.set("StemV", Object::Integer(font.descriptor.stem_v as i64));
2492        descriptor.set("FontFile2", Object::Reference(font_file_id));
2493        self.write_object(descriptor_id, Object::Dictionary(descriptor))?;
2494
2495        // Write font dictionary
2496        let mut font_dict = Dictionary::new();
2497        font_dict.set("Type", Object::Name("Font".to_string()));
2498        font_dict.set("Subtype", Object::Name("TrueType".to_string()));
2499        font_dict.set("BaseFont", Object::Name(font_name.to_string()));
2500        font_dict.set("FirstChar", Object::Integer(0));
2501        font_dict.set("LastChar", Object::Integer(255));
2502
2503        // Create widths array (simplified - all 600)
2504        let widths: Vec<Object> = (0..256).map(|_| Object::Integer(600)).collect();
2505        font_dict.set("Widths", Object::Array(widths));
2506        font_dict.set("FontDescriptor", Object::Reference(descriptor_id));
2507
2508        // Use WinAnsiEncoding for regular TrueType
2509        font_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
2510
2511        self.write_object(font_id, Object::Dictionary(font_dict))?;
2512
2513        Ok(font_id)
2514    }
2515
2516    fn write_pages(
2517        &mut self,
2518        document: &Document,
2519        font_refs: &HashMap<String, ObjectId>,
2520    ) -> Result<()> {
2521        let pages_id = self.get_pages_id()?;
2522        let mut pages_dict = Dictionary::new();
2523        pages_dict.set("Type", Object::Name("Pages".to_string()));
2524        pages_dict.set("Count", Object::Integer(document.pages.len() as i64));
2525
2526        let mut kids = Vec::new();
2527
2528        // Allocate page object IDs sequentially
2529        let mut page_ids = Vec::new();
2530        let mut content_ids = Vec::new();
2531        for _ in 0..document.pages.len() {
2532            page_ids.push(self.allocate_object_id());
2533            content_ids.push(self.allocate_object_id());
2534        }
2535
2536        for page_id in &page_ids {
2537            kids.push(Object::Reference(*page_id));
2538        }
2539
2540        pages_dict.set("Kids", Object::Array(kids));
2541
2542        self.write_object(pages_id, Object::Dictionary(pages_dict))?;
2543
2544        // Store page IDs for form field references
2545        self.page_ids = page_ids.clone();
2546
2547        // Write individual pages with font references
2548        for (i, page) in document.pages.iter().enumerate() {
2549            let page_id = page_ids[i];
2550            let content_id = content_ids[i];
2551
2552            // Issue #395: compute the collision-only preserved-font rename map
2553            // once and drive BOTH the resource-dict rename (write_page_with_fonts)
2554            // and the content-stream rewrite (write_page_content) from it, so the
2555            // two stay consistent by construction.
2556            let preserved_font_map = Self::preserved_font_disambiguation_map(page, font_refs);
2557            self.write_page_with_fonts(
2558                page_id,
2559                pages_id,
2560                content_id,
2561                page,
2562                document,
2563                font_refs,
2564                &preserved_font_map,
2565            )?;
2566            self.write_page_content(content_id, page, &preserved_font_map)?;
2567        }
2568
2569        Ok(())
2570    }
2571
2572    /// Compatibility alias for `write_pages` to maintain backwards compatibility
2573    #[allow(dead_code)]
2574    fn write_pages_with_fonts(
2575        &mut self,
2576        document: &Document,
2577        font_refs: &HashMap<String, ObjectId>,
2578    ) -> Result<()> {
2579        self.write_pages(document, font_refs)
2580    }
2581
2582    fn write_page_with_fonts(
2583        &mut self,
2584        page_id: ObjectId,
2585        parent_id: ObjectId,
2586        content_id: ObjectId,
2587        page: &crate::page::Page,
2588        _document: &Document,
2589        font_refs: &HashMap<String, ObjectId>,
2590        preserved_font_map: &HashMap<String, String>,
2591    ) -> Result<()> {
2592        // Start with the page's dictionary which includes annotations
2593        let mut page_dict = page.to_dict();
2594
2595        page_dict.set("Type", Object::Name("Page".to_string()));
2596        page_dict.set("Parent", Object::Reference(parent_id));
2597        page_dict.set("Contents", Object::Reference(content_id));
2598
2599        // Get resources dictionary or create new one
2600        let mut resources = if let Some(Object::Dictionary(res)) = page_dict.get("Resources") {
2601            res.clone()
2602        } else {
2603            Dictionary::new()
2604        };
2605
2606        // Add font resources
2607        let mut font_dict = Dictionary::new();
2608
2609        // Add ALL standard PDF fonts (Type1) with WinAnsiEncoding
2610        // This fixes the text rendering issue in dashboards where HelveticaBold was missing
2611
2612        // Helvetica family
2613        let mut helvetica_dict = Dictionary::new();
2614        helvetica_dict.set("Type", Object::Name("Font".to_string()));
2615        helvetica_dict.set("Subtype", Object::Name("Type1".to_string()));
2616        helvetica_dict.set("BaseFont", Object::Name("Helvetica".to_string()));
2617        helvetica_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
2618        font_dict.set("Helvetica", Object::Dictionary(helvetica_dict));
2619
2620        let mut helvetica_bold_dict = Dictionary::new();
2621        helvetica_bold_dict.set("Type", Object::Name("Font".to_string()));
2622        helvetica_bold_dict.set("Subtype", Object::Name("Type1".to_string()));
2623        helvetica_bold_dict.set("BaseFont", Object::Name("Helvetica-Bold".to_string()));
2624        helvetica_bold_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
2625        font_dict.set("Helvetica-Bold", Object::Dictionary(helvetica_bold_dict));
2626
2627        let mut helvetica_oblique_dict = Dictionary::new();
2628        helvetica_oblique_dict.set("Type", Object::Name("Font".to_string()));
2629        helvetica_oblique_dict.set("Subtype", Object::Name("Type1".to_string()));
2630        helvetica_oblique_dict.set("BaseFont", Object::Name("Helvetica-Oblique".to_string()));
2631        helvetica_oblique_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
2632        font_dict.set(
2633            "Helvetica-Oblique",
2634            Object::Dictionary(helvetica_oblique_dict),
2635        );
2636
2637        let mut helvetica_bold_oblique_dict = Dictionary::new();
2638        helvetica_bold_oblique_dict.set("Type", Object::Name("Font".to_string()));
2639        helvetica_bold_oblique_dict.set("Subtype", Object::Name("Type1".to_string()));
2640        helvetica_bold_oblique_dict.set(
2641            "BaseFont",
2642            Object::Name("Helvetica-BoldOblique".to_string()),
2643        );
2644        helvetica_bold_oblique_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
2645        font_dict.set(
2646            "Helvetica-BoldOblique",
2647            Object::Dictionary(helvetica_bold_oblique_dict),
2648        );
2649
2650        // Times family
2651        let mut times_dict = Dictionary::new();
2652        times_dict.set("Type", Object::Name("Font".to_string()));
2653        times_dict.set("Subtype", Object::Name("Type1".to_string()));
2654        times_dict.set("BaseFont", Object::Name("Times-Roman".to_string()));
2655        times_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
2656        font_dict.set("Times-Roman", Object::Dictionary(times_dict));
2657
2658        let mut times_bold_dict = Dictionary::new();
2659        times_bold_dict.set("Type", Object::Name("Font".to_string()));
2660        times_bold_dict.set("Subtype", Object::Name("Type1".to_string()));
2661        times_bold_dict.set("BaseFont", Object::Name("Times-Bold".to_string()));
2662        times_bold_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
2663        font_dict.set("Times-Bold", Object::Dictionary(times_bold_dict));
2664
2665        let mut times_italic_dict = Dictionary::new();
2666        times_italic_dict.set("Type", Object::Name("Font".to_string()));
2667        times_italic_dict.set("Subtype", Object::Name("Type1".to_string()));
2668        times_italic_dict.set("BaseFont", Object::Name("Times-Italic".to_string()));
2669        times_italic_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
2670        font_dict.set("Times-Italic", Object::Dictionary(times_italic_dict));
2671
2672        let mut times_bold_italic_dict = Dictionary::new();
2673        times_bold_italic_dict.set("Type", Object::Name("Font".to_string()));
2674        times_bold_italic_dict.set("Subtype", Object::Name("Type1".to_string()));
2675        times_bold_italic_dict.set("BaseFont", Object::Name("Times-BoldItalic".to_string()));
2676        times_bold_italic_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
2677        font_dict.set(
2678            "Times-BoldItalic",
2679            Object::Dictionary(times_bold_italic_dict),
2680        );
2681
2682        // Courier family
2683        let mut courier_dict = Dictionary::new();
2684        courier_dict.set("Type", Object::Name("Font".to_string()));
2685        courier_dict.set("Subtype", Object::Name("Type1".to_string()));
2686        courier_dict.set("BaseFont", Object::Name("Courier".to_string()));
2687        courier_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
2688        font_dict.set("Courier", Object::Dictionary(courier_dict));
2689
2690        let mut courier_bold_dict = Dictionary::new();
2691        courier_bold_dict.set("Type", Object::Name("Font".to_string()));
2692        courier_bold_dict.set("Subtype", Object::Name("Type1".to_string()));
2693        courier_bold_dict.set("BaseFont", Object::Name("Courier-Bold".to_string()));
2694        courier_bold_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
2695        font_dict.set("Courier-Bold", Object::Dictionary(courier_bold_dict));
2696
2697        let mut courier_oblique_dict = Dictionary::new();
2698        courier_oblique_dict.set("Type", Object::Name("Font".to_string()));
2699        courier_oblique_dict.set("Subtype", Object::Name("Type1".to_string()));
2700        courier_oblique_dict.set("BaseFont", Object::Name("Courier-Oblique".to_string()));
2701        courier_oblique_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
2702        font_dict.set("Courier-Oblique", Object::Dictionary(courier_oblique_dict));
2703
2704        let mut courier_bold_oblique_dict = Dictionary::new();
2705        courier_bold_oblique_dict.set("Type", Object::Name("Font".to_string()));
2706        courier_bold_oblique_dict.set("Subtype", Object::Name("Type1".to_string()));
2707        courier_bold_oblique_dict.set("BaseFont", Object::Name("Courier-BoldOblique".to_string()));
2708        courier_bold_oblique_dict.set("Encoding", Object::Name("WinAnsiEncoding".to_string()));
2709        font_dict.set(
2710            "Courier-BoldOblique",
2711            Object::Dictionary(courier_bold_oblique_dict),
2712        );
2713
2714        // Add custom fonts (Type0 fonts for Unicode support)
2715        for (font_name, font_id) in font_refs {
2716            font_dict.set(font_name, Object::Reference(*font_id));
2717        }
2718
2719        resources.set("Font", Object::Dictionary(font_dict));
2720
2721        // Add images and Form XObjects as XObjects
2722        let has_images = !page.images().is_empty();
2723        let has_forms = !page.form_xobjects().is_empty();
2724
2725        // Tracks name→ObjectId for every FormXObject written below.
2726        // Used downstream by the ExtGState SMask emission (ISO 32000-1
2727        // §11.6.4.3 Table 144 requires /G to be an INDIRECT reference
2728        // to a transparency-group Form XObject; the caller supplies the
2729        // group by name in `SoftMask::alpha(name)` and we resolve that
2730        // name to the ObjectId allocated here).
2731        let mut form_xobject_ids: HashMap<String, ObjectId> = HashMap::new();
2732
2733        if has_images || has_forms {
2734            let mut xobject_dict = Dictionary::new();
2735
2736            // Sort by name for reproducible output (images first, then
2737            // form xobjects — both sorted within their group). Sharing
2738            // the sort key produces the same layout across builds.
2739            let mut image_entries: Vec<(&String, &crate::graphics::Image)> =
2740                page.images().iter().collect();
2741            image_entries.sort_by_key(|(name, _)| name.as_str());
2742            for (name, image) in image_entries {
2743                // Use sequential ObjectId allocation to avoid conflicts
2744                let image_id = self.allocate_object_id();
2745
2746                // Check if image has transparency (alpha channel)
2747                if image.has_transparency() {
2748                    // Handle transparent images with SMask
2749                    let (mut main_obj, smask_obj) = image.to_pdf_object_with_transparency()?;
2750
2751                    // If we have a soft mask, write it as a separate object and reference it
2752                    if let Some(smask_stream) = smask_obj {
2753                        let smask_id = self.allocate_object_id();
2754                        self.write_object(smask_id, smask_stream)?;
2755
2756                        // Add SMask reference to the main image dictionary
2757                        if let Object::Stream(ref mut dict, _) = main_obj {
2758                            dict.set("SMask", Object::Reference(smask_id));
2759                        }
2760                    }
2761
2762                    // Write the main image XObject (now with SMask reference if applicable)
2763                    self.write_object(image_id, main_obj)?;
2764                } else {
2765                    // Write the image XObject without transparency
2766                    self.write_object(image_id, image.to_pdf_object())?;
2767                }
2768
2769                // Add reference to XObject dictionary
2770                xobject_dict.set(name, Object::Reference(image_id));
2771            }
2772
2773            // Write Form XObjects (used for overlay/watermark operations)
2774            let mut form_entries: Vec<(&String, &crate::graphics::FormXObject)> =
2775                page.form_xobjects().iter().collect();
2776            form_entries.sort_by_key(|(name, _)| name.as_str());
2777            for (name, form) in form_entries {
2778                let form_id = self.allocate_object_id();
2779                let stream = form.to_stream()?;
2780                let stream_obj =
2781                    Object::Stream(stream.dictionary().clone(), stream.data().to_vec());
2782                self.write_object(form_id, stream_obj)?;
2783                xobject_dict.set(name, Object::Reference(form_id));
2784                // Record the mapping so a downstream SoftMask with
2785                // `group_ref == name` can resolve to this indirect ref.
2786                form_xobject_ids.insert(name.clone(), form_id);
2787            }
2788
2789            resources.set("XObject", Object::Dictionary(xobject_dict));
2790        }
2791
2792        // Add ExtGState resources for transparency
2793        if let Some(extgstate_states) = page.get_extgstate_resources() {
2794            let mut extgstate_dict = Dictionary::new();
2795            // Sort ExtGState entries by name for reproducible output.
2796            let mut extgstate_entries: Vec<(&String, &crate::graphics::ExtGState)> =
2797                extgstate_states.iter().collect();
2798            extgstate_entries.sort_by_key(|(name, _)| name.as_str());
2799            for (name, state) in extgstate_entries {
2800                let mut state_dict = Dictionary::new();
2801                state_dict.set("Type", Object::Name("ExtGState".to_string()));
2802
2803                // Add transparency parameters
2804                if let Some(alpha_stroke) = state.alpha_stroke {
2805                    state_dict.set("CA", Object::Real(alpha_stroke));
2806                }
2807                if let Some(alpha_fill) = state.alpha_fill {
2808                    state_dict.set("ca", Object::Real(alpha_fill));
2809                }
2810
2811                // Add other parameters as needed
2812                if let Some(line_width) = state.line_width {
2813                    state_dict.set("LW", Object::Real(line_width));
2814                }
2815                if let Some(line_cap) = state.line_cap {
2816                    state_dict.set("LC", Object::Integer(line_cap as i64));
2817                }
2818                if let Some(line_join) = state.line_join {
2819                    state_dict.set("LJ", Object::Integer(line_join as i64));
2820                }
2821                if let Some(dash_pattern) = &state.dash_pattern {
2822                    let dash_objects: Vec<Object> = dash_pattern
2823                        .array
2824                        .iter()
2825                        .map(|&d| Object::Real(d))
2826                        .collect();
2827                    state_dict.set(
2828                        "D",
2829                        Object::Array(vec![
2830                            Object::Array(dash_objects),
2831                            Object::Real(dash_pattern.phase),
2832                        ]),
2833                    );
2834                }
2835
2836                // Blend mode (ISO 32000-1 §11.3.5, Table 137). Emitted as
2837                // a single name; blend-mode *arrays* (multiple fallback
2838                // modes) are not currently exposed by ExtGState.
2839                if let Some(ref bm) = state.blend_mode {
2840                    state_dict.set("BM", Object::Name(bm.pdf_name().to_string()));
2841                }
2842
2843                // Soft mask (ISO 32000-1 §11.6.4.3, Table 144).
2844                // `SoftMask::to_pdf_dictionary` returns a full mask dict
2845                // with /Type /Mask /S <Alpha|Luminosity|None> and,
2846                // when a transparency group is attached, the /G, /BC
2847                // and /TR entries. The `/SMask /None` Name shortcut is
2848                // *also* spec-legal per §11.6.4.3; we emit the dict
2849                // form unconditionally so callers see a consistent
2850                // shape (and because the builder already populated the
2851                // dict variant for them).
2852                //
2853                // /G MUST be an indirect reference (Table 144). The
2854                // `SoftMask` API models the group reference as a `String`
2855                // name matching a FormXObject registered on this page
2856                // via `Page::add_form_xobject(name, ...)`. Resolve the
2857                // name here to the indirect ObjectId allocated above.
2858                // If no matching FormXObject exists, surface a structured
2859                // error rather than emit a spec-invalid /G /<Name> token.
2860                if let Some(ref soft_mask) = state.soft_mask {
2861                    let mut mask_dict = soft_mask.to_pdf_dictionary()?;
2862                    if let Some(Object::Name(ref g_name)) = mask_dict.get("G").cloned() {
2863                        let form_id = form_xobject_ids.get(g_name).ok_or_else(|| {
2864                            crate::error::PdfError::InvalidStructure(format!(
2865                                "SoftMask references transparency group {:?} but no matching \
2866                                 FormXObject is registered on the page; call \
2867                                 Page::add_form_xobject({:?}, ...) before saving",
2868                                g_name, g_name
2869                            ))
2870                        })?;
2871                        mask_dict.set("G", Object::Reference(*form_id));
2872                    }
2873                    state_dict.set("SMask", Object::Dictionary(mask_dict));
2874                }
2875
2876                extgstate_dict.set(name, Object::Dictionary(state_dict));
2877            }
2878            if !extgstate_dict.is_empty() {
2879                resources.set("ExtGState", Object::Dictionary(extgstate_dict));
2880            }
2881        }
2882
2883        // ColorSpace resources (ISO 32000-1 §8.6, Table 62). Emitted as a
2884        // direct sub-dictionary — colour-space *parameters* (the dict
2885        // inside `[/CalRGB <<..>>]`) are generally small and inlining them
2886        // keeps the cross-reference table lean. Callers that need
2887        // larger / shared colour spaces can register them once and reuse
2888        // the same key across pages.
2889        // Deterministic emission of all three resource sub-dicts is
2890        // enforced at Dictionary write time (see QUAL-9 sort below in
2891        // `write_object_value`). We therefore iterate the source
2892        // HashMaps in any order here — the serializer reorders.
2893        // However we DO sort Pattern / Shading entries before
2894        // `allocate_object_id()` so object-id allocation is also
2895        // reproducible (two identical documents allocate ids in the
2896        // same sequence, producing byte-identical xref entries).
2897        if !page.color_spaces().is_empty() {
2898            let mut cs_dict = Dictionary::new();
2899            // Sort by name before allocating any stream object ids so id
2900            // allocation stays reproducible (mirrors the Pattern/Shading blocks).
2901            let mut entries: Vec<(&String, &crate::graphics::PageColorSpace)> =
2902                page.color_spaces().iter().collect();
2903            entries.sort_by_key(|(name, _)| name.as_str());
2904            for (name, cs) in entries {
2905                // ICCBased colour spaces MUST be an indirect stream carrying the
2906                // profile bytes (ISO 32000-1 §8.6.5.5) — a stream cannot be
2907                // inlined into the resource dict. Every other shape (device-name
2908                // alias, Cal*/Lab parameterised dict) is inline via `to_object`.
2909                if let Some((icc_dict, icc_data)) = cs.icc_stream_parts() {
2910                    let icc_id = self.allocate_object_id();
2911                    self.write_object(icc_id, Object::Stream(icc_dict, icc_data))?;
2912                    cs_dict.set(
2913                        name,
2914                        Object::Array(vec![
2915                            Object::Name("ICCBased".to_string()),
2916                            Object::Reference(icc_id),
2917                        ]),
2918                    );
2919                } else {
2920                    cs_dict.set(name, cs.to_object());
2921                }
2922            }
2923            resources.set("ColorSpace", Object::Dictionary(cs_dict));
2924        }
2925
2926        if !page.patterns().is_empty() {
2927            let mut pat_dict = Dictionary::new();
2928            let mut entries: Vec<(&String, &crate::graphics::TilingPattern)> =
2929                page.patterns().iter().collect();
2930            entries.sort_by_key(|(name, _)| name.as_str());
2931            for (name, pattern) in entries {
2932                let pattern_id = self.allocate_object_id();
2933                let pattern_dict = pattern.to_pdf_dictionary()?;
2934                self.write_object(
2935                    pattern_id,
2936                    Object::Stream(pattern_dict, pattern.content_stream.clone()),
2937                )?;
2938                pat_dict.set(name, Object::Reference(pattern_id));
2939            }
2940            resources.set("Pattern", Object::Dictionary(pat_dict));
2941        }
2942
2943        if !page.shadings().is_empty() || !page.advanced_shadings().is_empty() {
2944            let mut sh_dict = Dictionary::new();
2945
2946            // Gradient shadings (Axial/Radial/FunctionBased) → dictionaries.
2947            let mut entries: Vec<(&String, &crate::graphics::ShadingDefinition)> =
2948                page.shadings().iter().collect();
2949            entries.sort_by_key(|(name, _)| name.as_str());
2950            for (name, shading) in entries {
2951                let obj = Object::Dictionary(shading.to_pdf_dictionary()?);
2952                let shading_id = self.write_shading_object(obj)?;
2953                sh_dict.set(name, Object::Reference(shading_id));
2954            }
2955
2956            // Additive mesh (Type 4, stream) / conic (Type 1, dict) shadings
2957            // (#407), emitted into the same /Shading resource.
2958            let mut adv: Vec<(&String, &crate::graphics::AdvancedShading)> =
2959                page.advanced_shadings().iter().collect();
2960            adv.sort_by_key(|(name, _)| name.as_str());
2961            for (name, shading) in adv {
2962                let obj = shading.to_pdf_object()?;
2963                let shading_id = self.write_shading_object(obj)?;
2964                sh_dict.set(name, Object::Reference(shading_id));
2965            }
2966
2967            resources.set("Shading", Object::Dictionary(sh_dict));
2968        }
2969
2970        // Merge preserved resources from original PDF (if any)
2971        // Phase 2.3: Rename preserved fonts to avoid conflicts with overlay fonts
2972        if let Some(preserved_res) = page.get_preserved_resources() {
2973            // Convert pdf_objects::Dictionary to writer Dictionary FIRST
2974            let mut preserved_writer_dict = self.convert_pdf_objects_dict_to_writer(preserved_res);
2975
2976            // Step 1: Issue #395 — collision-only rename. Only preserved font
2977            // keys that collide with an injected/overlay key are renamed (per
2978            // `preserved_font_map`); every other key is kept, so non-colliding
2979            // fonts are never disturbed and their content is never rewritten.
2980            if let Some(Object::Dictionary(fonts)) = preserved_writer_dict.get("Font") {
2981                let renamed_fonts = crate::writer::apply_font_rename_map(fonts, preserved_font_map);
2982                preserved_writer_dict.set("Font", Object::Dictionary(renamed_fonts));
2983            }
2984
2985            // Phase 3.3: Write embedded font streams as indirect objects
2986            // Fonts that were resolved in Phase 3.2 have embedded Stream objects
2987            // We need to write these streams as separate PDF objects and replace with References
2988            if let Some(Object::Dictionary(fonts)) = preserved_writer_dict.get("Font") {
2989                let mut fonts_with_refs = crate::objects::Dictionary::new();
2990
2991                for (font_name, font_obj) in fonts.iter() {
2992                    if let Object::Dictionary(font_dict) = font_obj {
2993                        // Try to extract and write embedded font streams
2994                        let updated_font = self.write_embedded_font_streams(font_dict)?;
2995                        fonts_with_refs.set(font_name, Object::Dictionary(updated_font));
2996                    } else {
2997                        // Not a dictionary, keep as-is
2998                        fonts_with_refs.set(font_name, font_obj.clone());
2999                    }
3000                }
3001
3002                // Replace Font dictionary with version that has References instead of Streams
3003                preserved_writer_dict.set("Font", Object::Dictionary(fonts_with_refs));
3004            }
3005
3006            // Write preserved XObject streams as indirect objects
3007            // XObjects resolved in from_parsed_with_content may contain inline Stream data.
3008            // Per ISO 32000-1 §7.3.8, streams MUST be indirect objects.
3009            if let Some(Object::Dictionary(xobjects)) = preserved_writer_dict.get("XObject") {
3010                let mut xobjects_with_refs = crate::objects::Dictionary::new();
3011                tracing::debug!(
3012                    "Externalizing {} preserved XObject entries as indirect objects",
3013                    xobjects.len()
3014                );
3015
3016                for (xobj_name, xobj_obj) in xobjects.iter() {
3017                    match xobj_obj {
3018                        Object::Stream(dict, data) => {
3019                            // #465: an image XObject's dictionary can carry
3020                            // nested streams (a soft mask `/SMask`, a `/Mask`,
3021                            // an ICCBased colour space) that `from_parsed_with_content`
3022                            // inlined. Per ISO 32000-1 §7.3.8 a stream MUST be an
3023                            // indirect object, so a nested inline stream would
3024                            // produce invalid PDF; externalize each before
3025                            // writing the image itself.
3026                            let dict = self.externalize_nested_streams_in_dict(dict)?;
3027                            let obj_id = self.allocate_object_id();
3028                            self.write_object(obj_id, Object::Stream(dict, data.clone()))?;
3029                            xobjects_with_refs.set(xobj_name, Object::Reference(obj_id));
3030                        }
3031                        Object::Dictionary(dict) => {
3032                            // A bare-dictionary XObject is already invalid per
3033                            // ISO 32000-1 §8.10 (XObjects must be streams), so it
3034                            // cannot legitimately carry the array-/sub-dict-nested
3035                            // stream shapes the Stream arm above must handle.
3036                            // Top-level externalization is deliberately enough
3037                            // here; do not "fix" this into the recursive walk.
3038                            let externalized = self.externalize_streams_in_dict(dict)?;
3039                            xobjects_with_refs.set(xobj_name, Object::Dictionary(externalized));
3040                        }
3041                        _ => {
3042                            xobjects_with_refs.set(xobj_name, xobj_obj.clone());
3043                        }
3044                    }
3045                }
3046
3047                preserved_writer_dict.set("XObject", Object::Dictionary(xobjects_with_refs));
3048            }
3049
3050            // Merge each resource category (Font, XObject, ColorSpace, etc.)
3051            for (key, value) in preserved_writer_dict.iter() {
3052                // If the resource category already exists, merge dictionaries
3053                if let Some(Object::Dictionary(existing)) = resources.get(key) {
3054                    if let Object::Dictionary(preserved_dict) = value {
3055                        let mut merged = existing.clone();
3056                        // Add all preserved resources, giving priority to existing (overlay wins)
3057                        for (res_name, res_obj) in preserved_dict.iter() {
3058                            if !merged.contains_key(res_name) {
3059                                merged.set(res_name, res_obj.clone());
3060                            }
3061                        }
3062                        resources.set(key, Object::Dictionary(merged));
3063                    }
3064                } else {
3065                    // Resource category doesn't exist yet, add it directly
3066                    resources.set(key, value.clone());
3067                }
3068            }
3069        }
3070
3071        page_dict.set("Resources", Object::Dictionary(resources));
3072
3073        // Collect all annotation references for the /Annots array
3074        let mut annot_refs: Vec<Object> = Vec::new();
3075
3076        // 1. Process widget annotations already in page_dict (legacy form field path)
3077        if let Some(Object::Array(annots)) = page_dict.get("Annots") {
3078            for annot in annots {
3079                if let Object::Dictionary(ref annot_dict) = annot {
3080                    if let Some(Object::Name(subtype)) = annot_dict.get("Subtype") {
3081                        if subtype == "Widget" {
3082                            let widget_id = self.allocate_object_id();
3083                            self.write_object(widget_id, annot.clone())?;
3084                            annot_refs.push(Object::Reference(widget_id));
3085
3086                            // Track widget for form fields
3087                            if let Some(Object::Name(_ft)) = annot_dict.get("FT") {
3088                                if let Some(Object::String(field_name)) = annot_dict.get("T") {
3089                                    self.field_widget_map
3090                                        .entry(field_name.clone())
3091                                        .or_default()
3092                                        .push(widget_id);
3093                                    self.field_id_map.insert(field_name.clone(), widget_id);
3094                                    self.form_field_ids.push(widget_id);
3095                                }
3096                            }
3097                            continue;
3098                        }
3099                    }
3100                }
3101                annot_refs.push(annot.clone());
3102            }
3103        }
3104
3105        // 2. Write annotations from Page.annotations() (programmatic annotations)
3106        //    Handles highlights, text notes, stamps, links, etc. added via
3107        //    page.add_annotation(). Each is written as an indirect object.
3108        for annotation in page.annotations() {
3109            let annot_id = self.allocate_object_id();
3110            let mut annot_dict = annotation.to_dict();
3111
3112            // Remap `/Parent` from FormManager placeholder → real ObjectId.
3113            // `Annotation::field_parent` stores the placeholder ref returned
3114            // by FormManager::add_*_field (which uses a counter disjoint
3115            // from the writer's allocator). At this point the writer has
3116            // already pre-allocated real ids for every FormManager field
3117            // via `preallocate_form_manager_fields`, so we translate.
3118            //
3119            // We read `field_parent` straight off the struct instead of
3120            // round-tripping through `annot_dict.get("Parent")`: the
3121            // dictionary representation is what we're producing, not a
3122            // source of truth. The struct field is authoritative and
3123            // avoids matching on a value we just computed.
3124            //
3125            // Widgets whose parent placeholder is NOT in the map (e.g.
3126            // the caller supplied a hand-built ref, or `field_parent` was
3127            // set from outside the FormManager) are left unchanged — not
3128            // every `/Parent` necessarily comes from the FormManager.
3129            if let Some(placeholder) = annotation.field_parent {
3130                if let Some(real_id) = self.form_field_placeholder_map.get(&placeholder) {
3131                    annot_dict.set("Parent", Object::Reference(*real_id));
3132                }
3133            }
3134
3135            // Externalize inline streams inside /AP.
3136            //
3137            // `Widget::generate_appearance` (and any user-supplied appearance
3138            // dictionary) stores the /N, /R, /D entries as inline
3139            // `Object::Stream` values inside the /AP sub-dictionary. Per
3140            // ISO 32000-1 §7.3.8.1, "all streams shall be indirect objects" —
3141            // inline streams as dictionary values are not permitted. We
3142            // therefore externalize each inline stream to a freshly
3143            // allocated indirect object and replace it with a /Reference.
3144            //
3145            // /AP itself has two legal shapes (§12.5.5):
3146            //   * A single stream (direct or indirect) → the "default" state.
3147            //   * A sub-dictionary mapping state names (/N, /R, /D) to
3148            //     streams, where /D may further be a dict mapping values to
3149            //     streams (radio buttons, checkboxes).
3150            // We handle the sub-dict shape (which is what `fill_field`
3151            // emits); the legacy single-stream shape falls through to the
3152            // writer's default handling below.
3153            if let Some(Object::Dictionary(ap_dict)) = annot_dict.get("AP") {
3154                let mut updated_ap = crate::objects::Dictionary::new();
3155                for (state_key, state_val) in ap_dict.iter() {
3156                    match state_val {
3157                        Object::Stream(sd, data) => {
3158                            // Patch `/Resources/Font/<name>` placeholders to
3159                            // indirect references to the document-level fonts
3160                            // (issue #212 Fase 3). The placeholder is emitted
3161                            // by form-field appearance generators that don't
3162                            // know the Type0 font's ObjectId.
3163                            let patched_sd = Self::rewrite_ap_stream_font_resources(sd, font_refs);
3164                            let stream_id = self.allocate_object_id();
3165                            self.write_object(stream_id, Object::Stream(patched_sd, data.clone()))?;
3166                            updated_ap.set(state_key, Object::Reference(stream_id));
3167                        }
3168                        Object::Dictionary(down_dict) => {
3169                            // /D sub-dict case: map value → stream.
3170                            let externalized = self
3171                                .externalize_streams_in_dict_with_font_refs(down_dict, font_refs)?;
3172                            updated_ap.set(state_key, Object::Dictionary(externalized));
3173                        }
3174                        _ => {
3175                            updated_ap.set(state_key, state_val.clone());
3176                        }
3177                    }
3178                }
3179                annot_dict.set("AP", Object::Dictionary(updated_ap));
3180            }
3181
3182            self.write_object(annot_id, Object::Dictionary(annot_dict))?;
3183            annot_refs.push(Object::Reference(annot_id));
3184
3185            // Track widget annotations for AcroForm if they come through this path
3186            if annotation.annotation_type == crate::annotations::AnnotationType::Widget {
3187                if let Some(Object::String(field_name)) = annotation.properties.get("T") {
3188                    self.field_widget_map
3189                        .entry(field_name.clone())
3190                        .or_default()
3191                        .push(annot_id);
3192                    self.field_id_map.insert(field_name.clone(), annot_id);
3193                    self.form_field_ids.push(annot_id);
3194                }
3195            }
3196        }
3197
3198        // Set or remove /Annots based on whether we have any
3199        if !annot_refs.is_empty() {
3200            page_dict.set("Annots", Object::Array(annot_refs));
3201        } else {
3202            page_dict.remove("Annots");
3203        }
3204
3205        self.write_object(page_id, Object::Dictionary(page_dict))?;
3206        Ok(())
3207    }
3208}
3209
3210impl PdfWriter<BufWriter<std::fs::File>> {
3211    pub fn new(path: impl AsRef<Path>) -> Result<Self> {
3212        let file = std::fs::File::create(path)?;
3213        let writer = BufWriter::new(file);
3214
3215        Ok(Self {
3216            writer,
3217            xref_positions: HashMap::new(),
3218            current_position: 0,
3219            next_object_id: 1,
3220            catalog_id: None,
3221            pages_id: None,
3222            info_id: None,
3223            field_widget_map: HashMap::new(),
3224            field_id_map: HashMap::new(),
3225            form_field_ids: Vec::new(),
3226            page_ids: Vec::new(),
3227            config: WriterConfig::default(),
3228            document_used_chars_by_font: std::collections::HashMap::new(),
3229            buffered_objects: HashMap::new(),
3230            compressed_object_map: HashMap::new(),
3231            prev_xref_offset: None,
3232            base_pdf_size: None,
3233            encrypt_obj_id: None,
3234            file_id: None,
3235            encryption_state: None,
3236            pending_encrypt_dict: None,
3237            form_field_placeholder_map: HashMap::new(),
3238            form_manager_field_refs: Vec::new(),
3239        })
3240    }
3241}
3242
3243impl<W: Write> PdfWriter<W> {
3244    /// Write embedded font streams as indirect objects (Phase 3.3 + Phase 3.4)
3245    ///
3246    /// Takes a font dictionary that may contain embedded Stream objects
3247    /// in its FontDescriptor, writes those streams as separate PDF objects,
3248    /// and returns an updated font dictionary with References instead of Streams.
3249    ///
3250    /// For Type0 (composite) fonts, also handles:
3251    /// - DescendantFonts array with embedded CIDFont dictionaries
3252    /// - ToUnicode stream embedded directly in Type0 font
3253    /// - CIDFont → FontDescriptor → FontFile2/FontFile3 chain
3254    ///
3255    /// # Example
3256    /// FontDescriptor:
3257    ///   FontFile2: Stream(dict, font_data)  → Write stream as obj 50
3258    ///   FontFile2: Reference(50, 0)          → Updated reference
3259    /// Walks a dictionary and writes any inline Stream values as indirect objects,
3260    /// replacing them with References. Required because PDF streams must be indirect
3261    /// objects (ISO 32000-1 §7.3.8).
3262    fn externalize_streams_in_dict(
3263        &mut self,
3264        dict: &crate::objects::Dictionary,
3265    ) -> Result<crate::objects::Dictionary> {
3266        self.externalize_streams_in_dict_with_font_refs(dict, &HashMap::new())
3267    }
3268
3269    /// Recursively rewrites every inline `Object::Stream` reachable through a
3270    /// dictionary — including streams nested inside sub-dictionaries and arrays
3271    /// — into an indirect reference, writing each as its own object (#465).
3272    ///
3273    /// Unlike [`externalize_streams_in_dict`], which only externalizes streams
3274    /// at the top level of the dictionary, this walks arrays and nested
3275    /// dictionaries too. It is applied to a preserved image XObject's
3276    /// dictionary, where `from_parsed_with_content` may have inlined a soft
3277    /// mask (`/SMask`), a `/Mask`, or an ICCBased colour-space stream (which
3278    /// lives inside a `/ColorSpace` array). Per ISO 32000-1 §7.3.8 a stream
3279    /// must be an indirect object, so none may remain inline.
3280    fn externalize_nested_streams_in_dict(
3281        &mut self,
3282        dict: &crate::objects::Dictionary,
3283    ) -> Result<crate::objects::Dictionary> {
3284        let mut result = crate::objects::Dictionary::new();
3285        for (key, value) in dict.iter() {
3286            result.set(key, self.externalize_nested_streams_value(value)?);
3287        }
3288        Ok(result)
3289    }
3290
3291    fn externalize_nested_streams_value(&mut self, value: &Object) -> Result<Object> {
3292        match value {
3293            Object::Stream(d, data) => {
3294                let d = self.externalize_nested_streams_in_dict(d)?;
3295                let obj_id = self.allocate_object_id();
3296                self.write_object(obj_id, Object::Stream(d, data.clone()))?;
3297                Ok(Object::Reference(obj_id))
3298            }
3299            Object::Dictionary(d) => Ok(Object::Dictionary(
3300                self.externalize_nested_streams_in_dict(d)?,
3301            )),
3302            Object::Array(items) => {
3303                let mut out = Vec::with_capacity(items.len());
3304                for item in items {
3305                    out.push(self.externalize_nested_streams_value(item)?);
3306                }
3307                Ok(Object::Array(out))
3308            }
3309            other => Ok(other.clone()),
3310        }
3311    }
3312
3313    /// Same as [`externalize_streams_in_dict`] but also rewrites any
3314    /// `/Resources/Font/<name>` placeholders inside the externalised stream
3315    /// dictionaries to indirect references from `font_refs` (issue #212).
3316    fn externalize_streams_in_dict_with_font_refs(
3317        &mut self,
3318        dict: &crate::objects::Dictionary,
3319        font_refs: &HashMap<String, ObjectId>,
3320    ) -> Result<crate::objects::Dictionary> {
3321        let mut result = crate::objects::Dictionary::new();
3322        for (key, value) in dict.iter() {
3323            match value {
3324                Object::Stream(d, data) => {
3325                    let patched_d = Self::rewrite_ap_stream_font_resources(d, font_refs);
3326                    let obj_id = self.allocate_object_id();
3327                    self.write_object(obj_id, Object::Stream(patched_d, data.clone()))?;
3328                    result.set(key, Object::Reference(obj_id));
3329                }
3330                _ => {
3331                    result.set(key, value.clone());
3332                }
3333            }
3334        }
3335        Ok(result)
3336    }
3337
3338    /// Rewrite `/Resources/Font/<name>` entries inside an appearance-stream
3339    /// dictionary: any entry whose name appears in `font_refs` is replaced
3340    /// by an `Object::Reference` to the document-level font object.
3341    ///
3342    /// Why: form-field appearance generators cannot know the ObjectId of
3343    /// the Type0 font at content-stream build time — they emit a
3344    /// placeholder dict (see `TextFieldAppearance::generate_appearance_with_font`).
3345    /// This pass wires that placeholder to the real indirect object produced
3346    /// by `write_fonts`. Built-in Type1 fonts (Helvetica etc.) stay as
3347    /// inline dictionaries, since they have no document-level object.
3348    ///
3349    /// Returns a copy of the input dictionary with the /Resources/Font
3350    /// rewrite applied. All non-/Resources keys are passed through intact.
3351    /// Called on the stream DICTIONARY (not the stream data) so the original
3352    /// content bytes remain untouched.
3353    fn rewrite_ap_stream_font_resources(
3354        stream_dict: &crate::objects::Dictionary,
3355        font_refs: &HashMap<String, ObjectId>,
3356    ) -> crate::objects::Dictionary {
3357        // Fast path: if the document has no custom fonts registered (i.e.
3358        // `font_refs` is empty), no placeholder entry can possibly match.
3359        // Skip the clone+walk entirely — this is the common case for
3360        // built-in-font forms, and `externalize_streams_in_dict` (the
3361        // legacy non-AP path) calls us with an empty map for every stream
3362        // it externalises.
3363        if font_refs.is_empty() {
3364            return stream_dict.clone();
3365        }
3366
3367        let mut out = stream_dict.clone();
3368
3369        // Drill /Resources → /Font. Both may be direct dicts; we rebuild
3370        // them rather than mutate in place so reference semantics are
3371        // explicit. Indirect /Resources isn't emitted by our generators, so
3372        // only the direct-dict shape is handled here (defensive: anything
3373        // else is left untouched).
3374        let Some(Object::Dictionary(resources)) = stream_dict.get("Resources") else {
3375            return out;
3376        };
3377        let Some(Object::Dictionary(fonts)) = resources.get("Font") else {
3378            return out;
3379        };
3380
3381        let mut patched_fonts = crate::objects::Dictionary::new();
3382        let mut changed = false;
3383        for (font_name, entry) in fonts.iter() {
3384            // Rewrite when (a) this is the placeholder inline dict shape our
3385            // generator emits (Object::Dictionary with /Subtype /Type0), AND
3386            // (b) the name is registered as a document-level custom font.
3387            let should_rewrite = match entry {
3388                Object::Dictionary(d) => {
3389                    matches!(d.get("Subtype"), Some(Object::Name(s)) if s == "Type0")
3390                }
3391                _ => false,
3392            };
3393            if should_rewrite {
3394                if let Some(font_id) = font_refs.get(font_name.as_str()) {
3395                    patched_fonts.set(font_name, Object::Reference(*font_id));
3396                    changed = true;
3397                    continue;
3398                }
3399            }
3400            patched_fonts.set(font_name, entry.clone());
3401        }
3402
3403        if changed {
3404            let mut patched_resources = resources.clone();
3405            patched_resources.set("Font", Object::Dictionary(patched_fonts));
3406            out.set("Resources", Object::Dictionary(patched_resources));
3407        }
3408        out
3409    }
3410
3411    fn write_embedded_font_streams(
3412        &mut self,
3413        font_dict: &crate::objects::Dictionary,
3414    ) -> Result<crate::objects::Dictionary> {
3415        let mut updated_font = font_dict.clone();
3416
3417        // Phase 3.4: Check for Type0 fonts with embedded DescendantFonts
3418        if let Some(Object::Name(subtype)) = font_dict.get("Subtype") {
3419            if subtype == "Type0" {
3420                // Process DescendantFonts array
3421                if let Some(Object::Array(descendants)) = font_dict.get("DescendantFonts") {
3422                    let mut updated_descendants = Vec::new();
3423
3424                    for descendant in descendants {
3425                        match descendant {
3426                            Object::Dictionary(cidfont) => {
3427                                // CIDFont is embedded as Dictionary, process its FontDescriptor
3428                                let updated_cidfont =
3429                                    self.write_cidfont_embedded_streams(cidfont)?;
3430                                // Write CIDFont as a separate object
3431                                let cidfont_id = self.allocate_object_id();
3432                                self.write_object(cidfont_id, Object::Dictionary(updated_cidfont))?;
3433                                // Replace with reference
3434                                updated_descendants.push(Object::Reference(cidfont_id));
3435                            }
3436                            Object::Reference(_) => {
3437                                // Already a reference, keep as-is
3438                                updated_descendants.push(descendant.clone());
3439                            }
3440                            _ => {
3441                                updated_descendants.push(descendant.clone());
3442                            }
3443                        }
3444                    }
3445
3446                    updated_font.set("DescendantFonts", Object::Array(updated_descendants));
3447                }
3448
3449                // Process ToUnicode stream if embedded
3450                if let Some(Object::Stream(stream_dict, stream_data)) = font_dict.get("ToUnicode") {
3451                    let tounicode_id = self.allocate_object_id();
3452                    self.write_object(
3453                        tounicode_id,
3454                        Object::Stream(stream_dict.clone(), stream_data.clone()),
3455                    )?;
3456                    updated_font.set("ToUnicode", Object::Reference(tounicode_id));
3457                }
3458
3459                return Ok(updated_font);
3460            }
3461        }
3462
3463        // Original Phase 3.3 logic for simple fonts (Type1, TrueType, etc.)
3464        // Check if font has a FontDescriptor
3465        if let Some(Object::Dictionary(descriptor)) = font_dict.get("FontDescriptor") {
3466            let mut updated_descriptor = descriptor.clone();
3467            let font_file_keys = ["FontFile", "FontFile2", "FontFile3"];
3468
3469            // Check each font file key for embedded streams
3470            for key in &font_file_keys {
3471                if let Some(Object::Stream(stream_dict, stream_data)) = descriptor.get(*key) {
3472                    // Found embedded stream! Write it as a separate object
3473                    let stream_id = self.allocate_object_id();
3474                    let stream_obj = Object::Stream(stream_dict.clone(), stream_data.clone());
3475                    self.write_object(stream_id, stream_obj)?;
3476
3477                    // Replace Stream with Reference to the newly written object
3478                    updated_descriptor.set(*key, Object::Reference(stream_id));
3479                }
3480                // If it's already a Reference, leave it as-is
3481            }
3482
3483            // Update FontDescriptor in font dictionary
3484            updated_font.set("FontDescriptor", Object::Dictionary(updated_descriptor));
3485        }
3486
3487        Ok(updated_font)
3488    }
3489
3490    /// Helper function to process CIDFont embedded streams (Phase 3.4)
3491    fn write_cidfont_embedded_streams(
3492        &mut self,
3493        cidfont: &crate::objects::Dictionary,
3494    ) -> Result<crate::objects::Dictionary> {
3495        let mut updated_cidfont = cidfont.clone();
3496
3497        // Process FontDescriptor
3498        if let Some(Object::Dictionary(descriptor)) = cidfont.get("FontDescriptor") {
3499            let mut updated_descriptor = descriptor.clone();
3500            let font_file_keys = ["FontFile", "FontFile2", "FontFile3"];
3501
3502            // Write embedded font streams
3503            for key in &font_file_keys {
3504                if let Some(Object::Stream(stream_dict, stream_data)) = descriptor.get(*key) {
3505                    let stream_id = self.allocate_object_id();
3506                    self.write_object(
3507                        stream_id,
3508                        Object::Stream(stream_dict.clone(), stream_data.clone()),
3509                    )?;
3510                    updated_descriptor.set(*key, Object::Reference(stream_id));
3511                }
3512            }
3513
3514            // Write FontDescriptor as a separate object
3515            let descriptor_id = self.allocate_object_id();
3516            self.write_object(descriptor_id, Object::Dictionary(updated_descriptor))?;
3517
3518            // Update CIDFont to reference the FontDescriptor
3519            updated_cidfont.set("FontDescriptor", Object::Reference(descriptor_id));
3520        }
3521
3522        // Process CIDToGIDMap if present and embedded as stream
3523        if let Some(Object::Stream(map_dict, map_data)) = cidfont.get("CIDToGIDMap") {
3524            let map_id = self.allocate_object_id();
3525            self.write_object(map_id, Object::Stream(map_dict.clone(), map_data.clone()))?;
3526            updated_cidfont.set("CIDToGIDMap", Object::Reference(map_id));
3527        }
3528
3529        Ok(updated_cidfont)
3530    }
3531
3532    fn allocate_object_id(&mut self) -> ObjectId {
3533        let id = ObjectId::new(self.next_object_id, 0);
3534        self.next_object_id += 1;
3535        id
3536    }
3537
3538    /// Write a shading as an indirect object and return its id. For a
3539    /// dictionary shading, an inline `/Function` (dictionary OR stream) is
3540    /// first hoisted to its own indirect object: ISO 32000-1 §8.7.4.5.2 makes
3541    /// functions normally indirect, and a stream in particular CANNOT be an
3542    /// inline dictionary value (the conic Type 4 PostScript function would
3543    /// otherwise be invalid PDF). A `FunctionBased` shading whose `/Function`
3544    /// is an `Integer` placeholder is left untouched. Stream shadings (the
3545    /// Type 4 mesh) are written directly.
3546    fn write_shading_object(&mut self, obj: Object) -> Result<ObjectId> {
3547        match obj {
3548            Object::Dictionary(mut dict) => {
3549                if matches!(
3550                    dict.get("Function"),
3551                    Some(Object::Dictionary(_)) | Some(Object::Stream(_, _))
3552                ) {
3553                    if let Some(func_obj) = dict.remove("Function") {
3554                        let func_id = self.allocate_object_id();
3555                        self.write_object(func_id, func_obj)?;
3556                        dict.set("Function", Object::Reference(func_id));
3557                    }
3558                }
3559                let shading_id = self.allocate_object_id();
3560                self.write_object(shading_id, Object::Dictionary(dict))?;
3561                Ok(shading_id)
3562            }
3563            stream @ Object::Stream(_, _) => {
3564                let shading_id = self.allocate_object_id();
3565                self.write_object(shading_id, stream)?;
3566                Ok(shading_id)
3567            }
3568            other => {
3569                let shading_id = self.allocate_object_id();
3570                self.write_object(shading_id, other)?;
3571                Ok(shading_id)
3572            }
3573        }
3574    }
3575
3576    /// Get catalog_id, returning error if not initialized
3577    fn get_catalog_id(&self) -> Result<ObjectId> {
3578        self.catalog_id.ok_or_else(|| {
3579            PdfError::InvalidOperation(
3580                "catalog_id not initialized - write_document() must be called first".to_string(),
3581            )
3582        })
3583    }
3584
3585    /// Get pages_id, returning error if not initialized
3586    fn get_pages_id(&self) -> Result<ObjectId> {
3587        self.pages_id.ok_or_else(|| {
3588            PdfError::InvalidOperation(
3589                "pages_id not initialized - write_document() must be called first".to_string(),
3590            )
3591        })
3592    }
3593
3594    /// Get info_id, returning error if not initialized
3595    fn get_info_id(&self) -> Result<ObjectId> {
3596        self.info_id.ok_or_else(|| {
3597            PdfError::InvalidOperation(
3598                "info_id not initialized - write_document() must be called first".to_string(),
3599            )
3600        })
3601    }
3602
3603    fn write_object(&mut self, id: ObjectId, object: Object) -> Result<()> {
3604        use crate::writer::ObjectStreamWriter;
3605
3606        // Encrypt the object if encryption is active
3607        let object = if let Some(ref enc_state) = self.encryption_state {
3608            let mut obj = object;
3609            enc_state.encryptor.encrypt_object(&mut obj, &id)?;
3610            obj
3611        } else {
3612            object
3613        };
3614
3615        // If object streams enabled and object is compressible, buffer it
3616        if self.config.use_object_streams && ObjectStreamWriter::can_compress(&object) {
3617            let mut buffer = Vec::new();
3618            self.write_object_value_to_buffer(&object, &mut buffer)?;
3619            self.buffered_objects.insert(id, buffer);
3620            return Ok(());
3621        }
3622
3623        // Otherwise write immediately (streams, encryption dicts, etc.)
3624        self.xref_positions.insert(id, self.current_position);
3625
3626        // Pre-format header to count exact bytes once
3627        let header = format!("{} {} obj\n", id.number(), id.generation());
3628        self.write_bytes(header.as_bytes())?;
3629
3630        self.write_object_value(&object)?;
3631
3632        self.write_bytes(b"\nendobj\n")?;
3633        Ok(())
3634    }
3635
3636    fn write_object_value(&mut self, object: &Object) -> Result<()> {
3637        match object {
3638            Object::Null => self.write_bytes(b"null")?,
3639            Object::Boolean(b) => self.write_bytes(if *b { b"true" } else { b"false" })?,
3640            Object::Integer(i) => self.write_bytes(i.to_string().as_bytes())?,
3641            Object::Real(f) => self.write_bytes(
3642                format!("{f:.6}")
3643                    .trim_end_matches('0')
3644                    .trim_end_matches('.')
3645                    .as_bytes(),
3646            )?,
3647            Object::String(s) => {
3648                // ISO 32000-1 §7.3.4.2: inside a literal string, the
3649                // characters `\`, `(` and `)` MUST be escaped (as `\\`,
3650                // `\(`, `\)` respectively) so the parser does not
3651                // terminate the string early or treat `\` as an escape
3652                // introducer for the following byte. Without this, a
3653                // caller-supplied value containing `)` (e.g. through
3654                // `Document::fill_field`) would close the literal and
3655                // allow dict-level injection into the enclosing object.
3656                self.write_bytes(b"(")?;
3657                self.write_bytes(&escape_pdf_string_bytes(s.as_bytes()))?;
3658                self.write_bytes(b")")?;
3659            }
3660            Object::ByteString(bytes) => {
3661                // Write as PDF hex string <AABB...> for byte-perfect binary data
3662                self.write_bytes(b"<")?;
3663                for byte in bytes {
3664                    self.write_bytes(format!("{byte:02X}").as_bytes())?;
3665                }
3666                self.write_bytes(b">")?;
3667            }
3668            Object::Name(n) => {
3669                self.write_bytes(b"/")?;
3670                self.write_bytes(n.as_bytes())?;
3671            }
3672            Object::Array(arr) => {
3673                self.write_bytes(b"[")?;
3674                for (i, obj) in arr.iter().enumerate() {
3675                    if i > 0 {
3676                        self.write_bytes(b" ")?;
3677                    }
3678                    self.write_object_value(obj)?;
3679                }
3680                self.write_bytes(b"]")?;
3681            }
3682            Object::Dictionary(dict) => {
3683                // Sort entries lexicographically by key for reproducible
3684                // output. `Dictionary` is backed by `HashMap` (with
3685                // per-instance randomised iteration order), so two
3686                // identical logical documents would otherwise emit
3687                // byte-different PDFs. PDF dict entries are unordered
3688                // by spec (ISO 32000-1 §7.3.7 Table 5: "the order of
3689                // entries ... is not significant"), so sorting is safe.
3690                self.write_bytes(b"<<")?;
3691                let mut entries: Vec<(&String, &Object)> = dict.entries().collect();
3692                entries.sort_by_key(|(k, _)| k.as_str());
3693                for (key, value) in entries {
3694                    self.write_bytes(b"\n/")?;
3695                    self.write_bytes(key.as_bytes())?;
3696                    self.write_bytes(b" ")?;
3697                    self.write_object_value(value)?;
3698                }
3699                self.write_bytes(b"\n>>")?;
3700            }
3701            Object::Stream(dict, data) => {
3702                // CRITICAL: Ensure Length in dictionary matches actual data length
3703                // This prevents "Bad Length" PDF syntax errors
3704                let mut corrected_dict = dict.clone();
3705                corrected_dict.set("Length", Object::Integer(data.len() as i64));
3706
3707                self.write_object_value(&Object::Dictionary(corrected_dict))?;
3708                self.write_bytes(b"\nstream\n")?;
3709                self.write_bytes(data)?;
3710                self.write_bytes(b"\nendstream")?;
3711            }
3712            Object::Reference(id) => {
3713                let ref_str = format!("{} {} R", id.number(), id.generation());
3714                self.write_bytes(ref_str.as_bytes())?;
3715            }
3716        }
3717        Ok(())
3718    }
3719
3720    /// Write object value to a buffer (for object streams)
3721    fn write_object_value_to_buffer(&self, object: &Object, buffer: &mut Vec<u8>) -> Result<()> {
3722        match object {
3723            Object::Null => buffer.extend_from_slice(b"null"),
3724            Object::Boolean(b) => buffer.extend_from_slice(if *b { b"true" } else { b"false" }),
3725            Object::Integer(i) => buffer.extend_from_slice(i.to_string().as_bytes()),
3726            Object::Real(f) => buffer.extend_from_slice(
3727                format!("{f:.6}")
3728                    .trim_end_matches('0')
3729                    .trim_end_matches('.')
3730                    .as_bytes(),
3731            ),
3732            Object::String(s) => {
3733                // Same escape rules as the streaming `write_object_value`
3734                // path — see ISO 32000-1 §7.3.4.2.
3735                buffer.push(b'(');
3736                buffer.extend_from_slice(&escape_pdf_string_bytes(s.as_bytes()));
3737                buffer.push(b')');
3738            }
3739            Object::ByteString(bytes) => {
3740                buffer.push(b'<');
3741                for byte in bytes {
3742                    buffer.extend_from_slice(format!("{byte:02X}").as_bytes());
3743                }
3744                buffer.push(b'>');
3745            }
3746            Object::Name(n) => {
3747                buffer.push(b'/');
3748                buffer.extend_from_slice(n.as_bytes());
3749            }
3750            Object::Array(arr) => {
3751                buffer.push(b'[');
3752                for (i, obj) in arr.iter().enumerate() {
3753                    if i > 0 {
3754                        buffer.push(b' ');
3755                    }
3756                    self.write_object_value_to_buffer(obj, buffer)?;
3757                }
3758                buffer.push(b']');
3759            }
3760            Object::Dictionary(dict) => {
3761                // Same deterministic-order rule as the streaming writer
3762                // (see `write_object_value`): sort entries by key for
3763                // reproducible output across builds.
3764                buffer.extend_from_slice(b"<<");
3765                let mut entries: Vec<(&String, &Object)> = dict.entries().collect();
3766                entries.sort_by_key(|(k, _)| k.as_str());
3767                for (key, value) in entries {
3768                    buffer.extend_from_slice(b"\n/");
3769                    buffer.extend_from_slice(key.as_bytes());
3770                    buffer.push(b' ');
3771                    self.write_object_value_to_buffer(value, buffer)?;
3772                }
3773                buffer.extend_from_slice(b"\n>>");
3774            }
3775            Object::Stream(_, _) => {
3776                // Streams should never be compressed in object streams
3777                return Err(crate::error::PdfError::ObjectStreamError(
3778                    "Cannot compress stream objects in object streams".to_string(),
3779                ));
3780            }
3781            Object::Reference(id) => {
3782                let ref_str = format!("{} {} R", id.number(), id.generation());
3783                buffer.extend_from_slice(ref_str.as_bytes());
3784            }
3785        }
3786        Ok(())
3787    }
3788
3789    /// Flush buffered objects as compressed object streams
3790    fn flush_object_streams(&mut self) -> Result<()> {
3791        if self.buffered_objects.is_empty() {
3792            return Ok(());
3793        }
3794
3795        // Create object stream writer
3796        let config = ObjectStreamConfig {
3797            max_objects_per_stream: 100,
3798            compression_level: 6,
3799            enabled: true,
3800        };
3801        let mut os_writer = ObjectStreamWriter::new(config);
3802
3803        // Sort buffered objects by ID for deterministic output
3804        let mut buffered: Vec<_> = self.buffered_objects.iter().collect();
3805        buffered.sort_by_key(|(id, _)| id.number());
3806
3807        // Add all buffered objects to the stream writer
3808        for (id, data) in buffered {
3809            os_writer.add_object(*id, data.clone())?;
3810        }
3811
3812        // Finalize and get completed streams
3813        let streams = os_writer.finalize()?;
3814
3815        // Write each object stream to the PDF
3816        for mut stream in streams {
3817            let stream_id = stream.stream_id;
3818
3819            // Generate compressed stream data
3820            let compressed_data = stream.generate_stream_data(6)?;
3821
3822            // Generate stream dictionary
3823            let dict = stream.generate_dictionary(&compressed_data);
3824
3825            // Track compressed object mapping for xref
3826            for (index, (obj_id, _)) in stream.objects.iter().enumerate() {
3827                self.compressed_object_map
3828                    .insert(*obj_id, (stream_id, index as u32));
3829            }
3830
3831            // Write the object stream itself
3832            self.xref_positions.insert(stream_id, self.current_position);
3833
3834            let header = format!("{} {} obj\n", stream_id.number(), stream_id.generation());
3835            self.write_bytes(header.as_bytes())?;
3836
3837            self.write_object_value(&Object::Dictionary(dict))?;
3838
3839            self.write_bytes(b"\nstream\n")?;
3840            self.write_bytes(&compressed_data)?;
3841            self.write_bytes(b"\nendstream\nendobj\n")?;
3842        }
3843
3844        Ok(())
3845    }
3846
3847    fn write_xref(&mut self) -> Result<()> {
3848        self.write_bytes(b"xref\n")?;
3849
3850        // Sort by object number and write entries
3851        let mut entries: Vec<_> = self
3852            .xref_positions
3853            .iter()
3854            .map(|(id, pos)| (*id, *pos))
3855            .collect();
3856        entries.sort_by_key(|(id, _)| id.number());
3857
3858        // Find the highest object number to determine size
3859        let max_obj_num = entries.iter().map(|(id, _)| id.number()).max().unwrap_or(0);
3860
3861        // Write subsection header - PDF 1.7 spec allows multiple subsections
3862        // For simplicity, write one subsection from 0 to max
3863        self.write_bytes(b"0 ")?;
3864        self.write_bytes((max_obj_num + 1).to_string().as_bytes())?;
3865        self.write_bytes(b"\n")?;
3866
3867        // Write free object entry
3868        self.write_bytes(b"0000000000 65535 f \n")?;
3869
3870        // Write entries for all object numbers from 1 to max
3871        // Fill in gaps with free entries
3872        for obj_num in 1..=max_obj_num {
3873            let _obj_id = ObjectId::new(obj_num, 0);
3874            if let Some((_, position)) = entries.iter().find(|(id, _)| id.number() == obj_num) {
3875                let entry = format!("{:010} {:05} n \n", position, 0);
3876                self.write_bytes(entry.as_bytes())?;
3877            } else {
3878                // Free entry for gap
3879                self.write_bytes(b"0000000000 00000 f \n")?;
3880            }
3881        }
3882
3883        Ok(())
3884    }
3885
3886    fn write_xref_stream(&mut self) -> Result<()> {
3887        let catalog_id = self.get_catalog_id()?;
3888        let info_id = self.get_info_id()?;
3889
3890        // Allocate object ID for the xref stream
3891        let xref_stream_id = self.allocate_object_id();
3892        let xref_position = self.current_position;
3893
3894        // Create XRef stream writer with trailer information
3895        let mut xref_writer = XRefStreamWriter::new(xref_stream_id);
3896        xref_writer.set_trailer_info(catalog_id, info_id);
3897
3898        // Add free entry for object 0
3899        xref_writer.add_free_entry(0, 65535);
3900
3901        // Sort entries by object number
3902        let mut entries: Vec<_> = self
3903            .xref_positions
3904            .iter()
3905            .map(|(id, pos)| (*id, *pos))
3906            .collect();
3907        entries.sort_by_key(|(id, _)| id.number());
3908
3909        // Find the highest object number (including the xref stream itself)
3910        let max_obj_num = entries
3911            .iter()
3912            .map(|(id, _)| id.number())
3913            .max()
3914            .unwrap_or(0)
3915            .max(xref_stream_id.number());
3916
3917        // Add entries for all objects (including compressed objects)
3918        for obj_num in 1..=max_obj_num {
3919            let obj_id = ObjectId::new(obj_num, 0);
3920
3921            if obj_num == xref_stream_id.number() {
3922                // The xref stream entry will be added with the correct position
3923                xref_writer.add_in_use_entry(xref_position, 0);
3924            } else if let Some((stream_id, index)) = self.compressed_object_map.get(&obj_id) {
3925                // Type 2: Object is compressed in an object stream
3926                xref_writer.add_compressed_entry(stream_id.number(), *index);
3927            } else if let Some((id, position)) =
3928                entries.iter().find(|(id, _)| id.number() == obj_num)
3929            {
3930                // Type 1: Regular in-use entry
3931                xref_writer.add_in_use_entry(*position, id.generation());
3932            } else {
3933                // Type 0: Free entry for gap
3934                xref_writer.add_free_entry(0, 0);
3935            }
3936        }
3937
3938        // Mark position for xref stream object
3939        self.xref_positions.insert(xref_stream_id, xref_position);
3940
3941        // Write object header
3942        self.write_bytes(
3943            format!(
3944                "{} {} obj\n",
3945                xref_stream_id.number(),
3946                xref_stream_id.generation()
3947            )
3948            .as_bytes(),
3949        )?;
3950
3951        // Get the encoded data
3952        let uncompressed_data = xref_writer.encode_entries();
3953        let final_data = if self.config.compress_streams {
3954            crate::compression::compress(&uncompressed_data)?
3955        } else {
3956            uncompressed_data
3957        };
3958
3959        // Create and write dictionary
3960        let mut dict = xref_writer.create_dictionary(None);
3961        dict.set("Length", Object::Integer(final_data.len() as i64));
3962
3963        // Add filter if compression is enabled
3964        if self.config.compress_streams {
3965            dict.set("Filter", Object::Name("FlateDecode".to_string()));
3966        }
3967        self.write_bytes(b"<<")?;
3968        for (key, value) in dict.iter() {
3969            self.write_bytes(b"\n/")?;
3970            self.write_bytes(key.as_bytes())?;
3971            self.write_bytes(b" ")?;
3972            self.write_object_value(value)?;
3973        }
3974        self.write_bytes(b"\n>>\n")?;
3975
3976        // Write stream
3977        self.write_bytes(b"stream\n")?;
3978        self.write_bytes(&final_data)?;
3979        self.write_bytes(b"\nendstream\n")?;
3980        self.write_bytes(b"endobj\n")?;
3981
3982        // Write startxref and EOF
3983        self.write_bytes(b"\nstartxref\n")?;
3984        self.write_bytes(xref_position.to_string().as_bytes())?;
3985        self.write_bytes(b"\n%%EOF\n")?;
3986
3987        Ok(())
3988    }
3989
3990    /// Write the encryption dictionary as an indirect object and store
3991    /// the object ID and file ID for the trailer.
3992    /// Initialize encryption state: generates file ID, creates encryption dict,
3993    /// computes encryption key, and builds the ObjectEncryptor.
3994    /// The /Encrypt dict object is written later (after all other objects) since it
3995    /// must NOT be encrypted itself (ISO 32000-1 §7.6.1).
3996    fn init_encryption(&mut self, encryption: &crate::document::DocumentEncryption) -> Result<()> {
3997        use crate::encryption::{
3998            CryptFilterManager, CryptFilterMethod, FunctionalCryptFilter, ObjectEncryptor,
3999        };
4000        use std::sync::Arc;
4001
4002        // Generate file ID (16 random bytes, required by ISO 32000-1 §7.5.5)
4003        let mut fid = vec![0u8; 16];
4004        use rand::Rng;
4005        rand::rng().fill_bytes(&mut fid);
4006
4007        let enc_dict = encryption
4008            .create_encryption_dict(Some(&fid))
4009            .map_err(|e| PdfError::EncryptionError(format!("encryption dict: {}", e)))?;
4010
4011        // Compute encryption key
4012        let enc_key = encryption
4013            .get_encryption_key(&enc_dict, Some(&fid))
4014            .map_err(|e| PdfError::EncryptionError(format!("encryption key: {}", e)))?;
4015
4016        // Build CryptFilterManager based on encryption strength
4017        let handler = encryption.handler();
4018        let (method, key_len) = match encryption.strength {
4019            crate::document::EncryptionStrength::Rc4_40bit => (CryptFilterMethod::V2, Some(5)),
4020            crate::document::EncryptionStrength::Rc4_128bit => (CryptFilterMethod::V2, Some(16)),
4021            crate::document::EncryptionStrength::Aes128 => (CryptFilterMethod::AESV2, Some(16)),
4022            crate::document::EncryptionStrength::Aes256 => (CryptFilterMethod::AESV3, Some(32)),
4023        };
4024
4025        let std_filter = FunctionalCryptFilter {
4026            name: "StdCF".to_string(),
4027            method,
4028            length: key_len,
4029            auth_event: crate::encryption::AuthEvent::DocOpen,
4030            recipients: None,
4031        };
4032
4033        let mut filter_manager =
4034            CryptFilterManager::new(Box::new(handler), "StdCF".to_string(), "StdCF".to_string());
4035        filter_manager.add_filter(std_filter);
4036
4037        let encryptor =
4038            ObjectEncryptor::new(Arc::new(filter_manager), enc_key, enc_dict.encrypt_metadata);
4039
4040        // Reserve ID for /Encrypt dict (will be written at the end)
4041        let encrypt_id = self.allocate_object_id();
4042        self.encrypt_obj_id = Some(encrypt_id);
4043        self.file_id = Some(fid);
4044        self.encryption_state = Some(WriterEncryptionState { encryptor });
4045
4046        // Store the dict to write later
4047        self.pending_encrypt_dict = Some(enc_dict.to_dict());
4048
4049        Ok(())
4050    }
4051
4052    /// Write the /Encrypt dictionary object (must NOT be encrypted per ISO 32000-1 §7.6.1)
4053    fn write_encryption_dict(&mut self) -> Result<()> {
4054        if let (Some(encrypt_id), Some(dict)) =
4055            (self.encrypt_obj_id, self.pending_encrypt_dict.take())
4056        {
4057            // Temporarily disable encryption so the /Encrypt dict is not encrypted
4058            let enc_state = self.encryption_state.take();
4059            self.write_object(encrypt_id, Object::Dictionary(dict))?;
4060            self.encryption_state = enc_state;
4061        }
4062        Ok(())
4063    }
4064
4065    fn write_trailer(&mut self, xref_position: u64) -> Result<()> {
4066        let catalog_id = self.get_catalog_id()?;
4067        let info_id = self.get_info_id()?;
4068        // Find the highest object number to determine size
4069        let max_obj_num = self
4070            .xref_positions
4071            .keys()
4072            .map(|id| id.number())
4073            .max()
4074            .unwrap_or(0);
4075
4076        let mut trailer = Dictionary::new();
4077        trailer.set("Size", Object::Integer((max_obj_num + 1) as i64));
4078        trailer.set("Root", Object::Reference(catalog_id));
4079        trailer.set("Info", Object::Reference(info_id));
4080
4081        // Add /Prev pointer for incremental updates (ISO 32000-1 §7.5.6)
4082        if let Some(prev_xref) = self.prev_xref_offset {
4083            trailer.set("Prev", Object::Integer(prev_xref as i64));
4084        }
4085
4086        // Add /Encrypt reference and /ID array for encrypted documents
4087        if let Some(encrypt_id) = self.encrypt_obj_id {
4088            trailer.set("Encrypt", Object::Reference(encrypt_id));
4089        }
4090        if let Some(ref fid) = self.file_id {
4091            trailer.set(
4092                "ID",
4093                Object::Array(vec![
4094                    Object::ByteString(fid.clone()),
4095                    Object::ByteString(fid.clone()),
4096                ]),
4097            );
4098        }
4099
4100        self.write_bytes(b"trailer\n")?;
4101        self.write_object_value(&Object::Dictionary(trailer))?;
4102        self.write_bytes(b"\nstartxref\n")?;
4103        self.write_bytes(xref_position.to_string().as_bytes())?;
4104        self.write_bytes(b"\n%%EOF\n")?;
4105
4106        Ok(())
4107    }
4108
4109    fn write_bytes(&mut self, data: &[u8]) -> Result<()> {
4110        self.writer.write_all(data)?;
4111        self.current_position += data.len() as u64;
4112        Ok(())
4113    }
4114
4115    #[allow(dead_code)]
4116    fn create_widget_appearance_stream(&mut self, widget_dict: &Dictionary) -> Result<ObjectId> {
4117        // Get widget rectangle
4118        let rect = if let Some(Object::Array(rect_array)) = widget_dict.get("Rect") {
4119            if rect_array.len() >= 4 {
4120                if let (
4121                    Some(Object::Real(x1)),
4122                    Some(Object::Real(y1)),
4123                    Some(Object::Real(x2)),
4124                    Some(Object::Real(y2)),
4125                ) = (
4126                    rect_array.first(),
4127                    rect_array.get(1),
4128                    rect_array.get(2),
4129                    rect_array.get(3),
4130                ) {
4131                    (*x1, *y1, *x2, *y2)
4132                } else {
4133                    (0.0, 0.0, 100.0, 20.0) // Default
4134                }
4135            } else {
4136                (0.0, 0.0, 100.0, 20.0) // Default
4137            }
4138        } else {
4139            (0.0, 0.0, 100.0, 20.0) // Default
4140        };
4141
4142        let width = rect.2 - rect.0;
4143        let height = rect.3 - rect.1;
4144
4145        // Create appearance stream content
4146        let mut content = String::new();
4147
4148        // Set graphics state
4149        content.push_str("q\n");
4150
4151        // Draw border (black) — single source of truth for color emission.
4152        crate::graphics::color::write_stroke_color(&mut content, crate::graphics::Color::black());
4153        content.push_str("1 w\n"); // 1pt line width
4154
4155        // Draw rectangle border
4156        content.push_str(&format!("0 0 {width} {height} re\n"));
4157        content.push_str("S\n"); // Stroke
4158
4159        // Fill with white background
4160        crate::graphics::color::write_fill_color(&mut content, crate::graphics::Color::white());
4161        content.push_str(&format!("0.5 0.5 {} {} re\n", width - 1.0, height - 1.0));
4162        content.push_str("f\n"); // Fill
4163
4164        // Restore graphics state
4165        content.push_str("Q\n");
4166
4167        // Create stream dictionary
4168        let mut stream_dict = Dictionary::new();
4169        stream_dict.set("Type", Object::Name("XObject".to_string()));
4170        stream_dict.set("Subtype", Object::Name("Form".to_string()));
4171        stream_dict.set(
4172            "BBox",
4173            Object::Array(vec![
4174                Object::Real(0.0),
4175                Object::Real(0.0),
4176                Object::Real(width),
4177                Object::Real(height),
4178            ]),
4179        );
4180        stream_dict.set("Resources", Object::Dictionary(Dictionary::new()));
4181        stream_dict.set("Length", Object::Integer(content.len() as i64));
4182
4183        // Write the appearance stream
4184        let stream_id = self.allocate_object_id();
4185        self.write_object(stream_id, Object::Stream(stream_dict, content.into_bytes()))?;
4186
4187        Ok(stream_id)
4188    }
4189
4190    #[allow(dead_code)]
4191    fn create_field_appearance_stream(
4192        &mut self,
4193        field_dict: &Dictionary,
4194        widget: &crate::forms::Widget,
4195    ) -> Result<ObjectId> {
4196        let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
4197        let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
4198
4199        // Create appearance stream content
4200        let mut content = String::new();
4201
4202        // Set graphics state
4203        content.push_str("q\n");
4204
4205        // Draw background if specified — routed through the shared
4206        // NaN-sanitising helpers (issues #220, #221).
4207        if let Some(bg_color) = &widget.appearance.background_color {
4208            crate::graphics::color::write_fill_color(&mut content, *bg_color);
4209            content.push_str(&format!("0 0 {width} {height} re\n"));
4210            content.push_str("f\n");
4211        }
4212
4213        // Draw border
4214        if let Some(border_color) = &widget.appearance.border_color {
4215            crate::graphics::color::write_stroke_color(&mut content, *border_color);
4216            content.push_str(&format!("{} w\n", widget.appearance.border_width));
4217            content.push_str(&format!("0 0 {width} {height} re\n"));
4218            content.push_str("S\n");
4219        }
4220
4221        // For checkboxes, add a checkmark if checked
4222        if let Some(Object::Name(ft)) = field_dict.get("FT") {
4223            if ft == "Btn" {
4224                if let Some(Object::Name(v)) = field_dict.get("V") {
4225                    if v == "Yes" {
4226                        // Draw checkmark
4227                        crate::graphics::color::write_stroke_color(
4228                            &mut content,
4229                            crate::graphics::Color::black(),
4230                        );
4231                        content.push_str("2 w\n");
4232                        let margin = width * 0.2;
4233                        content.push_str(&format!("{} {} m\n", margin, height / 2.0));
4234                        content.push_str(&format!("{} {} l\n", width / 2.0, margin));
4235                        content.push_str(&format!("{} {} l\n", width - margin, height - margin));
4236                        content.push_str("S\n");
4237                    }
4238                }
4239            }
4240        }
4241
4242        // Restore graphics state
4243        content.push_str("Q\n");
4244
4245        // Create stream dictionary
4246        let mut stream_dict = Dictionary::new();
4247        stream_dict.set("Type", Object::Name("XObject".to_string()));
4248        stream_dict.set("Subtype", Object::Name("Form".to_string()));
4249        stream_dict.set(
4250            "BBox",
4251            Object::Array(vec![
4252                Object::Real(0.0),
4253                Object::Real(0.0),
4254                Object::Real(width),
4255                Object::Real(height),
4256            ]),
4257        );
4258        stream_dict.set("Resources", Object::Dictionary(Dictionary::new()));
4259        stream_dict.set("Length", Object::Integer(content.len() as i64));
4260
4261        // Write the appearance stream
4262        let stream_id = self.allocate_object_id();
4263        self.write_object(stream_id, Object::Stream(stream_dict, content.into_bytes()))?;
4264
4265        Ok(stream_id)
4266    }
4267}
4268
4269/// Format a DateTime as a PDF date string (D:YYYYMMDDHHmmSSOHH'mm)
4270fn format_pdf_date(date: DateTime<Utc>) -> String {
4271    // Format the UTC date according to PDF specification
4272    // D:YYYYMMDDHHmmSSOHH'mm where O is the relationship of local time to UTC (+ or -)
4273    let formatted = date.format("D:%Y%m%d%H%M%S");
4274
4275    // For UTC, the offset is always +00'00
4276    format!("{formatted}+00'00")
4277}
4278
4279#[cfg(test)]
4280mod tests;
4281
4282#[cfg(test)]
4283mod rigorous_tests;