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 images: Vec<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 images: Vec::new(),
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 images: vec![image],
413 }
414 }
415
416 pub fn from_images(images: Vec<CellImage>) -> Self {
418 Self {
419 content: String::new(),
420 style: None,
421 colspan: 1,
422 rowspan: 1,
423 text_wrap: false,
424 images,
425 }
426 }
427
428 pub fn with_image(mut self, image: CellImage) -> Self {
430 self.images = vec![image];
431 self
432 }
433
434 pub fn add_image(mut self, image: CellImage) -> Self {
436 self.images.push(image);
437 self
438 }
439
440 pub fn with_wrap(mut self, wrap: bool) -> Self {
442 self.text_wrap = wrap;
443 self
444 }
445
446 pub fn with_style(mut self, style: CellStyle) -> Self {
448 self.style = Some(style);
449 self
450 }
451
452 pub fn with_colspan(mut self, span: usize) -> Self {
454 self.colspan = span.max(1);
455 self
456 }
457
458 pub fn with_rowspan(mut self, span: usize) -> Self {
460 self.rowspan = span.max(1);
461 self
462 }
463
464 pub fn bold(mut self) -> Self {
466 let mut style = self.style.unwrap_or_default();
467 style.bold = true;
468 self.style = Some(style);
469 self
470 }
471
472 pub fn italic(mut self) -> Self {
474 let mut style = self.style.unwrap_or_default();
475 style.italic = true;
476 self.style = Some(style);
477 self
478 }
479
480 pub fn with_font_size(mut self, size: f32) -> Self {
482 let mut style = self.style.unwrap_or_default();
483 style.font_size = Some(size);
484 self.style = Some(style);
485 self
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492
493 #[test]
494 fn test_table_validation() {
495 let mut table = Table::new();
496 assert!(table.validate().is_err());
497
498 table = table.add_row(Row::new(vec![Cell::new("A"), Cell::new("B")]));
499 assert!(table.validate().is_ok());
500
501 table = table.add_row(Row::new(vec![Cell::new("C")]));
502 assert!(table.validate().is_err());
503 }
504
505 #[test]
506 fn test_cell_builder() {
507 let cell = Cell::new("Test")
508 .bold()
509 .italic()
510 .with_font_size(14.0)
511 .with_colspan(2);
512
513 assert_eq!(cell.content, "Test");
514 assert_eq!(cell.colspan, 2);
515 let style = cell.style.unwrap();
516 assert!(style.bold);
517 assert!(style.italic);
518 assert_eq!(style.font_size, Some(14.0));
519 }
520
521 #[test]
522 fn test_cell_font_name() {
523 let style = CellStyle {
525 font_name: Some("Courier".to_string()),
526 ..Default::default()
527 };
528 let cell = Cell::new("Monospace text").with_style(style);
529
530 assert_eq!(cell.content, "Monospace text");
531 let cell_style = cell.style.unwrap();
532 assert_eq!(cell_style.font_name, Some("Courier".to_string()));
533
534 let cell_default = Cell::new("Default font");
536 assert!(cell_default.style.is_none());
537 }
538
539 #[test]
540 fn test_with_bold_font_metrics_builder() {
541 struct DummyMetrics;
542
543 impl crate::font::FontMetrics for DummyMetrics {
544 fn char_width(&self, _ch: char, _font_size: f32) -> f32 {
545 5.0
546 }
547
548 fn text_width(&self, text: &str, _font_size: f32) -> f32 {
549 text.chars().count() as f32 * 5.0
550 }
551
552 fn encode_text(&self, text: &str) -> Vec<u8> {
553 vec![0; text.chars().count() * 2]
554 }
555 }
556
557 let table = Table::new().with_bold_font_metrics(DummyMetrics);
558 assert!(table.bold_font_metrics.is_some());
559 }
560}