1use crate::Result;
4use crate::error::TableError;
5use crate::font::FontMetrics;
6use crate::style::{CellStyle, RowStyle, TableStyle};
7use std::sync::Arc;
8use tracing::trace;
9
10#[derive(Debug, Clone, Copy, PartialEq, Default)]
12pub enum ImageFit {
13 #[default]
15 Contain,
16}
17
18#[derive(Debug, Clone)]
20pub struct ImageOverlay {
21 pub text: String,
23 pub font_size: f32,
25 pub bar_height: f32,
27 pub padding: f32,
29}
30
31impl ImageOverlay {
32 pub fn new(text: impl Into<String>) -> Self {
34 Self {
35 text: text.into(),
36 font_size: 8.0,
37 bar_height: 16.0,
38 padding: 4.0,
39 }
40 }
41}
42
43#[derive(Clone)]
49pub struct CellImage {
50 pub(crate) xobject: Arc<lopdf::Stream>,
52 pub(crate) width_px: u32,
54 pub(crate) height_px: u32,
56 pub(crate) max_render_height_pts: Option<f32>,
58 pub(crate) fit: ImageFit,
60 pub(crate) overlay: Option<ImageOverlay>,
62}
63
64impl std::fmt::Debug for CellImage {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.debug_struct("CellImage")
67 .field("width_px", &self.width_px)
68 .field("height_px", &self.height_px)
69 .field("max_render_height_pts", &self.max_render_height_pts)
70 .field("fit", &self.fit)
71 .field("overlay", &self.overlay)
72 .finish()
73 }
74}
75
76impl CellImage {
77 pub fn new(data: Vec<u8>) -> Result<Self> {
82 let stream = lopdf::xobject::image_from(data)
83 .map_err(|e| TableError::DrawingError(format!("Invalid image data: {e}")))?;
84
85 let width_px = stream
86 .dict
87 .get(b"Width")
88 .ok()
89 .and_then(|o| match o {
90 lopdf::Object::Integer(v) => Some(*v as u32),
91 _ => None,
92 })
93 .ok_or_else(|| TableError::DrawingError("Missing image Width".into()))?;
94
95 let height_px = stream
96 .dict
97 .get(b"Height")
98 .ok()
99 .and_then(|o| match o {
100 lopdf::Object::Integer(v) => Some(*v as u32),
101 _ => None,
102 })
103 .ok_or_else(|| TableError::DrawingError("Missing image Height".into()))?;
104
105 Ok(Self {
106 xobject: Arc::new(stream),
107 width_px,
108 height_px,
109 max_render_height_pts: None,
110 fit: ImageFit::default(),
111 overlay: None,
112 })
113 }
114
115 pub fn with_max_height(mut self, pts: f32) -> Self {
117 self.max_render_height_pts = Some(pts);
118 self
119 }
120
121 pub fn with_fit(mut self, fit: ImageFit) -> Self {
123 self.fit = fit;
124 self
125 }
126
127 pub fn with_overlay(mut self, overlay: ImageOverlay) -> Self {
129 self.overlay = Some(overlay);
130 self
131 }
132
133 pub fn width_px(&self) -> u32 {
135 self.width_px
136 }
137
138 pub fn height_px(&self) -> u32 {
140 self.height_px
141 }
142
143 pub fn aspect_ratio(&self) -> f32 {
145 self.width_px as f32 / self.height_px as f32
146 }
147}
148
149#[derive(Debug, Clone)]
151pub enum ColumnWidth {
152 Pixels(f32),
154 Percentage(f32),
156 Auto,
158}
159
160#[derive(Clone)]
162pub struct Table {
163 pub rows: Vec<Row>,
164 pub style: TableStyle,
165 pub column_widths: Option<Vec<ColumnWidth>>,
167 pub total_width: Option<f32>,
169 pub header_rows: usize,
171 pub font_metrics: Option<Arc<dyn FontMetrics>>,
174 pub bold_font_metrics: Option<Arc<dyn FontMetrics>>,
177}
178
179impl std::fmt::Debug for Table {
180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181 f.debug_struct("Table")
182 .field("rows", &self.rows)
183 .field("style", &self.style)
184 .field("column_widths", &self.column_widths)
185 .field("total_width", &self.total_width)
186 .field("header_rows", &self.header_rows)
187 .field("font_metrics", &self.font_metrics.as_ref().map(|_| "..."))
188 .field(
189 "bold_font_metrics",
190 &self.bold_font_metrics.as_ref().map(|_| "..."),
191 )
192 .finish()
193 }
194}
195
196impl Table {
197 pub fn new() -> Self {
199 Self {
200 rows: Vec::new(),
201 style: TableStyle::default(),
202 column_widths: None,
203 total_width: None,
204 header_rows: 0,
205 font_metrics: None,
206 bold_font_metrics: None,
207 }
208 }
209
210 pub fn add_row(mut self, row: Row) -> Self {
212 trace!("Adding row with {} cells", row.cells.len());
213 self.rows.push(row);
214 self
215 }
216
217 pub fn with_style(mut self, style: TableStyle) -> Self {
219 self.style = style;
220 self
221 }
222
223 pub fn with_column_widths(mut self, widths: Vec<ColumnWidth>) -> Self {
225 self.column_widths = Some(widths);
226 self
227 }
228
229 pub fn with_total_width(mut self, width: f32) -> Self {
231 self.total_width = Some(width);
232 self
233 }
234
235 pub fn with_pixel_widths(mut self, widths: Vec<f32>) -> Self {
237 self.column_widths = Some(widths.into_iter().map(ColumnWidth::Pixels).collect());
238 self
239 }
240
241 pub fn with_border(mut self, width: f32) -> Self {
243 self.style.border_width = width;
244 self
245 }
246
247 pub fn with_header_rows(mut self, count: usize) -> Self {
249 self.header_rows = count;
250 self
251 }
252
253 pub fn with_font_metrics(mut self, metrics: impl FontMetrics + 'static) -> Self {
259 self.font_metrics = Some(Arc::new(metrics));
260 self
261 }
262
263 pub fn with_bold_font_metrics(mut self, metrics: impl FontMetrics + 'static) -> Self {
269 self.bold_font_metrics = Some(Arc::new(metrics));
270 self
271 }
272
273 pub fn column_count(&self) -> usize {
275 self.rows
276 .first()
277 .map(|r| r.cells.iter().map(|c| c.colspan.max(1)).sum())
278 .unwrap_or(0)
279 }
280
281 pub fn validate(&self) -> Result<()> {
283 if self.rows.is_empty() {
284 return Err(crate::error::TableError::InvalidTable(
285 "Table has no rows".to_string(),
286 ));
287 }
288
289 let expected_cols = self.column_count();
290 for (i, row) in self.rows.iter().enumerate() {
291 let mut total_coverage = 0;
293 for cell in &row.cells {
294 total_coverage += cell.colspan.max(1);
295 }
296
297 if total_coverage != expected_cols {
298 return Err(crate::error::TableError::InvalidTable(format!(
299 "Row {} covers {} columns (with colspan), expected {}",
300 i, total_coverage, expected_cols
301 )));
302 }
303 }
304
305 if let Some(ref widths) = self.column_widths {
306 if widths.len() != expected_cols {
307 return Err(crate::error::TableError::InvalidTable(format!(
308 "Column widths array has {} elements, but table has {} columns",
309 widths.len(),
310 expected_cols
311 )));
312 }
313
314 let total_percentage: f32 = widths
316 .iter()
317 .filter_map(|w| match w {
318 ColumnWidth::Percentage(p) => Some(*p),
319 _ => None,
320 })
321 .sum();
322
323 if total_percentage > 100.0 {
324 return Err(crate::error::TableError::InvalidTable(format!(
325 "Total percentage widths ({:.1}%) exceed 100%",
326 total_percentage
327 )));
328 }
329 }
330
331 Ok(())
332 }
333}
334
335impl Default for Table {
336 fn default() -> Self {
337 Self::new()
338 }
339}
340
341#[derive(Debug, Clone)]
343pub struct Row {
344 pub cells: Vec<Cell>,
345 pub style: Option<RowStyle>,
346 pub height: Option<f32>,
348}
349
350impl Row {
351 pub fn new(cells: Vec<Cell>) -> Self {
353 Self {
354 cells,
355 style: None,
356 height: None,
357 }
358 }
359
360 pub fn with_style(mut self, style: RowStyle) -> Self {
362 self.style = Some(style);
363 self
364 }
365
366 pub fn with_height(mut self, height: f32) -> Self {
368 self.height = Some(height);
369 self
370 }
371}
372
373#[derive(Debug, Clone)]
375pub struct Cell {
376 pub content: String,
377 pub style: Option<CellStyle>,
378 pub colspan: usize,
379 pub rowspan: usize,
380 pub text_wrap: bool,
382 pub image: Option<CellImage>,
384}
385
386impl Cell {
387 pub fn new<S: Into<String>>(content: S) -> Self {
389 Self {
390 content: content.into(),
391 style: None,
392 colspan: 1,
393 rowspan: 1,
394 text_wrap: false,
395 image: None,
396 }
397 }
398
399 pub fn empty() -> Self {
401 Self::new("")
402 }
403
404 pub fn from_image(image: CellImage) -> Self {
406 Self {
407 content: String::new(),
408 style: None,
409 colspan: 1,
410 rowspan: 1,
411 text_wrap: false,
412 image: Some(image),
413 }
414 }
415
416 pub fn with_image(mut self, image: CellImage) -> Self {
418 self.image = Some(image);
419 self
420 }
421
422 pub fn with_wrap(mut self, wrap: bool) -> Self {
424 self.text_wrap = wrap;
425 self
426 }
427
428 pub fn with_style(mut self, style: CellStyle) -> Self {
430 self.style = Some(style);
431 self
432 }
433
434 pub fn with_colspan(mut self, span: usize) -> Self {
436 self.colspan = span.max(1);
437 self
438 }
439
440 pub fn with_rowspan(mut self, span: usize) -> Self {
442 self.rowspan = span.max(1);
443 self
444 }
445
446 pub fn bold(mut self) -> Self {
448 let mut style = self.style.unwrap_or_default();
449 style.bold = true;
450 self.style = Some(style);
451 self
452 }
453
454 pub fn italic(mut self) -> Self {
456 let mut style = self.style.unwrap_or_default();
457 style.italic = true;
458 self.style = Some(style);
459 self
460 }
461
462 pub fn with_font_size(mut self, size: f32) -> Self {
464 let mut style = self.style.unwrap_or_default();
465 style.font_size = Some(size);
466 self.style = Some(style);
467 self
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474
475 #[test]
476 fn test_table_validation() {
477 let mut table = Table::new();
478 assert!(table.validate().is_err());
479
480 table = table.add_row(Row::new(vec![Cell::new("A"), Cell::new("B")]));
481 assert!(table.validate().is_ok());
482
483 table = table.add_row(Row::new(vec![Cell::new("C")]));
484 assert!(table.validate().is_err());
485 }
486
487 #[test]
488 fn test_cell_builder() {
489 let cell = Cell::new("Test")
490 .bold()
491 .italic()
492 .with_font_size(14.0)
493 .with_colspan(2);
494
495 assert_eq!(cell.content, "Test");
496 assert_eq!(cell.colspan, 2);
497 let style = cell.style.unwrap();
498 assert!(style.bold);
499 assert!(style.italic);
500 assert_eq!(style.font_size, Some(14.0));
501 }
502
503 #[test]
504 fn test_cell_font_name() {
505 let style = CellStyle {
507 font_name: Some("Courier".to_string()),
508 ..Default::default()
509 };
510 let cell = Cell::new("Monospace text").with_style(style);
511
512 assert_eq!(cell.content, "Monospace text");
513 let cell_style = cell.style.unwrap();
514 assert_eq!(cell_style.font_name, Some("Courier".to_string()));
515
516 let cell_default = Cell::new("Default font");
518 assert!(cell_default.style.is_none());
519 }
520
521 #[test]
522 fn test_with_bold_font_metrics_builder() {
523 struct DummyMetrics;
524
525 impl crate::font::FontMetrics for DummyMetrics {
526 fn char_width(&self, _ch: char, _font_size: f32) -> f32 {
527 5.0
528 }
529
530 fn text_width(&self, text: &str, _font_size: f32) -> f32 {
531 text.chars().count() as f32 * 5.0
532 }
533
534 fn encode_text(&self, text: &str) -> Vec<u8> {
535 vec![0; text.chars().count() * 2]
536 }
537 }
538
539 let table = Table::new().with_bold_font_metrics(DummyMetrics);
540 assert!(table.bold_font_metrics.is_some());
541 }
542}