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