printpdf 0.9.1

Rust library for reading and writing PDF files
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
//! Text shaping and measurement
//!
//! This module provides functionality for measuring and shaping text,
//! allowing for complex text layout with precise positioning, line breaks,
//! and text flow around "holes" (like images or other non-text content).
//!
//! # Overview
//!
//! The text shaping pipeline converts raw text + font into positioned glyphs:
//!
//! ```text
//! Text + Font + Options
//!//! UnifiedLayout (from azul-layout text3 engine)
//!//! Vec<Op> (PDF operations)
//! ```
//!
//! # Example
//!
//! ```ignore
//! use printpdf::*;
//!
//! // Load a font
//! let font_bytes = std::fs::read("my_font.ttf")?;
//! let font = ParsedFont::from_bytes(&font_bytes, 0)?;
//!
//! // Shape text
//! let options = TextShapingOptions::new(Pt(12.0))
//!     .with_max_width(Pt(200.0))
//!     .with_align(TextAlign::Left);
//!
//! let layout = shape_text("Hello, World!", &font, &options)?;
//!
//! // Convert to PDF operations
//! let ops = layout_to_ops(&layout, page_height, &font_id, Color::black());
//! ```

use crate::{Color, FontId, Op, Pt, Rect, Rgb};

#[cfg(feature = "text_layout")]
use azul_layout::text3::{
    cache::{LoadedFonts, ParsedFontTrait, UnifiedLayout},
    glyphs::get_glyph_runs_pdf,
};

#[cfg(feature = "text_layout")]
use azul_css::props::basic::ColorU;

/// Represents a "hole" in the text layout where text won't flow.
///
/// Text holes are rectangular regions that text should flow around,
/// useful for inline images or other non-text content.
///
/// # Example
/// ```ignore
/// let hole = TextHole {
///     rect: Rect { x: Pt(100.0), y: Pt(200.0), width: Pt(50.0), height: Pt(50.0) }
/// };
/// ```
#[derive(Debug, Clone)]
pub struct TextHole {
    /// The rectangular area of the hole in points
    pub rect: Rect,
}

/// Horizontal text alignment options
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TextAlign {
    /// Left align text (default for LTR scripts)
    #[default]
    Left,
    /// Center align text
    Center,
    /// Right align text (default for RTL scripts)
    Right,
    /// Justified text (both edges aligned)
    Justify,
}

/// Text direction for bidirectional text support
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TextDirection {
    /// Left-to-right (default for Latin, Cyrillic, etc.)
    #[default]
    Ltr,
    /// Right-to-left (for Arabic, Hebrew, etc.)
    Rtl,
}

/// Options for text shaping and layout
#[derive(Debug, Clone)]
pub struct TextShapingOptions {
    /// Font size in points (1 pt = 1/72 inch)
    pub font_size: Pt,
    /// Line height in points (optional, defaults to font's recommended line height)
    pub line_height: Option<Pt>,
    /// Letter spacing in points (0.0 = default spacing)
    pub letter_spacing: Option<f32>,
    /// Word spacing in points (0.0 = default spacing)
    pub word_spacing: Option<f32>,
    /// Width of tab character in points (default: 4 spaces)
    pub tab_width: Option<f32>,
    /// Maximum width of text block for line wrapping (None = no wrapping)
    pub max_width: Option<Pt>,
    /// Maximum height of text block (None = unlimited)
    pub max_height: Option<Pt>,
    /// Horizontal text alignment
    pub align: TextAlign,
    /// Text direction (LTR or RTL)
    pub direction: TextDirection,
    /// Text color
    pub color: Color,
    /// Rectangular "holes" where text won't flow
    pub holes: Vec<TextHole>,
}

impl TextShapingOptions {
    /// Create new options with default values and the specified font size
    pub fn new(font_size: Pt) -> Self {
        Self {
            font_size,
            ..Default::default()
        }
    }

    /// Set the maximum width for line wrapping
    pub fn with_max_width(mut self, max_width: Pt) -> Self {
        self.max_width = Some(max_width);
        self
    }

    /// Set the maximum height for text clipping
    pub fn with_max_height(mut self, max_height: Pt) -> Self {
        self.max_height = Some(max_height);
        self
    }

    /// Set the text alignment
    pub fn with_align(mut self, align: TextAlign) -> Self {
        self.align = align;
        self
    }

    /// Set the text direction
    pub fn with_direction(mut self, direction: TextDirection) -> Self {
        self.direction = direction;
        self
    }

    /// Set the text color
    pub fn with_color(mut self, color: Color) -> Self {
        self.color = color;
        self
    }

    /// Set the line height
    pub fn with_line_height(mut self, line_height: Pt) -> Self {
        self.line_height = Some(line_height);
        self
    }

    /// Set the letter spacing
    pub fn with_letter_spacing(mut self, letter_spacing: f32) -> Self {
        self.letter_spacing = Some(letter_spacing);
        self
    }
}

impl Default for TextShapingOptions {
    fn default() -> Self {
        Self {
            font_size: Pt(12.0),
            line_height: None,
            letter_spacing: None,
            word_spacing: None,
            tab_width: None,
            max_width: None,
            max_height: None,
            align: TextAlign::default(),
            direction: TextDirection::default(),
            color: Color::Rgb(Rgb {
                r: 0.0,
                g: 0.0,
                b: 0.0,
                icc_profile: None,
            }),
            holes: Vec::new(),
        }
    }
}

/// A shaped word with positioning information.
///
/// Represents a single word after text shaping, with its computed
/// position and dimensions relative to the text block origin.
///
/// All measurements are in points (1/72 inch).
#[derive(Debug, Clone)]
pub struct ShapedWord {
    /// The text content of the word
    pub text: String,
    /// X position relative to the text origin (in points)
    pub x: f32,
    /// Y position relative to the text origin (in points)
    pub y: f32,
    /// Width of the word in points
    pub width: f32,
    /// Height of the word in points
    pub height: f32,
    /// Index of the word within its line (0-based)
    pub index: usize,
}

/// A line of shaped text.
///
/// Represents a single line of text after shaping and line-breaking,
/// containing the individual words and the line's position/dimensions.
///
/// All measurements are in points (1/72 inch).
#[derive(Debug, Clone)]
pub struct ShapedLine {
    /// Words making up the line
    pub words: Vec<ShapedWord>,
    /// X position relative to the text origin (in points)
    pub x: f32,
    /// Y position relative to the text origin (in points)
    pub y: f32,
    /// Width of the line in points
    pub width: f32,
    /// Height of the line in points
    pub height: f32,
    /// Line number (0-based)
    pub index: usize,
}

/// A block of shaped text with full layout information.
///
/// This is the result of text shaping and can be converted to PDF operations.
#[derive(Debug, Clone)]
pub struct ShapedText {
    /// Font ID that the ShapedText used
    pub font_id: FontId,
    /// Options that this text was laid out with
    pub options: TextShapingOptions,
    /// Lines making up the text block
    pub lines: Vec<ShapedLine>,
    /// Total width of the text block in points
    pub width: f32,
    /// Total height of the text block in points
    pub height: f32,
}

impl ShapedText {
    /// Get the bounding box of the shaped text
    pub fn bounds(&self) -> Rect {
        Rect {
            x: Pt(0.0),
            y: Pt(0.0),
            width: Pt(self.width),
            height: Pt(self.height),
            mode: None,
            winding_order: None,
        }
    }

    /// Check if the shaped text is empty
    pub fn is_empty(&self) -> bool {
        self.lines.is_empty()
    }

    /// Get the number of lines
    pub fn line_count(&self) -> usize {
        self.lines.len()
    }
}

// ============================================================================
// High-level API for text shaping (requires text_layout feature)
// ============================================================================

/// Convert a `UnifiedLayout` to PDF operations.
///
/// This is the main function for converting azul's layout output to printpdf operations.
/// It handles:
/// - Coordinate transformation from layout space (top-left origin) to PDF space (bottom-left origin)
/// - Font selection via SetFont operations
/// - Glyph positioning via SetTextMatrix
/// - Color handling via SetFillColor
///
/// # Arguments
/// * `layout` - The unified layout from azul's text3 engine
/// * `page_height` - The page height in points (for Y-coordinate flipping)
/// * `font_id` - The font ID to use in PDF operations
/// * `loaded_fonts` - Pre-loaded fonts for glyph lookup
/// * `default_color` - Default text color if not specified per-glyph
///
/// # Returns
/// A vector of PDF operations that render the text
#[cfg(feature = "text_layout")]
pub fn layout_to_ops<T: ParsedFontTrait + 'static>(
    layout: &UnifiedLayout,
    page_height: Pt,
    font_id: &FontId,
    loaded_fonts: &LoadedFonts<T>,
    default_color: Color,
) -> Vec<Op> {
    layout_to_ops_with_offset(layout, page_height, font_id, loaded_fonts, default_color, Pt(0.0), Pt(0.0))
}

/// Convert a `UnifiedLayout` to PDF operations with position offset.
///
/// Same as `layout_to_ops` but allows specifying an offset for positioning
/// the text block on the page.
///
/// # Arguments
/// * `layout` - The unified layout from azul's text3 engine
/// * `page_height` - The page height in points (for Y-coordinate flipping)
/// * `font_id` - The font ID to use in PDF operations
/// * `loaded_fonts` - Pre-loaded fonts for glyph lookup
/// * `default_color` - Default text color if not specified per-glyph
/// * `offset_x` - X offset from the left edge of the page
/// * `offset_y` - Y offset from the top edge of the page (will be converted to PDF coordinates)
///
/// # Returns
/// A vector of PDF operations that render the text
#[cfg(feature = "text_layout")]
pub fn layout_to_ops_with_offset<T: ParsedFontTrait + 'static>(
    layout: &UnifiedLayout,
    page_height: Pt,
    _font_id: &FontId,
    loaded_fonts: &LoadedFonts<T>,
    _default_color: Color,
    offset_x: Pt,
    offset_y: Pt,
) -> Vec<Op> {
    let mut ops = Vec::new();

    // Get PDF-optimized glyph runs from the layout
    let glyph_runs = get_glyph_runs_pdf(layout, loaded_fonts);

    if glyph_runs.is_empty() {
        return ops;
    }

    // Track current color to avoid redundant SetFillColor operations
    let mut current_color: Option<ColorU> = None;

    // Process each glyph run
    for run in glyph_runs.iter() {
        if run.glyphs.is_empty() {
            continue;
        }

        // Set color if changed
        if current_color != Some(run.color) {
            ops.push(Op::SetFillColor {
                col: coloru_to_color(&run.color),
            });
            current_color = Some(run.color);
        }

        // Set font (SetFont must be outside the text section in PDF)
        let run_font_id = FontId(format!("F{}", run.font_hash));
        ops.push(Op::SetFont {
            font: crate::ops::PdfFontHandle::External(run_font_id),
            size: Pt(run.font_size_px),
        });

        // Start text section
        ops.push(Op::StartTextSection);

        // Position and render each glyph
        for glyph in &run.glyphs {
            // Calculate PDF coordinates
            // Layout uses top-left origin, PDF uses bottom-left
            let pdf_x = glyph.position.x + offset_x.0;
            let pdf_y = page_height.0 - glyph.position.y - offset_y.0;

            // Set text matrix to position this glyph
            ops.push(Op::SetTextMatrix {
                matrix: crate::matrix::TextMatrix::Raw([
                    1.0,    // a: horizontal scaling
                    0.0,    // b: horizontal skewing
                    0.0,    // c: vertical skewing
                    1.0,    // d: vertical scaling
                    pdf_x,  // e: x translation
                    pdf_y,  // f: y translation
                ]),
            });

            // Render the glyph
            ops.push(Op::ShowText {
                items: vec![crate::text::TextItem::GlyphIds(vec![
                    crate::text::Codepoint {
                        gid: glyph.glyph_id,
                        offset: 0.0,
                        cid: Some(glyph.unicode_codepoint.clone()),
                    }
                ])],
            });
        }

        // End text section
        ops.push(Op::EndTextSection);
    }

    ops
}

/// Helper function to convert azul ColorU to printpdf Color
#[cfg(feature = "text_layout")]
fn coloru_to_color(color: &ColorU) -> Color {
    Color::Rgb(Rgb {
        r: color.r as f32 / 255.0,
        g: color.g as f32 / 255.0,
        b: color.b as f32 / 255.0,
        icc_profile: None,
    })
}