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