Skip to main content

xberg_native_pdf/elements/
text.rs

1//! Text content element types.
2//!
3//! This module provides the `TextContent` type and related structures
4//! for representing text in PDFs.
5
6use crate::extractors::text::ArtifactType;
7use crate::geometry::{Point, Rect};
8use crate::layout::{Color, FontWeight, TextSpan};
9
10/// Text content that can be extracted from or written to a PDF.
11///
12/// This is the unified text representation for both reading and writing.
13/// Unlike `TextSpan` which is extraction-focused, `TextContent` is designed
14/// to work symmetrically for both directions.
15#[derive(Debug, Clone, Default)]
16pub struct TextContent {
17    /// The text string
18    pub text: String,
19    /// Bounding box of the text
20    pub bbox: Rect,
21    /// Font specification
22    pub font: FontSpec,
23    /// Text styling (bold, italic, color, etc.)
24    pub style: TextStyle,
25    /// Reading order index (for extraction) or write order (for generation)
26    pub reading_order: Option<usize>,
27    /// Artifact type classification
28    pub artifact_type: Option<ArtifactType>,
29
30    /// Baseline origin point (extracted from text matrix)
31    pub origin: Option<Point>,
32    /// Rotation angle in degrees (0-360)
33    pub rotation_degrees: Option<f32>,
34    /// Full transformation matrix [a, b, c, d, e, f]
35    pub matrix: Option<[f32; 6]>,
36}
37
38impl TextContent {
39    /// Create a new text content element.
40    pub fn new(text: impl Into<String>, bbox: Rect, font: FontSpec, style: TextStyle) -> Self {
41        Self {
42            text: text.into(),
43            bbox,
44            font,
45            style,
46            reading_order: None,
47            artifact_type: None,
48            origin: None,
49            rotation_degrees: None,
50            matrix: None,
51        }
52    }
53
54    /// Create text content with reading order.
55    pub fn with_reading_order(mut self, order: usize) -> Self {
56        self.reading_order = Some(order);
57        self
58    }
59
60    /// Set the artifact type.
61    pub fn with_artifact_type(mut self, artifact_type: ArtifactType) -> Self {
62        self.artifact_type = Some(artifact_type);
63        self
64    }
65
66    /// Check if this text is bold.
67    pub fn is_bold(&self) -> bool {
68        self.style.weight.is_bold()
69    }
70
71    /// Check if this text is italic.
72    pub fn is_italic(&self) -> bool {
73        self.style.italic
74    }
75
76    /// Get the font size in points.
77    pub fn font_size(&self) -> f32 {
78        self.font.size
79    }
80
81    /// Set the transformation matrix.
82    pub fn with_matrix(mut self, matrix: [f32; 6]) -> Self {
83        self.matrix = Some(matrix);
84        self
85    }
86
87    /// Set the origin point.
88    pub fn with_origin(mut self, origin: Point) -> Self {
89        self.origin = Some(origin);
90        self
91    }
92
93    /// Set the rotation angle in degrees.
94    pub fn with_rotation(mut self, degrees: f32) -> Self {
95        self.rotation_degrees = Some(degrees);
96        self
97    }
98
99    /// Check if this text is rotated (non-zero rotation).
100    pub fn is_rotated(&self) -> bool {
101        self.rotation_degrees.map(|r| r.abs() > 0.1).unwrap_or(false)
102    }
103
104    /// Get rotation angle in radians.
105    pub fn rotation_radians(&self) -> Option<f32> {
106        self.rotation_degrees.map(|d| d.to_radians())
107    }
108
109    /// Get the transformation matrix if available.
110    pub fn get_matrix(&self) -> Option<[f32; 6]> {
111        self.matrix
112    }
113}
114
115/// Convert from TextSpan (extraction result) to TextContent (unified representation).
116impl From<TextSpan> for TextContent {
117    fn from(span: TextSpan) -> Self {
118        TextContent {
119            text: span.text,
120            bbox: span.bbox,
121            font: FontSpec {
122                name: span.font_name,
123                size: span.font_size,
124            },
125            style: TextStyle {
126                weight: span.font_weight,
127                italic: span.is_italic,
128                color: span.color,
129                underline: false,
130                strikethrough: false,
131            },
132            reading_order: Some(span.sequence),
133            artifact_type: span.artifact_type,
134            origin: None,
135            rotation_degrees: None,
136            matrix: None,
137        }
138    }
139}
140
141/// Convert from TextContent to TextSpan (for backward compatibility).
142impl From<TextContent> for TextSpan {
143    fn from(content: TextContent) -> Self {
144        TextSpan {
145            provenance: None,
146            text: content.text,
147            bbox: content.bbox,
148            font_name: content.font.name,
149            font_size: content.font.size,
150            font_weight: content.style.weight,
151            is_italic: content.style.italic,
152            is_monospace: false,
153            color: content.style.color,
154            mcid: None,
155            mcid_scope: None,
156            sequence: content.reading_order.unwrap_or(0),
157            split_boundary_before: false,
158            offset_semantic: false,
159            char_spacing: 0.0,
160            word_spacing: 0.0,
161            horizontal_scaling: 100.0,
162            primary_detected: false,
163            artifact_type: content.artifact_type,
164            char_widths: vec![],
165            char_x_offsets: Vec::new(),
166            heading_level: None,
167            rotation_degrees: 0.0,
168            wmode: 0,
169            text_rise: 0.0,
170            rtl_draw_logical: false,
171            mirrored: false,
172            page_rotation_applied: 0,
173        }
174    }
175}
176
177/// Font specification for text rendering.
178#[derive(Debug, Clone)]
179pub struct FontSpec {
180    /// Font name (e.g., "Helvetica")
181    pub name: String,
182    /// Font size in points
183    pub size: f32,
184}
185
186impl Default for FontSpec {
187    fn default() -> Self {
188        Self {
189            name: "Helvetica".to_string(),
190            size: 12.0,
191        }
192    }
193}
194
195impl FontSpec {
196    /// Create a new font specification.
197    pub fn new(name: impl Into<String>, size: f32) -> Self {
198        Self {
199            name: name.into(),
200            size,
201        }
202    }
203
204    /// Create a Helvetica font specification.
205    pub fn helvetica(size: f32) -> Self {
206        Self::new("Helvetica", size)
207    }
208
209    /// Create a Times-Roman font specification.
210    pub fn times(size: f32) -> Self {
211        Self::new("Times-Roman", size)
212    }
213
214    /// Create a Courier font specification.
215    pub fn courier(size: f32) -> Self {
216        Self::new("Courier", size)
217    }
218}
219
220/// Font style classification.
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
222pub enum FontStyle {
223    /// Normal (upright) style
224    #[default]
225    Normal,
226    /// Italic style
227    Italic,
228    /// Oblique style (slanted but not true italic)
229    Oblique,
230}
231
232/// Text styling information.
233#[derive(Debug, Clone, Default)]
234pub struct TextStyle {
235    /// Font weight (normal, bold, etc.)
236    pub weight: FontWeight,
237    /// Whether the text is italic
238    pub italic: bool,
239    /// Text color
240    pub color: Color,
241    /// Whether the text is underlined
242    pub underline: bool,
243    /// Whether the text has a strikethrough
244    pub strikethrough: bool,
245}
246
247impl TextStyle {
248    /// Create default text style.
249    pub fn new() -> Self {
250        Self::default()
251    }
252
253    /// Create a bold text style.
254    pub fn bold() -> Self {
255        Self {
256            weight: FontWeight::Bold,
257            ..Default::default()
258        }
259    }
260
261    /// Create an italic text style.
262    pub fn italic() -> Self {
263        Self {
264            italic: true,
265            ..Default::default()
266        }
267    }
268
269    /// Create a bold-italic text style.
270    pub fn bold_italic() -> Self {
271        Self {
272            weight: FontWeight::Bold,
273            italic: true,
274            ..Default::default()
275        }
276    }
277
278    /// Set font weight.
279    pub fn with_weight(mut self, weight: FontWeight) -> Self {
280        self.weight = weight;
281        self
282    }
283
284    /// Set italic style.
285    pub fn with_italic(mut self, italic: bool) -> Self {
286        self.italic = italic;
287        self
288    }
289
290    /// Set text color.
291    pub fn with_color(mut self, color: Color) -> Self {
292        self.color = color;
293        self
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_text_content_creation() {
303        let text = TextContent::new(
304            "Hello",
305            Rect::new(0.0, 0.0, 50.0, 12.0),
306            FontSpec::default(),
307            TextStyle::default(),
308        );
309
310        assert_eq!(text.text, "Hello");
311        assert_eq!(text.font_size(), 12.0);
312        assert!(!text.is_bold());
313        assert!(!text.is_italic());
314    }
315
316    #[test]
317    fn test_text_span_conversion() {
318        let span = TextSpan {
319            provenance: None,
320            text_rise: 0.0,
321            artifact_type: None,
322            text: "Test".to_string(),
323            bbox: Rect::new(10.0, 20.0, 40.0, 12.0),
324            font_name: "Times".to_string(),
325            font_size: 12.0,
326            font_weight: FontWeight::Bold,
327            is_italic: false,
328            is_monospace: false,
329            color: Color::black(),
330            mcid: None,
331            mcid_scope: None,
332            sequence: 3,
333            split_boundary_before: false,
334            offset_semantic: false,
335            char_spacing: 0.0,
336            word_spacing: 0.0,
337            horizontal_scaling: 100.0,
338            primary_detected: false,
339            char_widths: vec![],
340            char_x_offsets: Vec::new(),
341            heading_level: None,
342            rotation_degrees: 0.0,
343            wmode: 0,
344            rtl_draw_logical: false,
345            mirrored: false,
346            page_rotation_applied: 0,
347        };
348
349        let content: TextContent = span.into();
350
351        assert_eq!(content.text, "Test");
352        assert_eq!(content.font.name, "Times");
353        assert_eq!(content.font.size, 12.0);
354        assert!(content.is_bold());
355        assert_eq!(content.reading_order, Some(3));
356    }
357}