1use crate::geometry::Rect;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum TableSource {
14 StructureTree,
19 #[default]
24 SpatialDetection,
25 UserGenerated,
29}
30
31#[derive(Debug, Clone, Default)]
36pub struct TableDetectionInfo {
37 pub source: TableSource,
39 pub confidence: f32,
45 pub detection_method: Option<String>,
47}
48
49impl TableDetectionInfo {
50 pub fn from_structure_tree() -> Self {
52 Self {
53 source: TableSource::StructureTree,
54 confidence: 1.0,
55 detection_method: Some("structure_tree".to_string()),
56 }
57 }
58
59 pub fn from_spatial_detection(confidence: f32, method: impl Into<String>) -> Self {
61 Self {
62 source: TableSource::SpatialDetection,
63 confidence: confidence.clamp(0.0, 1.0),
64 detection_method: Some(method.into()),
65 }
66 }
67
68 pub fn user_generated() -> Self {
70 Self {
71 source: TableSource::UserGenerated,
72 confidence: 1.0,
73 detection_method: None,
74 }
75 }
76}
77
78#[derive(Debug, Clone)]
83pub struct TableContent {
84 pub bbox: Rect,
86 pub rows: Vec<TableRowContent>,
88 pub column_widths: Vec<f32>,
90 pub reading_order: Option<usize>,
92 pub caption: Option<String>,
94 pub style: TableContentStyle,
96 pub detection_info: TableDetectionInfo,
98}
99
100impl Default for TableContent {
101 fn default() -> Self {
102 Self {
103 bbox: Rect::new(0.0, 0.0, 0.0, 0.0),
104 rows: Vec::new(),
105 column_widths: Vec::new(),
106 reading_order: None,
107 caption: None,
108 style: TableContentStyle::default(),
109 detection_info: TableDetectionInfo::default(),
110 }
111 }
112}
113
114impl TableContent {
115 pub fn new(bbox: Rect) -> Self {
117 Self {
118 bbox,
119 ..Default::default()
120 }
121 }
122
123 pub fn add_row(&mut self, row: TableRowContent) {
125 self.rows.push(row);
126 }
127
128 pub fn row_count(&self) -> usize {
130 self.rows.len()
131 }
132
133 pub fn column_count(&self) -> usize {
135 self.rows.first().map(|r| r.cells.len()).unwrap_or(0)
136 }
137
138 pub fn get_cell(&self, row: usize, col: usize) -> Option<&TableCellContent> {
140 self.rows.get(row).and_then(|r| r.cells.get(col))
141 }
142
143 pub fn has_header(&self) -> bool {
145 self.rows.first().is_some_and(|r| r.is_header)
146 }
147
148 pub fn with_detection_info(mut self, info: TableDetectionInfo) -> Self {
150 self.detection_info = info;
151 self
152 }
153
154 pub fn user_generated(bbox: Rect) -> Self {
156 Self {
157 bbox,
158 detection_info: TableDetectionInfo::user_generated(),
159 ..Default::default()
160 }
161 }
162
163 pub fn is_from_structure_tree(&self) -> bool {
165 self.detection_info.source == TableSource::StructureTree
166 }
167
168 pub fn detection_confidence(&self) -> f32 {
170 self.detection_info.confidence
171 }
172}
173
174#[derive(Debug, Clone, Default)]
176pub struct TableRowContent {
177 pub cells: Vec<TableCellContent>,
179 pub is_header: bool,
181 pub height: Option<f32>,
183 pub background: Option<(f32, f32, f32)>,
185}
186
187impl TableRowContent {
188 pub fn new(cells: Vec<TableCellContent>) -> Self {
190 Self {
191 cells,
192 ..Default::default()
193 }
194 }
195
196 pub fn header(cells: Vec<TableCellContent>) -> Self {
198 Self {
199 cells,
200 is_header: true,
201 ..Default::default()
202 }
203 }
204
205 pub fn add_cell(&mut self, cell: TableCellContent) {
207 self.cells.push(cell);
208 }
209}
210
211#[derive(Debug, Clone)]
213pub struct TableCellContent {
214 pub text: String,
216 pub bbox: Rect,
218 pub colspan: usize,
220 pub rowspan: usize,
222 pub align: TableCellAlign,
224 pub valign: TableCellVAlign,
226 pub is_header: bool,
228 pub background: Option<(f32, f32, f32)>,
230 pub font_size: Option<f32>,
232 pub font_name: Option<String>,
234 pub bold: bool,
236 pub italic: bool,
238}
239
240impl Default for TableCellContent {
241 fn default() -> Self {
242 Self {
243 text: String::new(),
244 bbox: Rect::new(0.0, 0.0, 0.0, 0.0),
245 colspan: 1,
246 rowspan: 1,
247 align: TableCellAlign::Left,
248 valign: TableCellVAlign::Top,
249 is_header: false,
250 background: None,
251 font_size: None,
252 font_name: None,
253 bold: false,
254 italic: false,
255 }
256 }
257}
258
259impl TableCellContent {
260 pub fn new(text: impl Into<String>) -> Self {
262 Self {
263 text: text.into(),
264 ..Default::default()
265 }
266 }
267
268 pub fn header(text: impl Into<String>) -> Self {
270 Self {
271 text: text.into(),
272 is_header: true,
273 bold: true,
274 ..Default::default()
275 }
276 }
277
278 pub fn with_bbox(mut self, bbox: Rect) -> Self {
280 self.bbox = bbox;
281 self
282 }
283
284 pub fn with_colspan(mut self, colspan: usize) -> Self {
286 self.colspan = colspan;
287 self
288 }
289
290 pub fn with_rowspan(mut self, rowspan: usize) -> Self {
292 self.rowspan = rowspan;
293 self
294 }
295
296 pub fn with_align(mut self, align: TableCellAlign) -> Self {
298 self.align = align;
299 self
300 }
301
302 pub fn with_valign(mut self, valign: TableCellVAlign) -> Self {
304 self.valign = valign;
305 self
306 }
307
308 pub fn with_background(mut self, r: f32, g: f32, b: f32) -> Self {
310 self.background = Some((r, g, b));
311 self
312 }
313
314 pub fn with_font_size(mut self, size: f32) -> Self {
316 self.font_size = Some(size);
317 self
318 }
319
320 pub fn with_bold(mut self, bold: bool) -> Self {
322 self.bold = bold;
323 self
324 }
325
326 pub fn with_italic(mut self, italic: bool) -> Self {
328 self.italic = italic;
329 self
330 }
331}
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
335pub enum TableCellAlign {
336 #[default]
338 Left,
339 Center,
341 Right,
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
347pub enum TableCellVAlign {
348 #[default]
350 Top,
351 Middle,
353 Bottom,
355}
356
357#[derive(Debug, Clone)]
359pub struct TableContentStyle {
360 pub border_width: f32,
362 pub border_color: (f32, f32, f32),
364 pub cell_padding: f32,
366 pub horizontal_borders: bool,
368 pub vertical_borders: bool,
370 pub outer_border: bool,
372 pub header_background: Option<(f32, f32, f32)>,
374 pub stripe_background: Option<(f32, f32, f32)>,
376}
377
378impl Default for TableContentStyle {
379 fn default() -> Self {
380 Self {
381 border_width: 0.5,
382 border_color: (0.0, 0.0, 0.0),
383 cell_padding: 4.0,
384 horizontal_borders: true,
385 vertical_borders: true,
386 outer_border: true,
387 header_background: None,
388 stripe_background: None,
389 }
390 }
391}
392
393impl TableContentStyle {
394 pub fn minimal() -> Self {
396 Self {
397 vertical_borders: false,
398 outer_border: false,
399 ..Default::default()
400 }
401 }
402
403 pub fn bordered() -> Self {
405 Self::default()
406 }
407
408 pub fn striped() -> Self {
410 Self {
411 stripe_background: Some((0.95, 0.95, 0.95)),
412 ..Default::default()
413 }
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn test_table_content_creation() {
423 let mut table = TableContent::new(Rect::new(0.0, 0.0, 400.0, 200.0));
424
425 let header = TableRowContent::header(vec![
426 TableCellContent::header("Name"),
427 TableCellContent::header("Value"),
428 ]);
429 table.add_row(header);
430
431 let row = TableRowContent::new(vec![TableCellContent::new("Item 1"), TableCellContent::new("100")]);
432 table.add_row(row);
433
434 assert_eq!(table.row_count(), 2);
435 assert_eq!(table.column_count(), 2);
436 assert!(table.has_header());
437 }
438
439 #[test]
440 fn test_cell_content_builder() {
441 let cell = TableCellContent::new("Test")
442 .with_colspan(2)
443 .with_rowspan(1)
444 .with_align(TableCellAlign::Center)
445 .with_valign(TableCellVAlign::Middle)
446 .with_background(0.9, 0.9, 0.9)
447 .with_font_size(12.0)
448 .with_bold(true);
449
450 assert_eq!(cell.text, "Test");
451 assert_eq!(cell.colspan, 2);
452 assert_eq!(cell.align, TableCellAlign::Center);
453 assert_eq!(cell.valign, TableCellVAlign::Middle);
454 assert!(cell.bold);
455 assert_eq!(cell.font_size, Some(12.0));
456 }
457
458 #[test]
459 fn test_table_get_cell() {
460 let mut table = TableContent::default();
461 table.add_row(TableRowContent::new(vec![
462 TableCellContent::new("A1"),
463 TableCellContent::new("B1"),
464 ]));
465 table.add_row(TableRowContent::new(vec![
466 TableCellContent::new("A2"),
467 TableCellContent::new("B2"),
468 ]));
469
470 assert_eq!(table.get_cell(0, 0).map(|c| c.text.as_str()), Some("A1"));
471 assert_eq!(table.get_cell(1, 1).map(|c| c.text.as_str()), Some("B2"));
472 assert!(table.get_cell(2, 0).is_none());
473 }
474
475 #[test]
476 fn test_table_style_presets() {
477 let minimal = TableContentStyle::minimal();
478 assert!(!minimal.vertical_borders);
479 assert!(!minimal.outer_border);
480
481 let bordered = TableContentStyle::bordered();
482 assert!(bordered.vertical_borders);
483 assert!(bordered.outer_border);
484
485 let striped = TableContentStyle::striped();
486 assert!(striped.stripe_background.is_some());
487 }
488}