use crate::Result;
use crate::error::TableError;
use crate::font::FontMetrics;
use crate::style::{CellStyle, RowStyle, TableStyle};
use std::sync::Arc;
use tracing::trace;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum ImageFit {
#[default]
Contain,
}
#[derive(Debug, Clone)]
pub struct ImageOverlay {
pub text: String,
pub font_size: f32,
pub bar_height: f32,
pub padding: f32,
}
impl ImageOverlay {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
font_size: 8.0,
bar_height: 16.0,
padding: 4.0,
}
}
}
#[derive(Clone)]
pub struct CellImage {
pub(crate) xobject: Arc<lopdf::Stream>,
pub(crate) width_px: u32,
pub(crate) height_px: u32,
pub(crate) max_render_height_pts: Option<f32>,
pub(crate) fit: ImageFit,
pub(crate) overlay: Option<ImageOverlay>,
}
impl std::fmt::Debug for CellImage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CellImage")
.field("width_px", &self.width_px)
.field("height_px", &self.height_px)
.field("max_render_height_pts", &self.max_render_height_pts)
.field("fit", &self.fit)
.field("overlay", &self.overlay)
.finish()
}
}
impl CellImage {
pub fn new(data: Vec<u8>) -> Result<Self> {
let stream = lopdf::xobject::image_from(data)
.map_err(|e| TableError::DrawingError(format!("Invalid image data: {e}")))?;
let width_px = stream
.dict
.get(b"Width")
.ok()
.and_then(|o| match o {
lopdf::Object::Integer(v) => Some(*v as u32),
_ => None,
})
.ok_or_else(|| TableError::DrawingError("Missing image Width".into()))?;
let height_px = stream
.dict
.get(b"Height")
.ok()
.and_then(|o| match o {
lopdf::Object::Integer(v) => Some(*v as u32),
_ => None,
})
.ok_or_else(|| TableError::DrawingError("Missing image Height".into()))?;
Ok(Self {
xobject: Arc::new(stream),
width_px,
height_px,
max_render_height_pts: None,
fit: ImageFit::default(),
overlay: None,
})
}
pub fn with_max_height(mut self, pts: f32) -> Self {
self.max_render_height_pts = Some(pts);
self
}
pub fn with_fit(mut self, fit: ImageFit) -> Self {
self.fit = fit;
self
}
pub fn with_overlay(mut self, overlay: ImageOverlay) -> Self {
self.overlay = Some(overlay);
self
}
pub fn width_px(&self) -> u32 {
self.width_px
}
pub fn height_px(&self) -> u32 {
self.height_px
}
pub fn aspect_ratio(&self) -> f32 {
self.width_px as f32 / self.height_px as f32
}
}
#[derive(Debug, Clone)]
pub enum ColumnWidth {
Pixels(f32),
Percentage(f32),
Auto,
}
#[derive(Clone)]
pub struct Table {
pub rows: Vec<Row>,
pub style: TableStyle,
pub column_widths: Option<Vec<ColumnWidth>>,
pub total_width: Option<f32>,
pub header_rows: usize,
pub font_metrics: Option<Arc<dyn FontMetrics>>,
pub bold_font_metrics: Option<Arc<dyn FontMetrics>>,
}
impl std::fmt::Debug for Table {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Table")
.field("rows", &self.rows)
.field("style", &self.style)
.field("column_widths", &self.column_widths)
.field("total_width", &self.total_width)
.field("header_rows", &self.header_rows)
.field("font_metrics", &self.font_metrics.as_ref().map(|_| "..."))
.field(
"bold_font_metrics",
&self.bold_font_metrics.as_ref().map(|_| "..."),
)
.finish()
}
}
impl Table {
pub fn new() -> Self {
Self {
rows: Vec::new(),
style: TableStyle::default(),
column_widths: None,
total_width: None,
header_rows: 0,
font_metrics: None,
bold_font_metrics: None,
}
}
pub fn add_row(mut self, row: Row) -> Self {
trace!("Adding row with {} cells", row.cells.len());
self.rows.push(row);
self
}
pub fn with_style(mut self, style: TableStyle) -> Self {
self.style = style;
self
}
pub fn with_column_widths(mut self, widths: Vec<ColumnWidth>) -> Self {
self.column_widths = Some(widths);
self
}
pub fn with_total_width(mut self, width: f32) -> Self {
self.total_width = Some(width);
self
}
pub fn with_pixel_widths(mut self, widths: Vec<f32>) -> Self {
self.column_widths = Some(widths.into_iter().map(ColumnWidth::Pixels).collect());
self
}
pub fn with_border(mut self, width: f32) -> Self {
self.style.border_width = width;
self
}
pub fn with_header_rows(mut self, count: usize) -> Self {
self.header_rows = count;
self
}
pub fn with_font_metrics(mut self, metrics: impl FontMetrics + 'static) -> Self {
self.font_metrics = Some(Arc::new(metrics));
self
}
pub fn with_bold_font_metrics(mut self, metrics: impl FontMetrics + 'static) -> Self {
self.bold_font_metrics = Some(Arc::new(metrics));
self
}
pub fn column_count(&self) -> usize {
self.rows
.first()
.map(|r| r.cells.iter().map(|c| c.colspan.max(1)).sum())
.unwrap_or(0)
}
pub fn validate(&self) -> Result<()> {
if self.rows.is_empty() {
return Err(crate::error::TableError::InvalidTable(
"Table has no rows".to_string(),
));
}
let expected_cols = self.column_count();
for (i, row) in self.rows.iter().enumerate() {
let mut total_coverage = 0;
for cell in &row.cells {
total_coverage += cell.colspan.max(1);
}
if total_coverage != expected_cols {
return Err(crate::error::TableError::InvalidTable(format!(
"Row {} covers {} columns (with colspan), expected {}",
i, total_coverage, expected_cols
)));
}
}
if let Some(ref widths) = self.column_widths {
if widths.len() != expected_cols {
return Err(crate::error::TableError::InvalidTable(format!(
"Column widths array has {} elements, but table has {} columns",
widths.len(),
expected_cols
)));
}
let total_percentage: f32 = widths
.iter()
.filter_map(|w| match w {
ColumnWidth::Percentage(p) => Some(*p),
_ => None,
})
.sum();
if total_percentage > 100.0 {
return Err(crate::error::TableError::InvalidTable(format!(
"Total percentage widths ({:.1}%) exceed 100%",
total_percentage
)));
}
}
Ok(())
}
}
impl Default for Table {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct Row {
pub cells: Vec<Cell>,
pub style: Option<RowStyle>,
pub height: Option<f32>,
}
impl Row {
pub fn new(cells: Vec<Cell>) -> Self {
Self {
cells,
style: None,
height: None,
}
}
pub fn with_style(mut self, style: RowStyle) -> Self {
self.style = Some(style);
self
}
pub fn with_height(mut self, height: f32) -> Self {
self.height = Some(height);
self
}
}
#[derive(Debug, Clone)]
pub struct Cell {
pub content: String,
pub style: Option<CellStyle>,
pub colspan: usize,
pub rowspan: usize,
pub text_wrap: bool,
pub images: Vec<CellImage>,
}
impl Cell {
pub fn new<S: Into<String>>(content: S) -> Self {
Self {
content: content.into(),
style: None,
colspan: 1,
rowspan: 1,
text_wrap: false,
images: Vec::new(),
}
}
pub fn empty() -> Self {
Self::new("")
}
pub fn from_image(image: CellImage) -> Self {
Self {
content: String::new(),
style: None,
colspan: 1,
rowspan: 1,
text_wrap: false,
images: vec![image],
}
}
pub fn from_images(images: Vec<CellImage>) -> Self {
Self {
content: String::new(),
style: None,
colspan: 1,
rowspan: 1,
text_wrap: false,
images,
}
}
pub fn with_image(mut self, image: CellImage) -> Self {
self.images = vec![image];
self
}
pub fn add_image(mut self, image: CellImage) -> Self {
self.images.push(image);
self
}
pub fn with_wrap(mut self, wrap: bool) -> Self {
self.text_wrap = wrap;
self
}
pub fn with_style(mut self, style: CellStyle) -> Self {
self.style = Some(style);
self
}
pub fn with_colspan(mut self, span: usize) -> Self {
self.colspan = span.max(1);
self
}
pub fn with_rowspan(mut self, span: usize) -> Self {
self.rowspan = span.max(1);
self
}
pub fn bold(mut self) -> Self {
let mut style = self.style.unwrap_or_default();
style.bold = true;
self.style = Some(style);
self
}
pub fn italic(mut self) -> Self {
let mut style = self.style.unwrap_or_default();
style.italic = true;
self.style = Some(style);
self
}
pub fn with_font_size(mut self, size: f32) -> Self {
let mut style = self.style.unwrap_or_default();
style.font_size = Some(size);
self.style = Some(style);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_table_validation() {
let mut table = Table::new();
assert!(table.validate().is_err());
table = table.add_row(Row::new(vec![Cell::new("A"), Cell::new("B")]));
assert!(table.validate().is_ok());
table = table.add_row(Row::new(vec![Cell::new("C")]));
assert!(table.validate().is_err());
}
#[test]
fn test_cell_builder() {
let cell = Cell::new("Test")
.bold()
.italic()
.with_font_size(14.0)
.with_colspan(2);
assert_eq!(cell.content, "Test");
assert_eq!(cell.colspan, 2);
let style = cell.style.unwrap();
assert!(style.bold);
assert!(style.italic);
assert_eq!(style.font_size, Some(14.0));
}
#[test]
fn test_cell_font_name() {
let style = CellStyle {
font_name: Some("Courier".to_string()),
..Default::default()
};
let cell = Cell::new("Monospace text").with_style(style);
assert_eq!(cell.content, "Monospace text");
let cell_style = cell.style.unwrap();
assert_eq!(cell_style.font_name, Some("Courier".to_string()));
let cell_default = Cell::new("Default font");
assert!(cell_default.style.is_none());
}
#[test]
fn test_with_bold_font_metrics_builder() {
struct DummyMetrics;
impl crate::font::FontMetrics for DummyMetrics {
fn char_width(&self, _ch: char, _font_size: f32) -> f32 {
5.0
}
fn text_width(&self, text: &str, _font_size: f32) -> f32 {
text.chars().count() as f32 * 5.0
}
fn encode_text(&self, text: &str) -> Vec<u8> {
vec![0; text.chars().count() * 2]
}
}
let table = Table::new().with_bold_font_metrics(DummyMetrics);
assert!(table.bold_font_metrics.is_some());
}
}