xberg_native_pdf/elements/
text.rs1use crate::extractors::text::ArtifactType;
7use crate::geometry::{Point, Rect};
8use crate::layout::{Color, FontWeight, TextSpan};
9
10#[derive(Debug, Clone, Default)]
16pub struct TextContent {
17 pub text: String,
19 pub bbox: Rect,
21 pub font: FontSpec,
23 pub style: TextStyle,
25 pub reading_order: Option<usize>,
27 pub artifact_type: Option<ArtifactType>,
29
30 pub origin: Option<Point>,
32 pub rotation_degrees: Option<f32>,
34 pub matrix: Option<[f32; 6]>,
36}
37
38impl TextContent {
39 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 pub fn with_reading_order(mut self, order: usize) -> Self {
56 self.reading_order = Some(order);
57 self
58 }
59
60 pub fn with_artifact_type(mut self, artifact_type: ArtifactType) -> Self {
62 self.artifact_type = Some(artifact_type);
63 self
64 }
65
66 pub fn is_bold(&self) -> bool {
68 self.style.weight.is_bold()
69 }
70
71 pub fn is_italic(&self) -> bool {
73 self.style.italic
74 }
75
76 pub fn font_size(&self) -> f32 {
78 self.font.size
79 }
80
81 pub fn with_matrix(mut self, matrix: [f32; 6]) -> Self {
83 self.matrix = Some(matrix);
84 self
85 }
86
87 pub fn with_origin(mut self, origin: Point) -> Self {
89 self.origin = Some(origin);
90 self
91 }
92
93 pub fn with_rotation(mut self, degrees: f32) -> Self {
95 self.rotation_degrees = Some(degrees);
96 self
97 }
98
99 pub fn is_rotated(&self) -> bool {
101 self.rotation_degrees.map(|r| r.abs() > 0.1).unwrap_or(false)
102 }
103
104 pub fn rotation_radians(&self) -> Option<f32> {
106 self.rotation_degrees.map(|d| d.to_radians())
107 }
108
109 pub fn get_matrix(&self) -> Option<[f32; 6]> {
111 self.matrix
112 }
113}
114
115impl 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
141impl 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#[derive(Debug, Clone)]
179pub struct FontSpec {
180 pub name: String,
182 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 pub fn new(name: impl Into<String>, size: f32) -> Self {
198 Self {
199 name: name.into(),
200 size,
201 }
202 }
203
204 pub fn helvetica(size: f32) -> Self {
206 Self::new("Helvetica", size)
207 }
208
209 pub fn times(size: f32) -> Self {
211 Self::new("Times-Roman", size)
212 }
213
214 pub fn courier(size: f32) -> Self {
216 Self::new("Courier", size)
217 }
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
222pub enum FontStyle {
223 #[default]
225 Normal,
226 Italic,
228 Oblique,
230}
231
232#[derive(Debug, Clone, Default)]
234pub struct TextStyle {
235 pub weight: FontWeight,
237 pub italic: bool,
239 pub color: Color,
241 pub underline: bool,
243 pub strikethrough: bool,
245}
246
247impl TextStyle {
248 pub fn new() -> Self {
250 Self::default()
251 }
252
253 pub fn bold() -> Self {
255 Self {
256 weight: FontWeight::Bold,
257 ..Default::default()
258 }
259 }
260
261 pub fn italic() -> Self {
263 Self {
264 italic: true,
265 ..Default::default()
266 }
267 }
268
269 pub fn bold_italic() -> Self {
271 Self {
272 weight: FontWeight::Bold,
273 italic: true,
274 ..Default::default()
275 }
276 }
277
278 pub fn with_weight(mut self, weight: FontWeight) -> Self {
280 self.weight = weight;
281 self
282 }
283
284 pub fn with_italic(mut self, italic: bool) -> Self {
286 self.italic = italic;
287 self
288 }
289
290 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}