Skip to main content

fop_render/pdf/document/
page.rs

1//! PDF page operations
2//!
3//! Implements rendering operations on individual PDF pages.
4
5use fop_types::Length;
6
7use crate::pdf::font::FontManager;
8
9use super::types::{LinkAnnotation, LinkDestination, PdfPage};
10
11/// Build PDF character spacing (Tc) and word spacing (Tw) operator strings.
12///
13/// Returns a string with the appropriate operators for inclusion in a BT...ET block.
14/// Non-zero spacing values produce "Tc" or "Tw" lines; zero values are omitted.
15pub(super) fn build_spacing_ops(
16    letter_spacing: Option<Length>,
17    word_spacing: Option<Length>,
18) -> String {
19    let mut ops = String::new();
20    if let Some(ls) = letter_spacing {
21        let pt = ls.to_pt();
22        if pt.abs() > 0.0001 {
23            ops.push_str(&format!(
24                "{:.4} Tc
25",
26                pt
27            ));
28        }
29    }
30    if let Some(ws) = word_spacing {
31        let pt = ws.to_pt();
32        if pt.abs() > 0.0001 {
33            ops.push_str(&format!(
34                "{:.4} Tw
35",
36                pt
37            ));
38        }
39    }
40    ops
41}
42
43impl PdfPage {
44    /// Create a new PDF page
45    pub fn new(width: Length, height: Length) -> Self {
46        Self {
47            width,
48            height,
49            content: Vec::new(),
50            link_annotations: Vec::new(),
51        }
52    }
53
54    /// Add a link annotation to the page
55    ///
56    /// # Arguments
57    /// * `x` - X position (PDF coordinates: bottom-left origin)
58    /// * `y` - Y position (PDF coordinates: bottom-left origin)
59    /// * `width` - Width of the clickable area
60    /// * `height` - Height of the clickable area
61    /// * `destination` - Link destination (external URL or internal ID)
62    pub fn add_link_annotation(
63        &mut self,
64        x: Length,
65        y: Length,
66        width: Length,
67        height: Length,
68        destination: LinkDestination,
69    ) {
70        let rect = [
71            x.to_pt(),
72            y.to_pt(),
73            (x + width).to_pt(),
74            (y + height).to_pt(),
75        ];
76        self.link_annotations
77            .push(LinkAnnotation { rect, destination });
78    }
79
80    /// Encode text for PDF output using UTF-16BE for CID fonts
81    /// Uses UTF-16BE hex strings WITHOUT BOM (matches Java FOP StandardCharsets.UTF_16BE)
82    fn encode_pdf_text(text: &str) -> String {
83        // For CID fonts (Type 0), we use UTF-16BE encoding
84        // Java FOP: text.getBytes(StandardCharsets.UTF_16BE) - NO BOM!
85        // BOM should only be in ToUnicode CMap, not in content streams
86        let mut result = String::from("<");
87
88        // Encode each character as UTF-16BE (without BOM)
89        for c in text.chars() {
90            let code = c as u32;
91            if code <= 0xFFFF {
92                // BMP character (Basic Multilingual Plane)
93                result.push_str(&format!("{:04X}", code));
94            } else {
95                // Surrogate pair for non-BMP characters (above U+FFFF)
96                let code = code - 0x10000;
97                let high = 0xD800 + (code >> 10);
98                let low = 0xDC00 + (code & 0x3FF);
99                result.push_str(&format!("{:04X}{:04X}", high, low));
100            }
101        }
102
103        result.push('>');
104        result
105    }
106
107    /// Add text to the page using the default Helvetica font (F1)
108    pub fn add_text(&mut self, text: &str, x: Length, y: Length, font_size: Length) {
109        self.add_text_with_spacing(text, x, y, font_size, None, None);
110    }
111
112    /// Add text to the page using the default Helvetica font (F1) with optional letter/word spacing
113    ///
114    /// # Arguments
115    /// * `letter_spacing` - Optional character spacing in points (Tc operator)
116    /// * `word_spacing` - Optional word spacing in points (Tw operator)
117    pub fn add_text_with_spacing(
118        &mut self,
119        text: &str,
120        x: Length,
121        y: Length,
122        font_size: Length,
123        letter_spacing: Option<Length>,
124        word_spacing: Option<Length>,
125    ) {
126        // Build optional Tc/Tw spacing operators
127        let spacing_ops = build_spacing_ops(letter_spacing, word_spacing);
128        let ops = format!(
129            "BT\n/F1 {} Tf\n{}{} {} Td\n({}) Tj\nET\n",
130            font_size.to_pt(),
131            spacing_ops,
132            x.to_pt(),
133            y.to_pt(),
134            text
135        );
136        self.content.extend_from_slice(ops.as_bytes());
137    }
138
139    /// Add text to the page using a custom embedded font
140    ///
141    /// # Arguments
142    /// * `text` - The text to display
143    /// * `x` - X position
144    /// * `y` - Y position
145    /// * `font_size` - Font size
146    /// * `font_index` - Index of the embedded font (from `embed_font`)
147    ///
148    /// Note: Character usage tracking must be done separately via FontManager::record_text
149    pub fn add_text_with_font(
150        &mut self,
151        text: &str,
152        x: Length,
153        y: Length,
154        font_size: Length,
155        font_index: usize,
156    ) {
157        self.add_text_with_font_and_spacing(text, x, y, font_size, font_index, None, None);
158    }
159
160    /// Add text to the page using a custom embedded font with optional letter/word spacing
161    ///
162    /// # Arguments
163    /// * `text` - The text to display
164    /// * `x` - X position
165    /// * `y` - Y position
166    /// * `font_size` - Font size
167    /// * `font_index` - Index of the embedded font (from `embed_font`)
168    /// * `letter_spacing` - Optional character spacing in points (Tc operator)
169    /// * `word_spacing` - Optional word spacing in points (Tw operator)
170    #[allow(clippy::too_many_arguments)]
171    pub fn add_text_with_font_and_spacing(
172        &mut self,
173        text: &str,
174        x: Length,
175        y: Length,
176        font_size: Length,
177        font_index: usize,
178        letter_spacing: Option<Length>,
179        word_spacing: Option<Length>,
180    ) {
181        // Custom fonts are F2, F3, F4, etc. (F1 is reserved for Helvetica)
182        let font_name = format!("F{}", font_index + 2);
183
184        // Encode text for PDF - use hex strings for Unicode characters
185        let encoded_text = Self::encode_pdf_text(text);
186
187        // Build optional Tc/Tw spacing operators
188        let spacing_ops = build_spacing_ops(letter_spacing, word_spacing);
189
190        let ops = format!(
191            "BT\n/{} {} Tf\n{}{} {} Td\n{} Tj\nET\n",
192            font_name,
193            font_size.to_pt(),
194            spacing_ops,
195            x.to_pt(),
196            y.to_pt(),
197            encoded_text
198        );
199        self.content.extend_from_slice(ops.as_bytes());
200    }
201
202    /// Add text to the page using a custom embedded font and track character usage
203    ///
204    /// This is a convenience method that both adds the text and records character usage
205    /// for subsetting.
206    ///
207    /// # Arguments
208    /// * `text` - The text to display
209    /// * `x` - X position
210    /// * `y` - Y position
211    /// * `font_size` - Font size
212    /// * `font_index` - Index of the embedded font (from `embed_font`)
213    /// * `font_manager` - FontManager to record character usage
214    pub fn add_text_with_font_tracked(
215        &mut self,
216        text: &str,
217        x: Length,
218        y: Length,
219        font_size: Length,
220        font_index: usize,
221        font_manager: &mut FontManager,
222    ) {
223        // Record character usage for subsetting
224        font_manager.record_text(font_index, text);
225
226        // Add the text to the page
227        self.add_text_with_font(text, x, y, font_size, font_index);
228    }
229
230    /// Add background color to an area
231    pub fn add_background(
232        &mut self,
233        x: Length,
234        y: Length,
235        width: Length,
236        height: Length,
237        color: fop_types::Color,
238    ) {
239        self.add_background_with_radius(x, y, width, height, color, None);
240    }
241
242    /// Add background color to an area with optional rounded corners
243    pub fn add_background_with_radius(
244        &mut self,
245        x: Length,
246        y: Length,
247        width: Length,
248        height: Length,
249        color: fop_types::Color,
250        border_radius: Option<[Length; 4]>,
251    ) {
252        use crate::pdf::graphics::PdfGraphics;
253        let mut graphics = PdfGraphics::new();
254        let _ = graphics.set_fill_color(color);
255        let _ = graphics.fill_rectangle_with_radius(x, y, width, height, border_radius);
256        self.content
257            .extend_from_slice(graphics.content().as_bytes());
258    }
259
260    /// Add background color with opacity to an area with optional rounded corners
261    ///
262    /// # Arguments
263    /// * `x, y` - Bottom-left corner of the area (PDF coordinates)
264    /// * `width, height` - Dimensions of the area
265    /// * `color` - Fill color
266    /// * `border_radius` - Optional corner radii
267    /// * `gs_index` - Index of the ExtGState resource for opacity
268    #[allow(clippy::too_many_arguments)]
269    pub fn add_background_with_opacity(
270        &mut self,
271        x: Length,
272        y: Length,
273        width: Length,
274        height: Length,
275        color: fop_types::Color,
276        border_radius: Option<[Length; 4]>,
277        gs_index: usize,
278    ) {
279        use crate::pdf::graphics::PdfGraphics;
280        let mut graphics = PdfGraphics::new();
281        let _ = graphics.set_opacity(&format!("GS{}", gs_index));
282        let _ = graphics.set_fill_color(color);
283        let _ = graphics.fill_rectangle_with_radius(x, y, width, height, border_radius);
284        self.content
285            .extend_from_slice(graphics.content().as_bytes());
286    }
287
288    /// Add gradient background to an area
289    ///
290    /// # Arguments
291    /// * `x, y` - Bottom-left corner of the area (PDF coordinates)
292    /// * `width, height` - Dimensions of the area
293    /// * `gradient_index` - Index of the gradient in the document's gradient list
294    pub fn add_gradient_background(
295        &mut self,
296        x: Length,
297        y: Length,
298        width: Length,
299        height: Length,
300        gradient_index: usize,
301    ) {
302        self.add_gradient_background_with_radius(x, y, width, height, gradient_index, None);
303    }
304
305    /// Add gradient background to an area with optional rounded corners
306    ///
307    /// # Arguments
308    /// * `x, y` - Bottom-left corner of the area (PDF coordinates)
309    /// * `width, height` - Dimensions of the area
310    /// * `gradient_index` - Index of the gradient in the document's gradient list
311    /// * `border_radius` - Optional corner radii [top-left, top-right, bottom-right, bottom-left]
312    pub fn add_gradient_background_with_radius(
313        &mut self,
314        x: Length,
315        y: Length,
316        width: Length,
317        height: Length,
318        gradient_index: usize,
319        border_radius: Option<[Length; 4]>,
320    ) {
321        use crate::pdf::graphics::PdfGraphics;
322        let mut graphics = PdfGraphics::new();
323        let _ =
324            graphics.fill_gradient_with_radius(x, y, width, height, gradient_index, border_radius);
325        self.content
326            .extend_from_slice(graphics.content().as_bytes());
327    }
328
329    /// Add borders to an area
330    #[allow(clippy::too_many_arguments)]
331    pub fn add_borders(
332        &mut self,
333        x: Length,
334        y: Length,
335        width: Length,
336        height: Length,
337        border_widths: [Length; 4],
338        border_colors: [fop_types::Color; 4],
339        border_styles: [fop_layout::area::BorderStyle; 4],
340    ) {
341        self.add_borders_with_radius(
342            x,
343            y,
344            width,
345            height,
346            border_widths,
347            border_colors,
348            border_styles,
349            None,
350        );
351    }
352
353    /// Add borders to an area with optional rounded corners
354    #[allow(clippy::too_many_arguments)]
355    pub fn add_borders_with_radius(
356        &mut self,
357        x: Length,
358        y: Length,
359        width: Length,
360        height: Length,
361        border_widths: [Length; 4],
362        border_colors: [fop_types::Color; 4],
363        border_styles: [fop_layout::area::BorderStyle; 4],
364        border_radius: Option<[Length; 4]>,
365    ) {
366        use crate::pdf::graphics::PdfGraphics;
367        let mut graphics = PdfGraphics::new();
368        let _ = graphics.draw_borders_with_radius(
369            x,
370            y,
371            width,
372            height,
373            border_widths,
374            border_colors,
375            border_styles,
376            border_radius,
377        );
378        self.content
379            .extend_from_slice(graphics.content().as_bytes());
380    }
381
382    /// Add borders with opacity to an area with optional rounded corners
383    #[allow(clippy::too_many_arguments)]
384    pub fn add_borders_with_opacity(
385        &mut self,
386        x: Length,
387        y: Length,
388        width: Length,
389        height: Length,
390        border_widths: [Length; 4],
391        border_colors: [fop_types::Color; 4],
392        border_styles: [fop_layout::area::BorderStyle; 4],
393        border_radius: Option<[Length; 4]>,
394        gs_index: usize,
395    ) {
396        use crate::pdf::graphics::PdfGraphics;
397        let mut graphics = PdfGraphics::new();
398        let _ = graphics.set_stroke_opacity(&format!("GS{}", gs_index));
399        let _ = graphics.draw_borders_with_radius(
400            x,
401            y,
402            width,
403            height,
404            border_widths,
405            border_colors,
406            border_styles,
407            border_radius,
408        );
409        self.content
410            .extend_from_slice(graphics.content().as_bytes());
411    }
412
413    /// Add an image to the page
414    ///
415    /// # Arguments
416    /// * `image_index` - The index of the image XObject in the document's image_xobjects list
417    /// * `x` - X position in PDF coordinates (bottom-left origin)
418    /// * `y` - Y position in PDF coordinates (bottom-left origin)
419    /// * `width` - Display width
420    /// * `height` - Display height
421    pub fn add_image(
422        &mut self,
423        image_index: usize,
424        x: Length,
425        y: Length,
426        width: Length,
427        height: Length,
428    ) {
429        use std::fmt::Write;
430        let mut ops = String::new();
431
432        // Save graphics state
433        let _ = writeln!(&mut ops, "q");
434
435        // Set up transformation matrix: translate, then scale
436        // PDF images are 1x1 unit square by default, so we scale to width/height
437        let _ = writeln!(
438            &mut ops,
439            "{:.3} 0 0 {:.3} {:.3} {:.3} cm",
440            width.to_pt(),
441            height.to_pt(),
442            x.to_pt(),
443            y.to_pt()
444        );
445
446        // Draw the image
447        let _ = writeln!(&mut ops, "/Im{} Do", image_index);
448
449        // Restore graphics state
450        let _ = writeln!(&mut ops, "Q");
451
452        self.content.extend_from_slice(ops.as_bytes());
453    }
454
455    /// Add a horizontal rule (line) to the page
456    ///
457    /// # Arguments
458    /// * `x` - Left edge x-coordinate
459    /// * `y` - Bottom edge y-coordinate (PDF coordinate system)
460    /// * `width` - Rule width
461    /// * `thickness` - Line thickness
462    /// * `color` - Line color
463    /// * `style` - Line style (solid, dashed, dotted)
464    pub fn add_rule(
465        &mut self,
466        x: Length,
467        y: Length,
468        width: Length,
469        thickness: Length,
470        color: fop_types::Color,
471        style: &str,
472    ) {
473        use std::fmt::Write;
474        let mut ops = String::new();
475
476        // Set stroke color
477        let _ = writeln!(
478            &mut ops,
479            "{:.3} {:.3} {:.3} RG",
480            color.r_f32(),
481            color.g_f32(),
482            color.b_f32()
483        );
484
485        // Set line width
486        let _ = writeln!(&mut ops, "{:.3} w", thickness.to_pt());
487
488        // Set dash pattern based on style
489        match style {
490            "dashed" => {
491                let _ = writeln!(&mut ops, "[6 3] 0 d");
492            }
493            "dotted" => {
494                let _ = writeln!(&mut ops, "[1 2] 0 d");
495            }
496            _ => {
497                // solid or unknown - use solid line
498                let _ = writeln!(&mut ops, "[] 0 d");
499            }
500        }
501
502        // Draw the line (move to start, line to end, stroke)
503        let _ = writeln!(
504            &mut ops,
505            "{:.3} {:.3} m {:.3} {:.3} l S",
506            x.to_pt(),
507            y.to_pt(),
508            (x + width).to_pt(),
509            y.to_pt()
510        );
511
512        self.content.extend_from_slice(ops.as_bytes());
513    }
514
515    /// Save graphics state and set clipping path
516    ///
517    /// This method saves the current graphics state and establishes a rectangular
518    /// clipping path. Content drawn after this call will be clipped to the specified
519    /// rectangle until restore_clip_state() is called.
520    ///
521    /// PDF operators used:
522    /// - q: Save graphics state
523    /// - re: Rectangle path
524    /// - W: Set clipping path (intersect with current path)
525    /// - n: End path without stroking or filling
526    ///
527    /// # Arguments
528    /// * `x, y` - Bottom-left corner of clipping rectangle (PDF coordinates)
529    /// * `width, height` - Dimensions of clipping rectangle
530    ///
531    /// # PDF Reference
532    /// See PDF specification section 8.5 for clipping path details.
533    pub fn save_clip_state(
534        &mut self,
535        x: Length,
536        y: Length,
537        width: Length,
538        height: Length,
539    ) -> fop_types::Result<()> {
540        use std::fmt::Write;
541        let mut ops = String::new();
542
543        // Save graphics state
544        writeln!(&mut ops, "q").map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
545
546        // Define rectangle path and set as clipping path
547        writeln!(
548            &mut ops,
549            "{:.3} {:.3} {:.3} {:.3} re W n",
550            x.to_pt(),
551            y.to_pt(),
552            width.to_pt(),
553            height.to_pt()
554        )
555        .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
556
557        self.content.extend_from_slice(ops.as_bytes());
558        Ok(())
559    }
560
561    /// Restore graphics state after clipping
562    ///
563    /// This restores the graphics state that was saved by save_clip_state(),
564    /// removing the clipping path.
565    ///
566    /// PDF operator used:
567    /// - Q: Restore graphics state
568    pub fn restore_clip_state(&mut self) -> fop_types::Result<()> {
569        use std::fmt::Write;
570        let mut ops = String::new();
571
572        writeln!(&mut ops, "Q").map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
573
574        self.content.extend_from_slice(ops.as_bytes());
575        Ok(())
576    }
577}