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