use crate::geometry::Rect;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TableSource {
StructureTree,
#[default]
SpatialDetection,
UserGenerated,
}
#[derive(Debug, Clone, Default)]
pub struct TableDetectionInfo {
pub source: TableSource,
pub confidence: f32,
pub detection_method: Option<String>,
}
impl TableDetectionInfo {
pub fn from_structure_tree() -> Self {
Self {
source: TableSource::StructureTree,
confidence: 1.0,
detection_method: Some("structure_tree".to_string()),
}
}
pub fn from_spatial_detection(confidence: f32, method: impl Into<String>) -> Self {
Self {
source: TableSource::SpatialDetection,
confidence: confidence.clamp(0.0, 1.0),
detection_method: Some(method.into()),
}
}
pub fn user_generated() -> Self {
Self {
source: TableSource::UserGenerated,
confidence: 1.0,
detection_method: None,
}
}
}
#[derive(Debug, Clone)]
pub struct TableContent {
pub bbox: Rect,
pub rows: Vec<TableRowContent>,
pub column_widths: Vec<f32>,
pub reading_order: Option<usize>,
pub caption: Option<String>,
pub style: TableContentStyle,
pub detection_info: TableDetectionInfo,
}
impl Default for TableContent {
fn default() -> Self {
Self {
bbox: Rect::new(0.0, 0.0, 0.0, 0.0),
rows: Vec::new(),
column_widths: Vec::new(),
reading_order: None,
caption: None,
style: TableContentStyle::default(),
detection_info: TableDetectionInfo::default(),
}
}
}
impl TableContent {
pub fn new(bbox: Rect) -> Self {
Self {
bbox,
..Default::default()
}
}
pub fn add_row(&mut self, row: TableRowContent) {
self.rows.push(row);
}
pub fn row_count(&self) -> usize {
self.rows.len()
}
pub fn column_count(&self) -> usize {
self.rows.first().map(|r| r.cells.len()).unwrap_or(0)
}
pub fn get_cell(&self, row: usize, col: usize) -> Option<&TableCellContent> {
self.rows.get(row).and_then(|r| r.cells.get(col))
}
pub fn has_header(&self) -> bool {
self.rows.first().is_some_and(|r| r.is_header)
}
pub fn with_detection_info(mut self, info: TableDetectionInfo) -> Self {
self.detection_info = info;
self
}
pub fn user_generated(bbox: Rect) -> Self {
Self {
bbox,
detection_info: TableDetectionInfo::user_generated(),
..Default::default()
}
}
pub fn is_from_structure_tree(&self) -> bool {
self.detection_info.source == TableSource::StructureTree
}
pub fn detection_confidence(&self) -> f32 {
self.detection_info.confidence
}
}
#[derive(Debug, Clone, Default)]
pub struct TableRowContent {
pub cells: Vec<TableCellContent>,
pub is_header: bool,
pub height: Option<f32>,
pub background: Option<(f32, f32, f32)>,
}
impl TableRowContent {
pub fn new(cells: Vec<TableCellContent>) -> Self {
Self {
cells,
..Default::default()
}
}
pub fn header(cells: Vec<TableCellContent>) -> Self {
Self {
cells,
is_header: true,
..Default::default()
}
}
pub fn add_cell(&mut self, cell: TableCellContent) {
self.cells.push(cell);
}
}
#[derive(Debug, Clone)]
pub struct TableCellContent {
pub text: String,
pub bbox: Rect,
pub colspan: usize,
pub rowspan: usize,
pub align: TableCellAlign,
pub valign: TableCellVAlign,
pub is_header: bool,
pub background: Option<(f32, f32, f32)>,
pub font_size: Option<f32>,
pub font_name: Option<String>,
pub bold: bool,
pub italic: bool,
}
impl Default for TableCellContent {
fn default() -> Self {
Self {
text: String::new(),
bbox: Rect::new(0.0, 0.0, 0.0, 0.0),
colspan: 1,
rowspan: 1,
align: TableCellAlign::Left,
valign: TableCellVAlign::Top,
is_header: false,
background: None,
font_size: None,
font_name: None,
bold: false,
italic: false,
}
}
}
impl TableCellContent {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
..Default::default()
}
}
pub fn header(text: impl Into<String>) -> Self {
Self {
text: text.into(),
is_header: true,
bold: true,
..Default::default()
}
}
pub fn with_bbox(mut self, bbox: Rect) -> Self {
self.bbox = bbox;
self
}
pub fn with_colspan(mut self, colspan: usize) -> Self {
self.colspan = colspan;
self
}
pub fn with_rowspan(mut self, rowspan: usize) -> Self {
self.rowspan = rowspan;
self
}
pub fn with_align(mut self, align: TableCellAlign) -> Self {
self.align = align;
self
}
pub fn with_valign(mut self, valign: TableCellVAlign) -> Self {
self.valign = valign;
self
}
pub fn with_background(mut self, r: f32, g: f32, b: f32) -> Self {
self.background = Some((r, g, b));
self
}
pub fn with_font_size(mut self, size: f32) -> Self {
self.font_size = Some(size);
self
}
pub fn with_bold(mut self, bold: bool) -> Self {
self.bold = bold;
self
}
pub fn with_italic(mut self, italic: bool) -> Self {
self.italic = italic;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TableCellAlign {
#[default]
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TableCellVAlign {
#[default]
Top,
Middle,
Bottom,
}
#[derive(Debug, Clone)]
pub struct TableContentStyle {
pub border_width: f32,
pub border_color: (f32, f32, f32),
pub cell_padding: f32,
pub horizontal_borders: bool,
pub vertical_borders: bool,
pub outer_border: bool,
pub header_background: Option<(f32, f32, f32)>,
pub stripe_background: Option<(f32, f32, f32)>,
}
impl Default for TableContentStyle {
fn default() -> Self {
Self {
border_width: 0.5,
border_color: (0.0, 0.0, 0.0),
cell_padding: 4.0,
horizontal_borders: true,
vertical_borders: true,
outer_border: true,
header_background: None,
stripe_background: None,
}
}
}
impl TableContentStyle {
pub fn minimal() -> Self {
Self {
vertical_borders: false,
outer_border: false,
..Default::default()
}
}
pub fn bordered() -> Self {
Self::default()
}
pub fn striped() -> Self {
Self {
stripe_background: Some((0.95, 0.95, 0.95)),
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_table_content_creation() {
let mut table = TableContent::new(Rect::new(0.0, 0.0, 400.0, 200.0));
let header = TableRowContent::header(vec![
TableCellContent::header("Name"),
TableCellContent::header("Value"),
]);
table.add_row(header);
let row = TableRowContent::new(vec![
TableCellContent::new("Item 1"),
TableCellContent::new("100"),
]);
table.add_row(row);
assert_eq!(table.row_count(), 2);
assert_eq!(table.column_count(), 2);
assert!(table.has_header());
}
#[test]
fn test_cell_content_builder() {
let cell = TableCellContent::new("Test")
.with_colspan(2)
.with_rowspan(1)
.with_align(TableCellAlign::Center)
.with_valign(TableCellVAlign::Middle)
.with_background(0.9, 0.9, 0.9)
.with_font_size(12.0)
.with_bold(true);
assert_eq!(cell.text, "Test");
assert_eq!(cell.colspan, 2);
assert_eq!(cell.align, TableCellAlign::Center);
assert_eq!(cell.valign, TableCellVAlign::Middle);
assert!(cell.bold);
assert_eq!(cell.font_size, Some(12.0));
}
#[test]
fn test_table_get_cell() {
let mut table = TableContent::default();
table.add_row(TableRowContent::new(vec![
TableCellContent::new("A1"),
TableCellContent::new("B1"),
]));
table.add_row(TableRowContent::new(vec![
TableCellContent::new("A2"),
TableCellContent::new("B2"),
]));
assert_eq!(table.get_cell(0, 0).map(|c| c.text.as_str()), Some("A1"));
assert_eq!(table.get_cell(1, 1).map(|c| c.text.as_str()), Some("B2"));
assert!(table.get_cell(2, 0).is_none());
}
#[test]
fn test_table_style_presets() {
let minimal = TableContentStyle::minimal();
assert!(!minimal.vertical_borders);
assert!(!minimal.outer_border);
let bordered = TableContentStyle::bordered();
assert!(bordered.vertical_borders);
assert!(bordered.outer_border);
let striped = TableContentStyle::striped();
assert!(striped.stripe_background.is_some());
}
}