use oxidize_pdf::advanced_tables::{CellData, RowData};
use oxidize_pdf::text::metrics::{
get_custom_font_metrics, measure_text, register_custom_font_metrics, FontMetrics,
};
use oxidize_pdf::text::{HeaderStyle, Table, TableCell, TableOptions, TextAlign};
use oxidize_pdf::writer::WriterConfig;
use oxidize_pdf::{Color, Document, Font, Page, Result};
use std::fs;
use tempfile::TempDir;
#[test]
fn test_simple_table() -> Result<()> {
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("simple_table.pdf");
let mut doc = Document::new();
doc.set_title("Simple Table Test");
let mut page = Page::a4();
let mut table = Table::new(vec![150.0, 200.0, 150.0]);
table.set_position(50.0, 700.0);
table.add_header_row(vec![
"Product".to_string(),
"Description".to_string(),
"Price".to_string(),
])?;
table.add_row(vec![
"Widget A".to_string(),
"High-quality widget for everyday use".to_string(),
"$19.99".to_string(),
])?;
table.add_row(vec![
"Widget B".to_string(),
"Premium widget with advanced features".to_string(),
"$39.99".to_string(),
])?;
table.add_row(vec![
"Widget C".to_string(),
"Budget-friendly widget option".to_string(),
"$9.99".to_string(),
])?;
page.add_table(&table)?;
doc.add_page(page);
doc.save(&file_path)?;
assert!(file_path.exists());
let file_size = fs::metadata(&file_path)?.len();
assert!(file_size > 1000);
Ok(())
}
#[test]
fn test_table_with_custom_options() -> Result<()> {
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("custom_table.pdf");
let mut doc = Document::new();
doc.set_title("Custom Table Test");
let mut page = Page::a4();
let mut table = Table::new(vec![100.0, 150.0, 100.0, 100.0]);
table.set_position(50.0, 650.0);
let mut options = TableOptions::default();
options.border_width = 2.0;
options.border_color = Color::rgb(0.2, 0.3, 0.5);
options.cell_padding = 8.0;
options.font = Font::TimesRoman;
options.font_size = 11.0;
options.text_color = Color::rgb(0.1, 0.1, 0.1);
options.header_style = Some(HeaderStyle {
background_color: Color::rgb(0.9, 0.9, 0.95),
text_color: Color::rgb(0.0, 0.0, 0.5),
font: Font::TimesBold,
bold: true,
});
table.set_options(options);
table.add_header_row(vec![
"ID".to_string(),
"Name".to_string(),
"Status".to_string(),
"Score".to_string(),
])?;
table.add_row(vec![
"001".to_string(),
"Alice Johnson".to_string(),
"Active".to_string(),
"95".to_string(),
])?;
table.add_row(vec![
"002".to_string(),
"Bob Smith".to_string(),
"Pending".to_string(),
"87".to_string(),
])?;
page.add_table(&table)?;
doc.add_page(page);
doc.save(&file_path)?;
assert!(file_path.exists());
Ok(())
}
#[test]
fn test_table_alignment() -> Result<()> {
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("aligned_table.pdf");
let mut doc = Document::new();
doc.set_title("Table Alignment Test");
let mut page = Page::a4();
let mut table = Table::new(vec![120.0, 180.0, 120.0]);
table.set_position(50.0, 700.0);
table.add_header_row(vec![
"Left".to_string(),
"Center".to_string(),
"Right".to_string(),
])?;
table.add_row_with_alignment(
vec![
"Left text".to_string(),
"Center text".to_string(),
"Right text".to_string(),
],
TextAlign::Left,
)?;
let cells = vec![
TableCell::with_align("Left aligned".to_string(), TextAlign::Left),
TableCell::with_align("Center aligned".to_string(), TextAlign::Center),
TableCell::with_align("Right aligned".to_string(), TextAlign::Right),
];
table.add_custom_row(cells)?;
page.add_table(&table)?;
doc.add_page(page);
doc.save(&file_path)?;
assert!(file_path.exists());
Ok(())
}
#[test]
fn test_table_with_colspan() -> Result<()> {
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("colspan_table.pdf");
let mut doc = Document::new();
doc.set_title("Table Colspan Test");
let mut page = Page::a4();
let mut table = Table::new(vec![100.0, 100.0, 100.0, 100.0]);
table.set_position(50.0, 700.0);
table.add_header_row(vec![
"Col 1".to_string(),
"Col 2".to_string(),
"Col 3".to_string(),
"Col 4".to_string(),
])?;
let cells = vec![
TableCell::new("Normal cell".to_string()),
TableCell::with_colspan("Merged across 2 columns".to_string(), 2)
.set_align(TextAlign::Center)
.clone(),
TableCell::new("Normal cell".to_string()),
];
table.add_custom_row(cells)?;
let cells = vec![
TableCell::with_colspan("Merged across 3 columns".to_string(), 3)
.set_align(TextAlign::Center)
.clone(),
TableCell::new("Single".to_string()),
];
table.add_custom_row(cells)?;
let cells = vec![TableCell::with_colspan("Full width cell".to_string(), 4)
.set_align(TextAlign::Center)
.clone()];
table.add_custom_row(cells)?;
page.add_table(&table)?;
doc.add_page(page);
doc.save(&file_path)?;
assert!(file_path.exists());
Ok(())
}
#[test]
fn test_multiple_tables_on_page() -> Result<()> {
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("multiple_tables.pdf");
let mut doc = Document::new();
doc.set_title("Multiple Tables Test");
let mut page = Page::a4();
let mut table1 = Table::with_equal_columns(3, 300.0);
table1.set_position(50.0, 750.0);
table1.add_header_row(vec!["A".to_string(), "B".to_string(), "C".to_string()])?;
table1.add_row(vec!["1".to_string(), "2".to_string(), "3".to_string()])?;
page.add_table(&table1)?;
page.text()
.set_font(Font::Helvetica, 12.0)
.at(50.0, 650.0)
.write("Table comparison:")?;
let mut table2 = Table::new(vec![80.0, 120.0, 100.0, 80.0]);
table2.set_position(50.0, 600.0);
let mut options = TableOptions::default();
options.border_color = Color::rgb(0.8, 0.2, 0.2);
options.font_size = 9.0;
table2.set_options(options);
table2.add_header_row(vec![
"Type".to_string(),
"Description".to_string(),
"Value".to_string(),
"Unit".to_string(),
])?;
table2.add_row(vec![
"Speed".to_string(),
"Maximum velocity".to_string(),
"150".to_string(),
"km/h".to_string(),
])?;
page.add_table(&table2)?;
doc.add_page(page);
doc.save(&file_path)?;
assert!(file_path.exists());
Ok(())
}
#[test]
fn test_table_error_handling() {
let mut table = Table::new(vec![100.0, 100.0]);
let result = table.add_row(vec![
"One".to_string(),
"Two".to_string(),
"Three".to_string(), ]);
assert!(result.is_err());
let mut table = Table::new(vec![100.0, 100.0, 100.0]);
let cells = vec![
TableCell::new("Normal".to_string()),
TableCell::with_colspan("Too wide".to_string(), 3), ];
let result = table.add_custom_row(cells);
assert!(result.is_err());
}
#[test]
fn test_table_dimensions() -> Result<()> {
let mut table = Table::new(vec![100.0, 150.0, 200.0]);
assert_eq!(table.get_width(), 450.0);
table.add_row(vec!["A".to_string(), "B".to_string(), "C".to_string()])?;
table.add_row(vec!["D".to_string(), "E".to_string(), "F".to_string()])?;
assert_eq!(table.get_height(), 40.0);
let mut options = TableOptions::default();
options.row_height = 30.0;
table.set_options(options);
assert_eq!(table.get_height(), 60.0);
Ok(())
}
#[test]
fn test_table_with_custom_fonts() -> Result<()> {
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("custom_font_table.pdf");
let mut doc = Document::new();
doc.set_title("Custom Font Table Test");
let mut page = Page::a4();
let mut table = Table::new(vec![150.0, 150.0, 150.0]);
table.set_position(50.0, 700.0);
let mut options = TableOptions::default();
options.font = Font::Courier;
options.font_size = 10.0;
options.header_style = Some(HeaderStyle {
background_color: Color::gray(0.85),
text_color: Color::black(),
font: Font::CourierBold,
bold: true,
});
table.set_options(options);
table.add_header_row(vec![
"Code".to_string(),
"Function".to_string(),
"Status".to_string(),
])?;
table.add_row(vec![
"FN001".to_string(),
"initialize()".to_string(),
"OK".to_string(),
])?;
table.add_row(vec![
"FN002".to_string(),
"process()".to_string(),
"PENDING".to_string(),
])?;
page.add_table(&table)?;
doc.add_page(page);
doc.save(&file_path)?;
assert!(file_path.exists());
Ok(())
}
#[test]
fn test_table_with_custom_font_uses_hex_encoding() -> Result<()> {
let mut doc = Document::new();
doc.set_title("CJK Table Font Test - Issue #160");
let mut page = Page::a4();
let mut table = Table::new(vec![200.0, 200.0]);
table.set_position(50.0, 700.0);
let mut options = TableOptions::default();
options.font = Font::Custom("NotoSansCJK".to_string());
options.font_size = 12.0;
table.set_options(options);
table.add_row(vec!["你好".to_string(), "世界".to_string()])?;
page.add_table(&table)?;
doc.add_page(page);
let config = WriterConfig {
compress_streams: false,
..WriterConfig::default()
};
let pdf_bytes = doc.to_bytes_with_config(config)?;
let pdf_content = String::from_utf8_lossy(&pdf_bytes);
assert!(
pdf_content.contains("<4F60597D> Tj"),
"PDF should contain hex-encoded CJK text '你好' as <4F60597D> Tj operator"
);
assert!(
pdf_content.contains("<4E16754C> Tj"),
"PDF should contain hex-encoded CJK text '世界' as <4E16754C> Tj operator"
);
assert!(
!pdf_content.contains("(你好)"),
"PDF must not contain literal CJK in parenthesized string"
);
Ok(())
}
#[test]
fn test_table_with_standard_font_uses_literal_encoding() -> Result<()> {
let mut doc = Document::new();
let mut page = Page::a4();
let mut table = Table::new(vec![200.0]);
table.set_position(50.0, 700.0);
table.add_row(vec!["Hello World".to_string()])?;
page.add_table(&table)?;
doc.add_page(page);
let config = WriterConfig {
compress_streams: false,
..WriterConfig::default()
};
let pdf_bytes = doc.to_bytes_with_config(config)?;
let pdf_content = String::from_utf8_lossy(&pdf_bytes);
assert!(
pdf_content.contains("(Hello World) Tj"),
"Standard font table must use literal string encoding"
);
Ok(())
}
#[test]
fn test_measure_text_uses_registered_custom_font_metrics() {
let mut widths = std::collections::HashMap::new();
for ch in "测试中文长文本居中对齐".chars() {
widths.insert(ch, 1000u16);
}
widths.insert(' ', 500);
for ch in 'a'..='z' {
widths.insert(ch, 500);
}
let metrics = FontMetrics::from_char_map(widths, 500);
register_custom_font_metrics("TestCJKFont162".to_string(), metrics);
let font = Font::Custom("TestCJKFont162".to_string());
let cjk_text = "测试中文长文本居中对齐";
let cjk_width = measure_text(cjk_text, &font, 10.5);
let expected_cjk_width = 11.0 * 10.5; assert!(
(cjk_width - expected_cjk_width).abs() < 0.01,
"CJK text width should be {expected_cjk_width}, got {cjk_width}"
);
let ascii_width = measure_text("test", &font, 10.5);
let expected_ascii_width = 4.0 * 500.0 * 10.5 / 1000.0;
assert!(
(ascii_width - expected_ascii_width).abs() < 0.01,
"ASCII text width should be {expected_ascii_width}, got {ascii_width}"
);
assert!(
cjk_width > ascii_width,
"CJK text ({cjk_width}) should be wider than ASCII text ({ascii_width})"
);
}
#[test]
fn test_default_custom_metrics_cjk_width() {
let font = Font::Custom("UnregisteredFontForCJKTest".to_string());
let cjk_width = measure_text("中", &font, 10.0);
let expected = 1000.0 * 10.0 / 1000.0; assert!(
(cjk_width - expected).abs() < 0.01,
"CJK char '中' should measure {expected}, got {cjk_width}"
);
let ascii_width = measure_text("A", &font, 10.0);
let expected_ascii = 667.0 * 10.0 / 1000.0; assert!(
(ascii_width - expected_ascii).abs() < 0.01,
"ASCII 'A' should measure {expected_ascii}, got {ascii_width}"
);
}
#[test]
fn test_celldata_accessible_and_span_works() {
let cell = CellData::new("Hello").colspan(3).rowspan(2);
assert_eq!(cell.content, "Hello");
assert_eq!(cell.colspan, 3);
assert_eq!(cell.rowspan, 2);
let cell_zero = CellData::new("Zero span").colspan(0);
assert_eq!(cell_zero.colspan, 1, "colspan(0) should clamp to minimum 1");
let cell_zero_row = CellData::new("Zero row span").rowspan(0);
assert_eq!(
cell_zero_row.rowspan, 1,
"rowspan(0) should clamp to minimum 1"
);
}
#[test]
fn test_rowdata_from_cells() {
let cells = vec![
CellData::new("Cell 1").colspan(2),
CellData::new("Cell 2"),
CellData::new("Cell 3").rowspan(3),
];
let row = RowData::from_cells(cells);
assert_eq!(row.cells.len(), 3);
assert_eq!(row.cells[0].colspan, 2);
assert_eq!(row.cells[1].colspan, 1); assert_eq!(row.cells[2].rowspan, 3);
}
#[test]
fn test_from_char_map_default_width_is_average() {
let mut widths = std::collections::HashMap::new();
widths.insert('A', 700u16);
widths.insert('B', 600u16);
widths.insert('C', 500u16);
let avg: u16 = 600;
let metrics = FontMetrics::from_char_map(widths, avg);
assert_eq!(
metrics.char_width('Z'),
600,
"default_width should reflect the font's average width, not a hardcoded value"
);
}
#[test]
fn test_add_font_from_bytes_no_metrics_on_failure() {
let font_name = "FailTestFont_unique_162_163";
assert!(
get_custom_font_metrics(font_name).is_none(),
"metrics must not exist before the call"
);
let mut doc = Document::new();
let result = doc.add_font_from_bytes(font_name, vec![0u8; 16]);
assert!(result.is_err(), "invalid font data should produce an error");
assert!(
get_custom_font_metrics(font_name).is_none(),
"metrics must NOT be registered when add_font_from_bytes fails"
);
}