1use serde::{Deserialize, Serialize};
4
5use super::bbox::{BoundingBox, Vertex};
6use super::enums::{PdfLayer, TextFormat, TextType};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct TextChunk {
11 pub value: String,
13 pub bbox: BoundingBox,
15 pub font_name: String,
17 pub font_size: f64,
19 pub font_weight: f64,
21 pub italic_angle: f64,
23 pub font_color: String,
25 pub contrast_ratio: f64,
27 pub symbol_ends: Vec<f64>,
29 pub text_format: TextFormat,
31 pub text_type: TextType,
33 pub pdf_layer: PdfLayer,
35 pub ocg_visible: bool,
37 pub index: Option<usize>,
39 pub page_number: Option<u32>,
41 pub level: Option<String>,
43 #[serde(skip_serializing_if = "Option::is_none")]
46 pub mcid: Option<i64>,
47}
48
49impl TextChunk {
50 pub fn is_white_space_chunk(&self) -> bool {
52 self.value.chars().all(|c| c.is_whitespace())
53 }
54
55 pub fn compress_spaces(&mut self) {
57 let mut result = String::with_capacity(self.value.len());
58 let mut last_was_space = false;
59 for ch in self.value.chars() {
60 if ch == ' ' {
61 if !last_was_space {
62 result.push(' ');
63 }
64 last_was_space = true;
65 } else {
66 result.push(ch);
67 last_was_space = false;
68 }
69 }
70 self.value = result;
71 }
72
73 pub fn text_length(&self) -> usize {
75 self.value.chars().count()
76 }
77
78 pub fn average_symbol_width(&self) -> f64 {
80 let len = self.text_length();
81 if len == 0 {
82 return 0.0;
83 }
84 self.bbox.width() / len as f64
85 }
86
87 pub fn symbol_start_coordinate(&self, idx: usize) -> f64 {
89 if idx == 0 {
90 self.bbox.left_x
91 } else if idx <= self.symbol_ends.len() {
92 self.symbol_ends[idx - 1]
93 } else {
94 self.bbox.right_x
95 }
96 }
97
98 pub fn symbol_end_coordinate(&self, idx: usize) -> f64 {
100 if idx < self.symbol_ends.len() {
101 self.symbol_ends[idx]
102 } else {
103 self.bbox.right_x
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub enum ImagePaintSource {
112 #[default]
114 Unknown,
115 Do,
117 PatternFill,
119 Inline,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ImageChunk {
126 pub bbox: BoundingBox,
128 pub index: Option<u32>,
130 pub level: Option<String>,
132 #[serde(default)]
134 pub source: ImagePaintSource,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct LineChunk {
140 pub bbox: BoundingBox,
142 pub index: Option<u32>,
144 pub level: Option<String>,
146 pub start: Vertex,
148 pub end: Vertex,
150 pub width: f64,
152 pub is_horizontal_line: bool,
154 pub is_vertical_line: bool,
156 pub is_square: bool,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct LineArtChunk {
163 pub bbox: BoundingBox,
165 pub index: Option<u32>,
167 pub level: Option<String>,
169 pub line_chunks: Vec<LineChunk>,
171}
172
173pub const LINE_ART_SIZE_EPSILON: f64 = 1.0;
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 fn make_text_chunk(value: &str) -> TextChunk {
181 TextChunk {
182 value: value.to_string(),
183 bbox: BoundingBox::new(Some(1), 0.0, 0.0, 100.0, 12.0),
184 font_name: "Helvetica".to_string(),
185 font_size: 12.0,
186 font_weight: 400.0,
187 italic_angle: 0.0,
188 font_color: "#000000".to_string(),
189 contrast_ratio: 21.0,
190 symbol_ends: vec![],
191 text_format: TextFormat::Normal,
192 text_type: TextType::Regular,
193 pdf_layer: PdfLayer::Main,
194 ocg_visible: true,
195 index: None,
196 page_number: Some(1),
197 level: None,
198 mcid: None,
199 }
200 }
201
202 #[test]
203 fn test_is_white_space_chunk() {
204 assert!(make_text_chunk(" ").is_white_space_chunk());
205 assert!(!make_text_chunk("hello").is_white_space_chunk());
206 assert!(make_text_chunk("").is_white_space_chunk());
207 }
208
209 #[test]
210 fn test_compress_spaces() {
211 let mut chunk = make_text_chunk("hello world test");
212 chunk.compress_spaces();
213 assert_eq!(chunk.value, "hello world test");
214 }
215
216 #[test]
217 fn test_text_length() {
218 assert_eq!(make_text_chunk("hello").text_length(), 5);
219 assert_eq!(make_text_chunk("").text_length(), 0);
220 }
221
222 #[test]
223 fn test_average_symbol_width() {
224 let chunk = make_text_chunk("hello");
225 assert!((chunk.average_symbol_width() - 20.0).abs() < 0.01);
226 }
227}