Skip to main content

lopdf_table/
table.rs

1//! Core table structures
2
3use 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/// Image fit mode for cell rendering
11#[derive(Debug, Clone, Copy, PartialEq, Default)]
12pub enum ImageFit {
13    /// Scale to fit within cell bounds preserving aspect ratio
14    #[default]
15    Contain,
16}
17
18/// Text overlay drawn on top of a cell image (semi-transparent bar with white text).
19#[derive(Debug, Clone)]
20pub struct ImageOverlay {
21    /// Text to display in the overlay bar
22    pub text: String,
23    /// Font size for the overlay text (default: 8.0)
24    pub font_size: f32,
25    /// Height of the semi-transparent background bar (default: 16.0)
26    pub bar_height: f32,
27    /// Horizontal padding inside the bar (default: 4.0)
28    pub padding: f32,
29}
30
31impl ImageOverlay {
32    /// Create a new overlay with the given text and sensible defaults.
33    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/// Image payload for embedding in a table cell.
44///
45/// Constructed from raw JPEG or PNG bytes. The image is validated and
46/// converted to a PDF XObject stream at construction time. Cheap to
47/// clone via internal `Arc`.
48#[derive(Clone)]
49pub struct CellImage {
50    /// Pre-built XObject stream ready for PDF embedding
51    pub(crate) xobject: Arc<lopdf::Stream>,
52    /// Intrinsic width in pixels
53    pub(crate) width_px: u32,
54    /// Intrinsic height in pixels
55    pub(crate) height_px: u32,
56    /// Maximum rendered height in points (caps row height contribution)
57    pub(crate) max_render_height_pts: Option<f32>,
58    /// Fit mode
59    pub(crate) fit: ImageFit,
60    /// Optional text overlay drawn on top of the image
61    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    /// Create a new image from raw JPEG or PNG bytes.
78    ///
79    /// Validates the image and pre-builds the PDF XObject stream.
80    /// Returns an error if the bytes are not a valid/supported image.
81    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    /// Set maximum rendered height in points.
116    pub fn with_max_height(mut self, pts: f32) -> Self {
117        self.max_render_height_pts = Some(pts);
118        self
119    }
120
121    /// Set the image fit mode.
122    pub fn with_fit(mut self, fit: ImageFit) -> Self {
123        self.fit = fit;
124        self
125    }
126
127    /// Attach a text overlay to this image.
128    pub fn with_overlay(mut self, overlay: ImageOverlay) -> Self {
129        self.overlay = Some(overlay);
130        self
131    }
132
133    /// Intrinsic pixel width.
134    pub fn width_px(&self) -> u32 {
135        self.width_px
136    }
137
138    /// Intrinsic pixel height.
139    pub fn height_px(&self) -> u32 {
140        self.height_px
141    }
142
143    /// Aspect ratio (width / height).
144    pub fn aspect_ratio(&self) -> f32 {
145        self.width_px as f32 / self.height_px as f32
146    }
147}
148
149/// Column width specification
150#[derive(Debug, Clone)]
151pub enum ColumnWidth {
152    /// Fixed width in points
153    Pixels(f32),
154    /// Percentage of available table width
155    Percentage(f32),
156    /// Automatically calculate based on content
157    Auto,
158}
159
160/// Represents a table with rows and styling
161#[derive(Clone)]
162pub struct Table {
163    pub rows: Vec<Row>,
164    pub style: TableStyle,
165    /// Column width specifications
166    pub column_widths: Option<Vec<ColumnWidth>>,
167    /// Total table width (if None, auto-calculate based on content)
168    pub total_width: Option<f32>,
169    /// Number of header rows to repeat on each page when paginating
170    pub header_rows: usize,
171    /// Font metrics for accurate text measurement and Unicode encoding.
172    /// When set, enables font-aware text wrapping and glyph ID encoding.
173    pub font_metrics: Option<Arc<dyn FontMetrics>>,
174    /// Bold font metrics for accurate bold text measurement and Unicode encoding.
175    /// When set, bold cells can use a dedicated embedded bold font.
176    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    /// Create a new empty table
198    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    /// Add a row to the table
211    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    /// Set the table style
218    pub fn with_style(mut self, style: TableStyle) -> Self {
219        self.style = style;
220        self
221    }
222
223    /// Set column width specifications
224    pub fn with_column_widths(mut self, widths: Vec<ColumnWidth>) -> Self {
225        self.column_widths = Some(widths);
226        self
227    }
228
229    /// Set total table width
230    pub fn with_total_width(mut self, width: f32) -> Self {
231        self.total_width = Some(width);
232        self
233    }
234
235    /// Convenience method to set pixel widths for all columns
236    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    /// Set border width for the entire table
242    pub fn with_border(mut self, width: f32) -> Self {
243        self.style.border_width = width;
244        self
245    }
246
247    /// Set the number of header rows to repeat on each page
248    pub fn with_header_rows(mut self, count: usize) -> Self {
249        self.header_rows = count;
250        self
251    }
252
253    /// Set font metrics for accurate text measurement and Unicode encoding.
254    ///
255    /// When font metrics are provided along with `embedded_font_resource_name`
256    /// on the table style, text will be encoded as glyph IDs and measured
257    /// using the actual font data instead of heuristic estimates.
258    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    /// Set bold font metrics for accurate bold text measurement and Unicode encoding.
264    ///
265    /// When font metrics are provided along with `embedded_font_resource_name_bold`
266    /// on the table style, bold cell text will be encoded as glyph IDs and measured
267    /// using the bold font data.
268    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    /// Get the number of columns (based on the first row, accounting for colspan)
274    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    /// Validate table structure
282    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            // Calculate the total column coverage including colspan
292            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            // Check that percentage widths don't exceed 100%
315            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/// Represents a row in a table
342#[derive(Debug, Clone)]
343pub struct Row {
344    pub cells: Vec<Cell>,
345    pub style: Option<RowStyle>,
346    /// Explicit height (if None, auto-calculate)
347    pub height: Option<f32>,
348}
349
350impl Row {
351    /// Create a new row with cells
352    pub fn new(cells: Vec<Cell>) -> Self {
353        Self {
354            cells,
355            style: None,
356            height: None,
357        }
358    }
359
360    /// Set row style
361    pub fn with_style(mut self, style: RowStyle) -> Self {
362        self.style = Some(style);
363        self
364    }
365
366    /// Set explicit row height
367    pub fn with_height(mut self, height: f32) -> Self {
368        self.height = Some(height);
369        self
370    }
371}
372
373/// Represents a cell in a table
374#[derive(Debug, Clone)]
375pub struct Cell {
376    pub content: String,
377    pub style: Option<CellStyle>,
378    pub colspan: usize,
379    pub rowspan: usize,
380    /// Enable text wrapping for this cell
381    pub text_wrap: bool,
382    /// Image payloads for this cell (rendered side-by-side when multiple).
383    pub images: Vec<CellImage>,
384}
385
386impl Cell {
387    /// Create a new cell with text content
388    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    /// Create an empty cell
400    pub fn empty() -> Self {
401        Self::new("")
402    }
403
404    /// Create a cell containing a single image (with empty text).
405    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    /// Create a cell containing multiple images rendered side-by-side (with empty text).
417    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    /// Set a single image payload for this cell (replaces any existing images).
429    pub fn with_image(mut self, image: CellImage) -> Self {
430        self.images = vec![image];
431        self
432    }
433
434    /// Append an additional image to this cell.
435    pub fn add_image(mut self, image: CellImage) -> Self {
436        self.images.push(image);
437        self
438    }
439
440    /// Enable text wrapping for this cell
441    pub fn with_wrap(mut self, wrap: bool) -> Self {
442        self.text_wrap = wrap;
443        self
444    }
445
446    /// Set cell style
447    pub fn with_style(mut self, style: CellStyle) -> Self {
448        self.style = Some(style);
449        self
450    }
451
452    /// Set colspan
453    pub fn with_colspan(mut self, span: usize) -> Self {
454        self.colspan = span.max(1);
455        self
456    }
457
458    /// Set rowspan
459    pub fn with_rowspan(mut self, span: usize) -> Self {
460        self.rowspan = span.max(1);
461        self
462    }
463
464    /// Make text bold
465    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    /// Make text italic
473    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    /// Set font size
481    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        // Test with custom font
524        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        // Test with default (no font specified)
535        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}