Skip to main content

pivot_pdf/
document.rs

1use std::collections::BTreeMap;
2use std::collections::BTreeSet;
3use std::fmt;
4use std::fs::File;
5use std::io::{self, BufWriter, Write};
6use std::path::Path;
7
8use flate2::write::ZlibEncoder;
9use flate2::Compression;
10
11use crate::fonts::{BuiltinFont, FontRef, TrueTypeFontId};
12use crate::graphics::{Angle, Color};
13use crate::images::{self, ImageData, ImageFit, ImageFormat, ImageId};
14use crate::objects::{ObjId, PdfObject};
15use crate::tables::{Row, Table, TableCursor};
16use crate::textflow::{FitResult, Rect, TextFlow, TextStyle};
17use crate::truetype::TrueTypeFont;
18use crate::writer::PdfWriter;
19
20// -------------------------------------------------------
21// Coordinate origin types
22// -------------------------------------------------------
23
24/// Coordinate origin used for all user-supplied coordinates.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Origin {
27    /// PDF's native bottom-left origin; y increases upward. This is the default.
28    BottomLeft,
29    /// Screen/web-style top-left origin; y increases downward.
30    TopLeft,
31}
32
33/// Options for configuring a new PDF document.
34#[derive(Debug, Clone)]
35pub struct DocumentOptions {
36    /// Coordinate origin for all user-supplied coordinates.
37    /// Defaults to `Origin::BottomLeft` (PDF native).
38    pub origin: Origin,
39}
40
41impl Default for DocumentOptions {
42    fn default() -> Self {
43        DocumentOptions {
44            origin: Origin::BottomLeft,
45        }
46    }
47}
48
49// -------------------------------------------------------
50// Form field types
51// -------------------------------------------------------
52
53/// Errors that can occur when adding form fields to a document.
54#[derive(Debug)]
55pub enum FormFieldError {
56    /// `add_text_field` was called without an active page.
57    NoActivePage,
58    /// A field with the given name already exists in this document.
59    DuplicateName(String),
60}
61
62impl fmt::Display for FormFieldError {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            FormFieldError::NoActivePage => write!(f, "add_text_field called with no active page"),
66            FormFieldError::DuplicateName(name) => {
67                write!(f, "duplicate field name: '{}'", name)
68            }
69        }
70    }
71}
72
73impl std::error::Error for FormFieldError {}
74
75/// A form field definition accumulated while a page is open.
76struct FormFieldDef {
77    name: String,
78    rect: Rect,
79}
80
81/// A form field with a pre-allocated PDF object ID, stored in `PageRecord`.
82struct FormFieldRecord {
83    name: String,
84    rect: Rect,
85    obj_id: ObjId,
86}
87
88const CATALOG_OBJ: ObjId = ObjId(1, 0);
89const PAGES_OBJ: ObjId = ObjId(2, 0);
90const FIRST_PAGE_OBJ_NUM: u32 = 3;
91
92/// Pre-allocated object IDs for an image XObject.
93struct ImageObjIds {
94    xobject: ObjId,
95    smask: Option<ObjId>,
96    pdf_name: String,
97}
98
99/// Pre-allocated object IDs for a TrueType font's PDF objects.
100struct TrueTypeFontObjIds {
101    type0: ObjId,
102    cid_font: ObjId,
103    descriptor: ObjId,
104    font_file: ObjId,
105    tounicode: ObjId,
106}
107
108/// Accumulated record for a completed page.
109/// Page dictionaries are deferred until `end_document()` so that
110/// overlay content streams (e.g. page numbers) can be appended
111/// after all pages have been written.
112struct PageRecord {
113    obj_id: ObjId,
114    /// Content stream IDs: first is the main stream, any beyond that are overlays.
115    content_ids: Vec<ObjId>,
116    width: f64,
117    height: f64,
118    used_fonts: BTreeSet<BuiltinFont>,
119    used_truetype_fonts: BTreeSet<usize>,
120    used_images: BTreeSet<usize>,
121    /// Form fields on this page with pre-allocated object IDs.
122    fields: Vec<FormFieldRecord>,
123}
124
125/// High-level API for building PDF documents.
126///
127/// Generic over `Write` so it works with files (`BufWriter<File>`),
128/// in-memory buffers (`Vec<u8>`), or any other writer.
129///
130/// Pages are written incrementally: `end_page()` flushes page data
131/// to the writer and frees page content from memory. This keeps
132/// memory usage low even for documents with hundreds of pages.
133pub struct PdfDocument<W: Write> {
134    writer: PdfWriter<W>,
135    info: Vec<(String, String)>,
136    page_records: Vec<PageRecord>,
137    current_page: Option<PageBuilder>,
138    next_obj_num: u32,
139    /// Maps each used builtin font to its written ObjId.
140    font_obj_ids: BTreeMap<BuiltinFont, ObjId>,
141    /// Loaded TrueType fonts.
142    truetype_fonts: Vec<TrueTypeFont>,
143    /// Pre-allocated ObjIds for TrueType fonts (by index).
144    truetype_font_obj_ids: BTreeMap<usize, TrueTypeFontObjIds>,
145    /// Next font number for PDF resource names (F15, F16, ...).
146    next_font_num: u32,
147    /// Whether to compress stream objects with FlateDecode.
148    compress: bool,
149    /// Loaded images.
150    images: Vec<ImageData>,
151    /// Pre-allocated ObjIds for images (by index).
152    image_obj_ids: BTreeMap<usize, ImageObjIds>,
153    /// Images whose XObjects have already been written.
154    written_images: BTreeSet<usize>,
155    /// Next image number for PDF resource names (Im1, Im2, ...).
156    next_image_num: u32,
157    /// Document-level set of used form field names (enforces uniqueness).
158    form_field_names: BTreeSet<String>,
159    /// Coordinate origin for all user-supplied coordinates.
160    origin: Origin,
161}
162
163struct PageBuilder {
164    width: f64,
165    height: f64,
166    content_ops: Vec<u8>,
167    used_fonts: BTreeSet<BuiltinFont>,
168    used_truetype_fonts: BTreeSet<usize>,
169    used_images: BTreeSet<usize>,
170    /// When `Some(idx)`, this builder is adding an overlay to `page_records[idx]`
171    /// rather than creating a new page.
172    overlay_for: Option<usize>,
173    /// Form fields added while this page was open.
174    fields: Vec<FormFieldDef>,
175}
176
177impl PdfDocument<BufWriter<File>> {
178    /// Create a new PDF document that writes to a file.
179    pub fn create<P: AsRef<Path>>(path: P, options: DocumentOptions) -> io::Result<Self> {
180        let file = File::create(path)?;
181        Self::new(BufWriter::new(file), options)
182    }
183}
184
185impl<W: Write> PdfDocument<W> {
186    /// Create a new PDF document that writes to the given writer.
187    /// Writes the PDF header immediately.
188    pub fn new(writer: W, options: DocumentOptions) -> io::Result<Self> {
189        let mut pdf_writer = PdfWriter::new(writer);
190        pdf_writer.write_header()?;
191
192        Ok(PdfDocument {
193            writer: pdf_writer,
194            info: Vec::new(),
195            page_records: Vec::new(),
196            current_page: None,
197            next_obj_num: FIRST_PAGE_OBJ_NUM,
198            font_obj_ids: BTreeMap::new(),
199            truetype_fonts: Vec::new(),
200            truetype_font_obj_ids: BTreeMap::new(),
201            next_font_num: 15,
202            compress: false,
203            images: Vec::new(),
204            image_obj_ids: BTreeMap::new(),
205            written_images: BTreeSet::new(),
206            next_image_num: 1,
207            form_field_names: BTreeSet::new(),
208            origin: options.origin,
209        })
210    }
211
212    /// Set a document info entry (e.g. "Creator", "Title").
213    pub fn set_info(&mut self, key: &str, value: &str) -> &mut Self {
214        self.info.push((key.to_string(), value.to_string()));
215        self
216    }
217
218    /// Enable or disable FlateDecode compression for stream objects.
219    /// When enabled, page content, embedded fonts, and ToUnicode CMaps
220    /// are compressed, typically reducing file size by 50-80%.
221    /// Disabled by default.
222    pub fn set_compression(&mut self, enabled: bool) -> &mut Self {
223        self.compress = enabled;
224        self
225    }
226
227    /// Load a TrueType font from a file path.
228    /// Returns a FontRef that can be used in TextStyle.
229    pub fn load_font_file<P: AsRef<Path>>(&mut self, path: P) -> Result<FontRef, String> {
230        let data =
231            std::fs::read(path.as_ref()).map_err(|e| format!("Failed to read font file: {}", e))?;
232        self.load_font_bytes(data)
233    }
234
235    /// Load a TrueType font from raw bytes.
236    /// Returns a FontRef that can be used in TextStyle.
237    pub fn load_font_bytes(&mut self, data: Vec<u8>) -> Result<FontRef, String> {
238        let font_num = self.next_font_num;
239        self.next_font_num += 1;
240        let font = TrueTypeFont::from_bytes(data, font_num)?;
241        let idx = self.truetype_fonts.len();
242        self.truetype_fonts.push(font);
243        Ok(FontRef::TrueType(TrueTypeFontId(idx)))
244    }
245
246    /// Returns the number of completed pages (pages for which `end_page` has been called).
247    pub fn page_count(&self) -> usize {
248        self.page_records.len()
249    }
250
251    /// Begin a new page with the given dimensions in points.
252    /// If a page is currently open, it is automatically closed.
253    pub fn begin_page(&mut self, width: f64, height: f64) -> &mut Self {
254        if self.current_page.is_some() {
255            let _ = self.end_page();
256        }
257        self.current_page = Some(PageBuilder {
258            width,
259            height,
260            content_ops: Vec::new(),
261            used_fonts: BTreeSet::new(),
262            used_truetype_fonts: BTreeSet::new(),
263            used_images: BTreeSet::new(),
264            overlay_for: None,
265            fields: Vec::new(),
266        });
267        self
268    }
269
270    /// Open a completed page for editing (1-indexed).
271    ///
272    /// Used for adding overlay content such as page numbers ("Page X of Y")
273    /// after all pages have been written. The overlay content is written as
274    /// an additional content stream appended to the page's `/Contents` array.
275    ///
276    /// If a page is currently open, it is automatically closed first.
277    ///
278    /// Returns an error if `page_num` is out of range.
279    pub fn open_page(&mut self, page_num: usize) -> io::Result<()> {
280        if page_num == 0 || page_num > self.page_records.len() {
281            return Err(io::Error::new(
282                io::ErrorKind::InvalidInput,
283                format!(
284                    "open_page: page_num {} out of range (1..={})",
285                    page_num,
286                    self.page_records.len()
287                ),
288            ));
289        }
290
291        if self.current_page.is_some() {
292            self.end_page()?;
293        }
294
295        let idx = page_num - 1;
296        let width = self.page_records[idx].width;
297        let height = self.page_records[idx].height;
298
299        self.current_page = Some(PageBuilder {
300            width,
301            height,
302            content_ops: Vec::new(),
303            used_fonts: BTreeSet::new(),
304            used_truetype_fonts: BTreeSet::new(),
305            used_images: BTreeSet::new(),
306            overlay_for: Some(idx),
307            fields: Vec::new(),
308        });
309
310        Ok(())
311    }
312
313    /// Place text at position (x, y) using default 12pt Helvetica.
314    ///
315    /// With [`Origin::BottomLeft`] (default), `(x, y)` is in PDF's native
316    /// bottom-left coordinate system. With [`Origin::TopLeft`], `y` is measured
317    /// from the top of the page, increasing downward.
318    pub fn place_text(&mut self, text: &str, x: f64, y: f64) -> &mut Self {
319        let y_pdf = self.transform_y(y);
320        let page = self
321            .current_page
322            .as_mut()
323            .expect("place_text called with no open page");
324        page.used_fonts.insert(BuiltinFont::Helvetica);
325        let escaped = crate::writer::escape_pdf_string(text);
326        let ops = format!(
327            "BT\n/F1 12 Tf\n{} {} Td\n({}) Tj\nET\n",
328            format_coord(x),
329            format_coord(y_pdf),
330            escaped,
331        );
332        page.content_ops.extend_from_slice(ops.as_bytes());
333        self
334    }
335
336    /// Place text at position (x, y) with the given style.
337    ///
338    /// With [`Origin::BottomLeft`] (default), `(x, y)` is in PDF's native
339    /// bottom-left coordinate system. With [`Origin::TopLeft`], `y` is measured
340    /// from the top of the page, increasing downward.
341    pub fn place_text_styled(
342        &mut self,
343        text: &str,
344        x: f64,
345        y: f64,
346        style: &TextStyle,
347    ) -> &mut Self {
348        // Encode text before borrowing page mutably
349        let (font_name, text_op) = match style.font {
350            FontRef::Builtin(b) => {
351                let escaped = crate::writer::escape_pdf_string(text);
352                (b.pdf_name().to_string(), format!("({}) Tj", escaped))
353            }
354            FontRef::TrueType(id) => {
355                let font = &mut self.truetype_fonts[id.0];
356                let hex = font.encode_text_hex(text);
357                (font.pdf_name.clone(), format!("{} Tj", hex))
358            }
359        };
360
361        let y_pdf = self.transform_y(y);
362        let page = self
363            .current_page
364            .as_mut()
365            .expect("place_text_styled called with no open page");
366
367        match style.font {
368            FontRef::Builtin(b) => {
369                page.used_fonts.insert(b);
370            }
371            FontRef::TrueType(id) => {
372                page.used_truetype_fonts.insert(id.0);
373            }
374        }
375
376        let ops = format!(
377            "BT\n/{} {} Tf\n{} {} Td\n{}\nET\n",
378            font_name,
379            format_coord(style.font_size),
380            format_coord(x),
381            format_coord(y_pdf),
382            text_op,
383        );
384        page.content_ops.extend_from_slice(ops.as_bytes());
385        self
386    }
387
388    /// Fit a TextFlow into a bounding rectangle on the current page.
389    ///
390    /// The flow's cursor advances so subsequent calls continue where it left off
391    /// (for multi-page flow). With [`Origin::TopLeft`], `(rect.x, rect.y)` is
392    /// the top-left corner of the area.
393    pub fn fit_textflow(&mut self, flow: &mut TextFlow, rect: &Rect) -> io::Result<FitResult> {
394        let pdf_rect = self.transform_rect_top_edge(rect);
395        let (ops, result, used_fonts) =
396            flow.generate_content_ops(&pdf_rect, &mut self.truetype_fonts);
397
398        let page = self
399            .current_page
400            .as_mut()
401            .expect("fit_textflow called with no open page");
402        page.content_ops.extend_from_slice(&ops);
403        page.used_fonts.extend(used_fonts.builtin);
404        page.used_truetype_fonts.extend(used_fonts.truetype);
405        Ok(result)
406    }
407
408    /// Place a single table row on the current page.
409    ///
410    /// `cursor` tracks the current Y position within the page. Pass the same
411    /// cursor to successive calls; call `cursor.reset()` when starting a new page.
412    ///
413    /// Returns:
414    /// - `Stop`     — row placed; advance to the next row.
415    /// - `BoxFull`  — page full; end the page, begin a new one, reset the cursor, retry.
416    /// - `BoxEmpty` — rect too small for this row even from the top; skip or abort.
417    pub fn fit_row(
418        &mut self,
419        table: &Table,
420        row: &Row,
421        cursor: &mut TableCursor,
422    ) -> io::Result<FitResult> {
423        let total_span: usize = row.cells.iter().map(|c| c.col_span.max(1)).sum();
424        if total_span != table.columns.len() {
425            return Err(io::Error::new(
426                io::ErrorKind::InvalidInput,
427                format!(
428                    "row col_span sum ({}) must equal table column count ({})",
429                    total_span,
430                    table.columns.len()
431                ),
432            ));
433        }
434
435        let (ops, result, used_fonts) = match self.origin {
436            Origin::BottomLeft => table.generate_row_ops(row, cursor, &mut self.truetype_fonts),
437            Origin::TopLeft => {
438                let page_h = self.current_page_height();
439                // Convert cursor from user (TopLeft) to PDF top-edge coords.
440                let pdf_top = page_h - cursor.rect.y;
441                let pdf_rect = Rect {
442                    x: cursor.rect.x,
443                    y: pdf_top,
444                    width: cursor.rect.width,
445                    height: cursor.rect.height,
446                };
447                let pdf_current_y = page_h - cursor.current_y;
448                let mut pdf_cursor = crate::tables::TableCursor {
449                    rect: pdf_rect,
450                    current_y: pdf_current_y,
451                    first_row: cursor.first_row,
452                };
453                let result = table.generate_row_ops(row, &mut pdf_cursor, &mut self.truetype_fonts);
454                // Back-transform: current_y in PDF → user space.
455                cursor.current_y = page_h - pdf_cursor.current_y;
456                cursor.first_row = pdf_cursor.first_row;
457                result
458            }
459        };
460
461        let page = self
462            .current_page
463            .as_mut()
464            .expect("fit_row called with no open page");
465        page.content_ops.extend_from_slice(&ops);
466        page.used_fonts.extend(used_fonts.builtin);
467        page.used_truetype_fonts.extend(used_fonts.truetype);
468        Ok(result)
469    }
470
471    // -------------------------------------------------------
472    // Image operations
473    // -------------------------------------------------------
474
475    /// Load an image from a file path.
476    /// Returns an ImageId that can be used with `place_image`.
477    pub fn load_image_file<P: AsRef<Path>>(&mut self, path: P) -> Result<ImageId, String> {
478        let data = std::fs::read(path.as_ref())
479            .map_err(|e| format!("Failed to read image file: {}", e))?;
480        self.load_image_bytes(data)
481    }
482
483    /// Load an image from raw bytes (JPEG or PNG).
484    /// Returns an ImageId that can be used with `place_image`.
485    pub fn load_image_bytes(&mut self, data: Vec<u8>) -> Result<ImageId, String> {
486        let image_data = images::load_image(data)?;
487        let idx = self.images.len();
488        self.images.push(image_data);
489        Ok(ImageId(idx))
490    }
491
492    /// Place an image on the current page within the given bounding rect.
493    ///
494    /// With [`Origin::TopLeft`], `(rect.x, rect.y)` is the top-left corner and
495    /// `rect.height` extends downward. With [`Origin::BottomLeft`] (default),
496    /// `rect.y` is the bottom edge in PDF space.
497    pub fn place_image(&mut self, image: &ImageId, rect: &Rect, fit: ImageFit) -> &mut Self {
498        let idx = image.0;
499        let img = &self.images[idx];
500
501        // Transform rect to PDF bottom-left space before computing placement.
502        let pdf_rect = self.transform_rect(rect);
503        let placement = images::calculate_placement(img.width, img.height, &pdf_rect, fit);
504
505        self.ensure_image_obj_ids(idx);
506        let pdf_name = self.image_obj_ids[&idx].pdf_name.clone();
507
508        let page = self
509            .current_page
510            .as_mut()
511            .expect("place_image called with no open page");
512        page.used_images.insert(idx);
513
514        // Build content stream operators
515        let mut ops = String::new();
516        ops.push_str("q\n");
517
518        // Clipping (for Fill mode)
519        if let Some(clip) = &placement.clip {
520            ops.push_str(&format!(
521                "{} {} {} {} re W n\n",
522                format_coord(clip.x),
523                format_coord(clip.y),
524                format_coord(clip.width),
525                format_coord(clip.height),
526            ));
527        }
528
529        // Transformation matrix: scale and position
530        // cm matrix: [width 0 0 height x y]
531        ops.push_str(&format!(
532            "{} 0 0 {} {} {} cm\n",
533            format_coord(placement.width),
534            format_coord(placement.height),
535            format_coord(placement.x),
536            format_coord(placement.y),
537        ));
538
539        // Paint the image
540        ops.push_str(&format!("/{} Do\n", pdf_name));
541        ops.push_str("Q\n");
542
543        page.content_ops.extend_from_slice(ops.as_bytes());
544        self
545    }
546
547    /// Add a fillable text field to the current page.
548    ///
549    /// `name` must be unique across the document. Returns an error if called
550    /// with no active page or if the name has already been used.
551    ///
552    /// With [`Origin::TopLeft`], `(rect.x, rect.y)` is the top-left corner.
553    pub fn add_text_field(&mut self, name: &str, rect: Rect) -> Result<(), FormFieldError> {
554        if self.current_page.is_none() {
555            return Err(FormFieldError::NoActivePage);
556        }
557        if self.form_field_names.contains(name) {
558            return Err(FormFieldError::DuplicateName(name.to_string()));
559        }
560        let pdf_rect = self.transform_rect(&rect);
561        self.form_field_names.insert(name.to_string());
562        let page = self.current_page.as_mut().unwrap();
563        page.fields.push(FormFieldDef {
564            name: name.to_string(),
565            rect: pdf_rect,
566        });
567        Ok(())
568    }
569
570    /// Pre-allocate ObjIds for an image if not yet done.
571    fn ensure_image_obj_ids(&mut self, idx: usize) {
572        if self.image_obj_ids.contains_key(&idx) {
573            return;
574        }
575        let xobject = ObjId(self.next_obj_num, 0);
576        self.next_obj_num += 1;
577
578        let smask = if self.images[idx].smask_data.is_some() {
579            let id = ObjId(self.next_obj_num, 0);
580            self.next_obj_num += 1;
581            Some(id)
582        } else {
583            None
584        };
585
586        let pdf_name = format!("Im{}", self.next_image_num);
587        self.next_image_num += 1;
588
589        self.image_obj_ids.insert(
590            idx,
591            ImageObjIds {
592                xobject,
593                smask,
594                pdf_name,
595            },
596        );
597    }
598
599    /// Write the image XObject stream(s) for the given image index.
600    fn write_image_xobject(&mut self, idx: usize) -> io::Result<()> {
601        if self.written_images.contains(&idx) {
602            return Ok(());
603        }
604
605        let img = &self.images[idx];
606        let obj_ids = &self.image_obj_ids[&idx];
607        let xobject_id = obj_ids.xobject;
608        let smask_id = obj_ids.smask;
609
610        // Write SMask XObject first if alpha data exists
611        if let (Some(smask_obj_id), Some(smask_data)) = (smask_id, img.smask_data.as_ref()) {
612            let smask_stream = self.make_stream(
613                vec![
614                    ("Type", PdfObject::name("XObject")),
615                    ("Subtype", PdfObject::name("Image")),
616                    ("Width", PdfObject::Integer(img.width as i64)),
617                    ("Height", PdfObject::Integer(img.height as i64)),
618                    ("ColorSpace", PdfObject::name("DeviceGray")),
619                    ("BitsPerComponent", PdfObject::Integer(8)),
620                ],
621                smask_data.clone(),
622            );
623            self.writer.write_object(smask_obj_id, &smask_stream)?;
624        }
625
626        // Build image XObject dict entries
627        let mut entries: Vec<(&str, PdfObject)> = vec![
628            ("Type", PdfObject::name("XObject")),
629            ("Subtype", PdfObject::name("Image")),
630            ("Width", PdfObject::Integer(img.width as i64)),
631            ("Height", PdfObject::Integer(img.height as i64)),
632            ("ColorSpace", PdfObject::name(img.color_space.pdf_name())),
633            (
634                "BitsPerComponent",
635                PdfObject::Integer(img.bits_per_component as i64),
636            ),
637        ];
638
639        if let Some(smask_obj_id) = smask_id {
640            entries.push(("SMask", PdfObject::Reference(smask_obj_id)));
641        }
642
643        // For JPEG: embed raw data with DCTDecode, never double-compress
644        // For PNG (decoded pixels): use make_stream for optional FlateDecode
645        let image_obj = match img.format {
646            ImageFormat::Jpeg => {
647                entries.push(("Filter", PdfObject::name("DCTDecode")));
648                PdfObject::stream(entries, img.data.clone())
649            }
650            ImageFormat::Png => self.make_stream(entries, img.data.clone()),
651        };
652
653        self.writer.write_object(xobject_id, &image_obj)?;
654        self.written_images.insert(idx);
655        Ok(())
656    }
657
658    // -------------------------------------------------------
659    // Graphics operations
660    // -------------------------------------------------------
661
662    /// Set the stroke color (PDF `RG` operator).
663    pub fn set_stroke_color(&mut self, color: Color) -> &mut Self {
664        let page = self
665            .current_page
666            .as_mut()
667            .expect("set_stroke_color called with no open page");
668        let ops = format!(
669            "{} {} {} RG\n",
670            format_coord(color.r),
671            format_coord(color.g),
672            format_coord(color.b),
673        );
674        page.content_ops.extend_from_slice(ops.as_bytes());
675        self
676    }
677
678    /// Set the fill color (PDF `rg` operator).
679    pub fn set_fill_color(&mut self, color: Color) -> &mut Self {
680        let page = self
681            .current_page
682            .as_mut()
683            .expect("set_fill_color called with no open page");
684        let ops = format!(
685            "{} {} {} rg\n",
686            format_coord(color.r),
687            format_coord(color.g),
688            format_coord(color.b),
689        );
690        page.content_ops.extend_from_slice(ops.as_bytes());
691        self
692    }
693
694    /// Set the line width (PDF `w` operator).
695    pub fn set_line_width(&mut self, width: f64) -> &mut Self {
696        let page = self
697            .current_page
698            .as_mut()
699            .expect("set_line_width called with no open page");
700        let ops = format!("{} w\n", format_coord(width));
701        page.content_ops.extend_from_slice(ops.as_bytes());
702        self
703    }
704
705    /// Move to a point without drawing (PDF `m` operator).
706    ///
707    /// With [`Origin::TopLeft`], `y` is measured from the top of the page,
708    /// increasing downward.
709    pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
710        let y_pdf = self.transform_y(y);
711        let page = self
712            .current_page
713            .as_mut()
714            .expect("move_to called with no open page");
715        let ops = format!("{} {} m\n", format_coord(x), format_coord(y_pdf));
716        page.content_ops.extend_from_slice(ops.as_bytes());
717        self
718    }
719
720    /// Draw a line to a point (PDF `l` operator).
721    ///
722    /// With [`Origin::TopLeft`], `y` is measured from the top of the page,
723    /// increasing downward.
724    pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
725        let y_pdf = self.transform_y(y);
726        let page = self
727            .current_page
728            .as_mut()
729            .expect("line_to called with no open page");
730        let ops = format!("{} {} l\n", format_coord(x), format_coord(y_pdf));
731        page.content_ops.extend_from_slice(ops.as_bytes());
732        self
733    }
734
735    /// Append a rectangle to the path (PDF `re` operator).
736    ///
737    /// With [`Origin::TopLeft`], `(x, y)` is the **top-left** corner of the
738    /// rectangle and `height` extends downward.
739    pub fn rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
740        let r = self.transform_rect(&Rect {
741            x,
742            y,
743            width,
744            height,
745        });
746        let page = self
747            .current_page
748            .as_mut()
749            .expect("rect called with no open page");
750        let ops = format!(
751            "{} {} {} {} re\n",
752            format_coord(r.x),
753            format_coord(r.y),
754            format_coord(r.width),
755            format_coord(r.height),
756        );
757        page.content_ops.extend_from_slice(ops.as_bytes());
758        self
759    }
760
761    /// Append a cubic Bezier curve to the path (PDF `c` operator).
762    ///
763    /// `(x1, y1)` and `(x2, y2)` are the two control points; `(x3, y3)` is
764    /// the endpoint. All y-coordinates are transformed according to the
765    /// document's origin setting.
766    pub fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> &mut Self {
767        let y1_pdf = self.transform_y(y1);
768        let y2_pdf = self.transform_y(y2);
769        let y3_pdf = self.transform_y(y3);
770        let page = self
771            .current_page
772            .as_mut()
773            .expect("curve_to called with no open page");
774        let ops = format!(
775            "{} {} {} {} {} {} c\n",
776            format_coord(x1),
777            format_coord(y1_pdf),
778            format_coord(x2),
779            format_coord(y2_pdf),
780            format_coord(x3),
781            format_coord(y3_pdf),
782        );
783        page.content_ops.extend_from_slice(ops.as_bytes());
784        self
785    }
786
787    /// Append an arc to the current path.
788    ///
789    /// The arc is centered at `(cx, cy)` with the given `radius`. Angles follow
790    /// standard math convention: 0° = right, counter-clockwise positive.
791    /// Use [`Angle::degrees`] or [`Angle::radians`] to construct the angle.
792    ///
793    /// The arc is approximated with cubic Bezier segments (up to one per 90°).
794    /// This method moves to the arc's start point; the caller is responsible for
795    /// painting (stroke/fill/fill_stroke).
796    ///
797    /// With [`Origin::TopLeft`], `(cx, cy)` is measured from the top of the page.
798    pub fn arc(&mut self, cx: f64, cy: f64, radius: f64, start: Angle, end: Angle) -> &mut Self {
799        let start_rad = start.to_radians();
800        let end_rad = end.to_radians();
801
802        // Start point in user space
803        let sx = cx + radius * start_rad.cos();
804        let sy = cy + radius * start_rad.sin();
805        self.move_to(sx, sy);
806
807        for (x1, y1, x2, y2, x3, y3) in arc_bezier_segments(cx, cy, radius, start_rad, end_rad) {
808            self.curve_to(x1, y1, x2, y2, x3, y3);
809        }
810        self
811    }
812
813    /// Append a full circle to the current path (closed).
814    ///
815    /// The circle is centered at `(cx, cy)` with the given `radius`. The path
816    /// is automatically closed with the `h` operator; the caller is responsible
817    /// for painting (stroke/fill/fill_stroke).
818    ///
819    /// With [`Origin::TopLeft`], `(cx, cy)` is measured from the top of the page.
820    pub fn circle(&mut self, cx: f64, cy: f64, radius: f64) -> &mut Self {
821        self.arc(
822            cx,
823            cy,
824            radius,
825            Angle::radians(0.0),
826            Angle::radians(std::f64::consts::TAU),
827        )
828        .close_path()
829    }
830
831    /// Close the current subpath (PDF `h` operator).
832    pub fn close_path(&mut self) -> &mut Self {
833        let page = self
834            .current_page
835            .as_mut()
836            .expect("close_path called with no open page");
837        page.content_ops.extend_from_slice(b"h\n");
838        self
839    }
840
841    /// Stroke the current path (PDF `S` operator).
842    pub fn stroke(&mut self) -> &mut Self {
843        let page = self
844            .current_page
845            .as_mut()
846            .expect("stroke called with no open page");
847        page.content_ops.extend_from_slice(b"S\n");
848        self
849    }
850
851    /// Fill the current path (PDF `f` operator).
852    pub fn fill(&mut self) -> &mut Self {
853        let page = self
854            .current_page
855            .as_mut()
856            .expect("fill called with no open page");
857        page.content_ops.extend_from_slice(b"f\n");
858        self
859    }
860
861    /// Fill and stroke the current path (PDF `B` operator).
862    pub fn fill_stroke(&mut self) -> &mut Self {
863        let page = self
864            .current_page
865            .as_mut()
866            .expect("fill_stroke called with no open page");
867        page.content_ops.extend_from_slice(b"B\n");
868        self
869    }
870
871    /// Save the graphics state (PDF `q` operator).
872    pub fn save_state(&mut self) -> &mut Self {
873        let page = self
874            .current_page
875            .as_mut()
876            .expect("save_state called with no open page");
877        page.content_ops.extend_from_slice(b"q\n");
878        self
879    }
880
881    /// Restore the graphics state (PDF `Q` operator).
882    pub fn restore_state(&mut self) -> &mut Self {
883        let page = self
884            .current_page
885            .as_mut()
886            .expect("restore_state called with no open page");
887        page.content_ops.extend_from_slice(b"Q\n");
888        self
889    }
890
891    // -------------------------------------------------------
892    // Coordinate transform helpers
893    // -------------------------------------------------------
894
895    /// Transform a user y-coordinate to PDF space.
896    /// With `TopLeft`, flips y: `page_height - y`.
897    /// With `BottomLeft`, returns y unchanged.
898    fn transform_y(&self, y: f64) -> f64 {
899        match self.origin {
900            Origin::BottomLeft => y,
901            Origin::TopLeft => {
902                let page_height = self
903                    .current_page
904                    .as_ref()
905                    .expect("transform_y called with no open page")
906                    .height;
907                page_height - y
908            }
909        }
910    }
911
912    /// Transform a user-space rect to PDF bottom-left space.
913    ///
914    /// With `TopLeft`, `(x, y)` is the top-left corner; transforms to
915    /// PDF bottom-left: `y_pdf_bottom = page_height - y_user - height`.
916    ///
917    /// With `BottomLeft`, returns the rect unchanged (`y` is already the
918    /// bottom edge in PDF space).
919    fn transform_rect(&self, rect: &Rect) -> Rect {
920        match self.origin {
921            Origin::BottomLeft => *rect,
922            Origin::TopLeft => {
923                let page_height = self
924                    .current_page
925                    .as_ref()
926                    .expect("transform_rect called with no open page")
927                    .height;
928                Rect {
929                    x: rect.x,
930                    y: page_height - rect.y - rect.height,
931                    width: rect.width,
932                    height: rect.height,
933                }
934            }
935        }
936    }
937
938    /// Transform a user-space rect to the "top-edge-in-PDF-space" format
939    /// used by the text layout and table engines (where `y` = top edge,
940    /// decreasing downward).
941    ///
942    /// With `TopLeft`, `y_top_pdf = page_height - y_user`.
943    /// With `BottomLeft`, returns the rect unchanged (y is already top-edge
944    /// in PDF space for layout engines).
945    fn transform_rect_top_edge(&self, rect: &Rect) -> Rect {
946        match self.origin {
947            Origin::BottomLeft => *rect,
948            Origin::TopLeft => {
949                let page_height = self
950                    .current_page
951                    .as_ref()
952                    .expect("transform_rect_top_edge called with no open page")
953                    .height;
954                Rect {
955                    x: rect.x,
956                    y: page_height - rect.y,
957                    width: rect.width,
958                    height: rect.height,
959                }
960            }
961        }
962    }
963
964    /// Current page height, panics if no page is open.
965    fn current_page_height(&self) -> f64 {
966        self.current_page.as_ref().expect("no open page").height
967    }
968
969    /// Build a stream object, optionally compressing the data with FlateDecode.
970    fn make_stream(&self, mut dict_entries: Vec<(&str, PdfObject)>, data: Vec<u8>) -> PdfObject {
971        if self.compress {
972            let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
973            encoder.write_all(&data).expect("flate2 in-memory write");
974            let compressed = encoder.finish().expect("flate2 finish");
975            dict_entries.push(("Filter", PdfObject::name("FlateDecode")));
976            PdfObject::stream(dict_entries, compressed)
977        } else {
978            PdfObject::stream(dict_entries, data)
979        }
980    }
981
982    /// Ensure a builtin font's dictionary object has been written.
983    fn ensure_font_written(&mut self, font: BuiltinFont) -> io::Result<ObjId> {
984        if let Some(&id) = self.font_obj_ids.get(&font) {
985            return Ok(id);
986        }
987        let id = ObjId(self.next_obj_num, 0);
988        self.next_obj_num += 1;
989        let obj = PdfObject::dict(vec![
990            ("Type", PdfObject::name("Font")),
991            ("Subtype", PdfObject::name("Type1")),
992            ("BaseFont", PdfObject::name(font.pdf_base_name())),
993        ]);
994        self.writer.write_object(id, &obj)?;
995        self.font_obj_ids.insert(font, id);
996        Ok(id)
997    }
998
999    /// Pre-allocate ObjIds for a TrueType font if not yet done.
1000    fn ensure_tt_font_obj_ids(&mut self, idx: usize) -> &TrueTypeFontObjIds {
1001        if !self.truetype_font_obj_ids.contains_key(&idx) {
1002            let type0 = ObjId(self.next_obj_num, 0);
1003            self.next_obj_num += 1;
1004            let cid_font = ObjId(self.next_obj_num, 0);
1005            self.next_obj_num += 1;
1006            let descriptor = ObjId(self.next_obj_num, 0);
1007            self.next_obj_num += 1;
1008            let font_file = ObjId(self.next_obj_num, 0);
1009            self.next_obj_num += 1;
1010            let tounicode = ObjId(self.next_obj_num, 0);
1011            self.next_obj_num += 1;
1012            self.truetype_font_obj_ids.insert(
1013                idx,
1014                TrueTypeFontObjIds {
1015                    type0,
1016                    cid_font,
1017                    descriptor,
1018                    font_file,
1019                    tounicode,
1020                },
1021            );
1022        }
1023        &self.truetype_font_obj_ids[&idx]
1024    }
1025
1026    /// End the current page. Writes the content stream to the writer
1027    /// and frees page content from memory. The page dictionary is
1028    /// deferred until `end_document()` so overlay streams can be added.
1029    pub fn end_page(&mut self) -> io::Result<()> {
1030        let page = self
1031            .current_page
1032            .take()
1033            .expect("end_page called with no open page");
1034
1035        // Write builtin font objects for any not yet written
1036        for &font in &page.used_fonts {
1037            self.ensure_font_written(font)?;
1038        }
1039
1040        // Pre-allocate ObjIds for TrueType fonts used on this page
1041        for &idx in &page.used_truetype_fonts {
1042            self.ensure_tt_font_obj_ids(idx);
1043        }
1044
1045        // Write image XObjects for images used on this page
1046        let used_images: Vec<usize> = page.used_images.iter().copied().collect();
1047        for idx in &used_images {
1048            self.write_image_xobject(*idx)?;
1049        }
1050
1051        let content_id = ObjId(self.next_obj_num, 0);
1052        self.next_obj_num += 1;
1053
1054        // Write content stream immediately (keeps memory usage low)
1055        let content_stream = self.make_stream(vec![], page.content_ops);
1056        self.writer.write_object(content_id, &content_stream)?;
1057
1058        // Pre-allocate ObjIds for form fields on this page
1059        let field_records: Vec<FormFieldRecord> = page
1060            .fields
1061            .into_iter()
1062            .map(|def| {
1063                let obj_id = ObjId(self.next_obj_num, 0);
1064                self.next_obj_num += 1;
1065                FormFieldRecord {
1066                    name: def.name,
1067                    rect: def.rect,
1068                    obj_id,
1069                }
1070            })
1071            .collect();
1072
1073        match page.overlay_for {
1074            None => {
1075                // New page: pre-allocate the page dict ObjId and store the record.
1076                // The page dictionary itself is written in write_page_dicts().
1077                let page_id = ObjId(self.next_obj_num, 0);
1078                self.next_obj_num += 1;
1079
1080                self.page_records.push(PageRecord {
1081                    obj_id: page_id,
1082                    content_ids: vec![content_id],
1083                    width: page.width,
1084                    height: page.height,
1085                    used_fonts: page.used_fonts,
1086                    used_truetype_fonts: page.used_truetype_fonts,
1087                    used_images: page.used_images,
1088                    fields: field_records,
1089                });
1090            }
1091            Some(idx) => {
1092                // Overlay: append content stream to existing page record.
1093                let record = &mut self.page_records[idx];
1094                record.content_ids.push(content_id);
1095                record.used_fonts.extend(page.used_fonts);
1096                record.used_truetype_fonts.extend(page.used_truetype_fonts);
1097                record.used_images.extend(page.used_images);
1098                // Overlays don't support adding new fields; field_records is empty for overlays.
1099            }
1100        }
1101
1102        Ok(())
1103    }
1104
1105    /// Build the font resource dictionary for a page.
1106    fn build_font_dict(&self, used_fonts: &[BuiltinFont], used_truetype: &[usize]) -> PdfObject {
1107        let mut entries: Vec<(String, PdfObject)> = used_fonts
1108            .iter()
1109            .map(|f| {
1110                (
1111                    f.pdf_name().to_string(),
1112                    PdfObject::Reference(self.font_obj_ids[f]),
1113                )
1114            })
1115            .collect();
1116
1117        for &idx in used_truetype {
1118            let name = self.truetype_fonts[idx].pdf_name.clone();
1119            let type0_id = self.truetype_font_obj_ids[&idx].type0;
1120            entries.push((name, PdfObject::Reference(type0_id)));
1121        }
1122
1123        PdfObject::Dictionary(entries)
1124    }
1125
1126    /// Build the resource dictionary for a page.
1127    fn build_resource_dict(
1128        &self,
1129        used_fonts: &[BuiltinFont],
1130        used_truetype: &[usize],
1131        used_images: &[usize],
1132    ) -> PdfObject {
1133        let font_dict = self.build_font_dict(used_fonts, used_truetype);
1134
1135        let xobject_entries: Vec<(String, PdfObject)> = used_images
1136            .iter()
1137            .filter_map(|idx| {
1138                self.image_obj_ids
1139                    .get(idx)
1140                    .map(|ids| (ids.pdf_name.clone(), PdfObject::Reference(ids.xobject)))
1141            })
1142            .collect();
1143
1144        let mut resource_entries: Vec<(String, PdfObject)> = vec![("Font".to_string(), font_dict)];
1145        if !xobject_entries.is_empty() {
1146            resource_entries.push((
1147                "XObject".to_string(),
1148                PdfObject::Dictionary(xobject_entries),
1149            ));
1150        }
1151
1152        PdfObject::Dictionary(resource_entries)
1153    }
1154
1155    /// Build the `/Contents` entry: single reference for one stream, array for multiple.
1156    fn build_contents(content_ids: &[ObjId]) -> PdfObject {
1157        if content_ids.len() == 1 {
1158            PdfObject::Reference(content_ids[0])
1159        } else {
1160            PdfObject::array(
1161                content_ids
1162                    .iter()
1163                    .map(|id| PdfObject::Reference(*id))
1164                    .collect(),
1165            )
1166        }
1167    }
1168
1169    /// Write widget annotation objects for all form fields in a page.
1170    fn write_widget_annotations(&mut self, page_idx: usize) -> io::Result<()> {
1171        let page_obj_id = self.page_records[page_idx].obj_id;
1172        let field_ids: Vec<(String, Rect, ObjId)> = self.page_records[page_idx]
1173            .fields
1174            .iter()
1175            .map(|f| (f.name.clone(), f.rect, f.obj_id))
1176            .collect();
1177
1178        for (name, rect, obj_id) in field_ids {
1179            // Widget annotation: /Rect is [x_ll y_ll x_ur y_ur] in PDF coordinates
1180            let rect_array = PdfObject::array(vec![
1181                PdfObject::Real(rect.x),
1182                PdfObject::Real(rect.y),
1183                PdfObject::Real(rect.x + rect.width),
1184                PdfObject::Real(rect.y + rect.height),
1185            ]);
1186            let widget = PdfObject::dict(vec![
1187                ("Type", PdfObject::name("Annot")),
1188                ("Subtype", PdfObject::name("Widget")),
1189                ("FT", PdfObject::name("Tx")),
1190                ("T", PdfObject::literal_string(&name)),
1191                ("Rect", rect_array),
1192                ("P", PdfObject::Reference(page_obj_id)),
1193                ("F", PdfObject::Integer(4)), // Print flag
1194            ]);
1195            self.writer.write_object(obj_id, &widget)?;
1196        }
1197        Ok(())
1198    }
1199
1200    /// Write page dictionaries for all pages. Called from `end_document()`
1201    /// after all content streams (including overlays) have been written.
1202    fn write_page_dicts(&mut self) -> io::Result<()> {
1203        for i in 0..self.page_records.len() {
1204            self.write_widget_annotations(i)?;
1205
1206            // Copy out page data to release the borrow before writing
1207            let obj_id = self.page_records[i].obj_id;
1208            let content_ids: Vec<ObjId> =
1209                self.page_records[i].content_ids.iter().copied().collect();
1210            let width = self.page_records[i].width;
1211            let height = self.page_records[i].height;
1212            let used_fonts: Vec<BuiltinFont> =
1213                self.page_records[i].used_fonts.iter().copied().collect();
1214            let used_truetype: Vec<usize> = self.page_records[i]
1215                .used_truetype_fonts
1216                .iter()
1217                .copied()
1218                .collect();
1219            let used_images: Vec<usize> =
1220                self.page_records[i].used_images.iter().copied().collect();
1221            let annot_ids: Vec<ObjId> = self.page_records[i]
1222                .fields
1223                .iter()
1224                .map(|f| f.obj_id)
1225                .collect();
1226
1227            let resources = self.build_resource_dict(&used_fonts, &used_truetype, &used_images);
1228            let contents = Self::build_contents(&content_ids);
1229
1230            let mut page_entries = vec![
1231                ("Type", PdfObject::name("Page")),
1232                ("Parent", PdfObject::Reference(PAGES_OBJ)),
1233                (
1234                    "MediaBox",
1235                    PdfObject::array(vec![
1236                        PdfObject::Integer(0),
1237                        PdfObject::Integer(0),
1238                        PdfObject::Real(width),
1239                        PdfObject::Real(height),
1240                    ]),
1241                ),
1242                ("Contents", contents),
1243                ("Resources", resources),
1244            ];
1245
1246            if !annot_ids.is_empty() {
1247                let annots = PdfObject::array(
1248                    annot_ids
1249                        .iter()
1250                        .map(|id| PdfObject::Reference(*id))
1251                        .collect(),
1252                );
1253                page_entries.push(("Annots", annots));
1254            }
1255
1256            let page_dict = PdfObject::dict(page_entries);
1257            self.writer.write_object(obj_id, &page_dict)?;
1258        }
1259        Ok(())
1260    }
1261
1262    /// Collect all form field ObjIds across all pages.
1263    fn collect_all_field_ids(&self) -> Vec<ObjId> {
1264        self.page_records
1265            .iter()
1266            .flat_map(|r| r.fields.iter().map(|f| f.obj_id))
1267            .collect()
1268    }
1269
1270    /// Write the /AcroForm dictionary in the catalog if any fields exist.
1271    /// Returns the ObjId of the AcroForm dict, or None if no fields.
1272    fn write_acroform(&mut self) -> io::Result<Option<ObjId>> {
1273        let all_field_ids = self.collect_all_field_ids();
1274        if all_field_ids.is_empty() {
1275            return Ok(None);
1276        }
1277
1278        let fields_array = PdfObject::array(
1279            all_field_ids
1280                .iter()
1281                .map(|id| PdfObject::Reference(*id))
1282                .collect(),
1283        );
1284
1285        let acroform_id = ObjId(self.next_obj_num, 0);
1286        self.next_obj_num += 1;
1287
1288        let acroform = PdfObject::dict(vec![
1289            ("Fields", fields_array),
1290            ("NeedAppearances", PdfObject::Boolean(true)),
1291            // Default appearance: Helvetica 12pt black
1292            ("DA", PdfObject::literal_string("/Helv 12 Tf 0 g")),
1293        ]);
1294        self.writer.write_object(acroform_id, &acroform)?;
1295        Ok(Some(acroform_id))
1296    }
1297
1298    /// Write all TrueType font objects. Called during
1299    /// end_document, after all pages have been written.
1300    fn write_truetype_fonts(&mut self) -> io::Result<()> {
1301        let indices: Vec<usize> = self.truetype_font_obj_ids.keys().copied().collect();
1302
1303        for idx in indices {
1304            let obj_ids_type0 = self.truetype_font_obj_ids[&idx].type0;
1305            let obj_ids_cid = self.truetype_font_obj_ids[&idx].cid_font;
1306            let obj_ids_desc = self.truetype_font_obj_ids[&idx].descriptor;
1307            let obj_ids_file = self.truetype_font_obj_ids[&idx].font_file;
1308            let obj_ids_tounicode = self.truetype_font_obj_ids[&idx].tounicode;
1309
1310            let font = &self.truetype_fonts[idx];
1311
1312            // 1. FontFile2 stream (raw .ttf data)
1313            let original_len = font.font_data.len() as i64;
1314            let font_file_stream = self.make_stream(
1315                vec![("Length1", PdfObject::Integer(original_len))],
1316                font.font_data.clone(),
1317            );
1318            self.writer.write_object(obj_ids_file, &font_file_stream)?;
1319
1320            // 2. FontDescriptor (values scaled to PDF units: 1/1000)
1321            let descriptor = PdfObject::dict(vec![
1322                ("Type", PdfObject::name("FontDescriptor")),
1323                ("FontName", PdfObject::name(&font.postscript_name)),
1324                ("Flags", PdfObject::Integer(font.flags as i64)),
1325                (
1326                    "FontBBox",
1327                    PdfObject::array(vec![
1328                        PdfObject::Integer(font.scale_to_pdf(font.bbox[0])),
1329                        PdfObject::Integer(font.scale_to_pdf(font.bbox[1])),
1330                        PdfObject::Integer(font.scale_to_pdf(font.bbox[2])),
1331                        PdfObject::Integer(font.scale_to_pdf(font.bbox[3])),
1332                    ]),
1333                ),
1334                ("ItalicAngle", PdfObject::Real(font.italic_angle)),
1335                ("Ascent", PdfObject::Integer(font.scale_to_pdf(font.ascent))),
1336                (
1337                    "Descent",
1338                    PdfObject::Integer(font.scale_to_pdf(font.descent)),
1339                ),
1340                (
1341                    "CapHeight",
1342                    PdfObject::Integer(font.scale_to_pdf(font.cap_height)),
1343                ),
1344                ("StemV", PdfObject::Integer(font.scale_to_pdf(font.stem_v))),
1345                ("FontFile2", PdfObject::Reference(obj_ids_file)),
1346            ]);
1347            self.writer.write_object(obj_ids_desc, &descriptor)?;
1348
1349            // 3. CIDFontType2
1350            let w_array = font.build_w_array();
1351            let cid_font = PdfObject::dict(vec![
1352                ("Type", PdfObject::name("Font")),
1353                ("Subtype", PdfObject::name("CIDFontType2")),
1354                ("BaseFont", PdfObject::name(&font.postscript_name)),
1355                (
1356                    "CIDSystemInfo",
1357                    PdfObject::dict(vec![
1358                        ("Registry", PdfObject::literal_string("Adobe")),
1359                        ("Ordering", PdfObject::literal_string("Identity")),
1360                        ("Supplement", PdfObject::Integer(0)),
1361                    ]),
1362                ),
1363                ("FontDescriptor", PdfObject::Reference(obj_ids_desc)),
1364                ("DW", PdfObject::Integer(font.default_width_pdf())),
1365                ("W", PdfObject::Array(w_array)),
1366            ]);
1367            self.writer.write_object(obj_ids_cid, &cid_font)?;
1368
1369            // 4. ToUnicode CMap stream
1370            let tounicode_data = font.build_tounicode_cmap();
1371            let tounicode = self.make_stream(vec![], tounicode_data);
1372            self.writer.write_object(obj_ids_tounicode, &tounicode)?;
1373
1374            // 5. Type0 font (top-level)
1375            let type0 = PdfObject::dict(vec![
1376                ("Type", PdfObject::name("Font")),
1377                ("Subtype", PdfObject::name("Type0")),
1378                ("BaseFont", PdfObject::name(&font.postscript_name)),
1379                ("Encoding", PdfObject::name("Identity-H")),
1380                (
1381                    "DescendantFonts",
1382                    PdfObject::array(vec![PdfObject::Reference(obj_ids_cid)]),
1383                ),
1384                ("ToUnicode", PdfObject::Reference(obj_ids_tounicode)),
1385            ]);
1386            self.writer.write_object(obj_ids_type0, &type0)?;
1387        }
1388
1389        Ok(())
1390    }
1391
1392    /// Finish the document. Writes page dictionaries, the catalog, pages tree,
1393    /// info dictionary, xref table, and trailer.
1394    /// Consumes self -- no further operations are possible.
1395    pub fn end_document(mut self) -> io::Result<W> {
1396        // Auto-close any open page
1397        if self.current_page.is_some() {
1398            self.end_page()?;
1399        }
1400
1401        // Write page dictionaries (deferred so overlays can be accumulated first)
1402        self.write_page_dicts()?;
1403
1404        // Write TrueType font objects (deferred until now)
1405        self.write_truetype_fonts()?;
1406
1407        // Write AcroForm if any form fields exist
1408        let acroform_id = self.write_acroform()?;
1409
1410        // Write info dictionary if any entries exist
1411        let info_id = if !self.info.is_empty() {
1412            let id = ObjId(self.next_obj_num, 0);
1413            self.next_obj_num += 1;
1414            let entries: Vec<(&str, PdfObject)> = self
1415                .info
1416                .iter()
1417                .map(|(k, v)| (k.as_str(), PdfObject::literal_string(v)))
1418                .collect();
1419            let info_obj = PdfObject::dict(entries);
1420            self.writer.write_object(id, &info_obj)?;
1421            Some(id)
1422        } else {
1423            None
1424        };
1425
1426        // Write pages tree (obj 2)
1427        let kids: Vec<PdfObject> = self
1428            .page_records
1429            .iter()
1430            .map(|r| PdfObject::Reference(r.obj_id))
1431            .collect();
1432        let page_count = self.page_records.len() as i64;
1433        let pages = PdfObject::dict(vec![
1434            ("Type", PdfObject::name("Pages")),
1435            ("Kids", PdfObject::Array(kids)),
1436            ("Count", PdfObject::Integer(page_count)),
1437        ]);
1438        self.writer.write_object(PAGES_OBJ, &pages)?;
1439
1440        // Write catalog (obj 1)
1441        let mut catalog_entries = vec![
1442            ("Type", PdfObject::name("Catalog")),
1443            ("Pages", PdfObject::Reference(PAGES_OBJ)),
1444        ];
1445        if let Some(acroform) = acroform_id {
1446            catalog_entries.push(("AcroForm", PdfObject::Reference(acroform)));
1447        }
1448        let catalog = PdfObject::dict(catalog_entries);
1449        self.writer.write_object(CATALOG_OBJ, &catalog)?;
1450
1451        // Write xref and trailer
1452        self.writer.write_xref_and_trailer(CATALOG_OBJ, info_id)?;
1453
1454        Ok(self.writer.into_inner())
1455    }
1456}
1457
1458/// Decompose an arc into cubic Bezier segments (up to one per 90°).
1459///
1460/// Given center `(cx, cy)`, `radius`, and start/end angles in radians
1461/// (standard math convention: 0 = right, CCW positive), returns a sequence of
1462/// `(x1, y1, x2, y2, x3, y3)` control-point tuples in user space.
1463///
1464/// The magic constant `k = 4/3 * tan(α/4)` approximates the arc to within
1465/// 0.027% for a 90° segment (see PDF 32000-1:2008, §8.5.2).
1466fn arc_bezier_segments(
1467    cx: f64,
1468    cy: f64,
1469    radius: f64,
1470    start_rad: f64,
1471    end_rad: f64,
1472) -> Vec<(f64, f64, f64, f64, f64, f64)> {
1473    const MAX_SEGMENT: f64 = std::f64::consts::FRAC_PI_2; // 90°
1474
1475    let mut segments = Vec::new();
1476    let total = end_rad - start_rad;
1477    let n = (total.abs() / MAX_SEGMENT).ceil().max(1.0) as u32;
1478    let step = total / n as f64;
1479
1480    for i in 0..n {
1481        let a = start_rad + i as f64 * step;
1482        let b = a + step;
1483        let k = 4.0 / 3.0 * ((b - a) / 4.0).tan();
1484
1485        let (cos_a, sin_a) = (a.cos(), a.sin());
1486        let (cos_b, sin_b) = (b.cos(), b.sin());
1487
1488        let x1 = cx + radius * (cos_a - k * sin_a);
1489        let y1 = cy + radius * (sin_a + k * cos_a);
1490        let x2 = cx + radius * (cos_b + k * sin_b);
1491        let y2 = cy + radius * (sin_b - k * cos_b);
1492        let x3 = cx + radius * cos_b;
1493        let y3 = cy + radius * sin_b;
1494
1495        segments.push((x1, y1, x2, y2, x3, y3));
1496    }
1497    segments
1498}
1499
1500/// Format a coordinate value for PDF content streams.
1501pub(crate) fn format_coord(v: f64) -> String {
1502    if v == v.floor() && v.abs() < 1e15 {
1503        format!("{}", v as i64)
1504    } else {
1505        let s = format!("{:.4}", v);
1506        let s = s.trim_end_matches('0');
1507        let s = s.trim_end_matches('.');
1508        s.to_string()
1509    }
1510}