use crate::document::format_coord;
use crate::fonts::{BuiltinFont, FontRef};
use crate::graphics::Color;
use crate::textflow::{
break_word, line_height_for, measure_word, FitResult, Rect, TextStyle, UsedFonts, WordBreak,
};
use crate::truetype::TrueTypeFont;
use crate::writer::escape_pdf_string;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TextAlign {
#[default]
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CellOverflow {
Wrap,
Clip,
Shrink,
}
#[derive(Debug, Clone)]
pub struct CellStyle {
pub background_color: Option<Color>,
pub text_color: Option<Color>,
pub font: FontRef,
pub font_size: f64,
pub padding: f64,
pub overflow: CellOverflow,
pub word_break: WordBreak,
pub text_align: TextAlign,
}
impl Default for CellStyle {
fn default() -> Self {
CellStyle {
background_color: None,
text_color: None,
font: FontRef::Builtin(BuiltinFont::Helvetica),
font_size: 10.0,
padding: 4.0,
overflow: CellOverflow::Wrap,
word_break: WordBreak::BreakAll,
text_align: TextAlign::Left,
}
}
}
#[derive(Clone)]
pub struct Cell {
pub text: String,
pub style: CellStyle,
pub col_span: usize,
}
impl Cell {
pub fn new(text: impl Into<String>) -> Self {
Cell {
text: text.into(),
style: CellStyle::default(),
col_span: 1,
}
}
pub fn styled(text: impl Into<String>, style: CellStyle) -> Self {
Cell {
text: text.into(),
style,
col_span: 1,
}
}
}
#[derive(Clone)]
pub struct Row {
pub cells: Vec<Cell>,
pub background_color: Option<Color>,
pub height: Option<f64>,
}
impl Row {
pub fn new(cells: Vec<Cell>) -> Self {
Row {
cells,
background_color: None,
height: None,
}
}
}
pub struct Table {
pub columns: Vec<f64>,
pub default_style: CellStyle,
pub border_color: Color,
pub border_width: f64,
}
impl Table {
pub fn new(columns: Vec<f64>) -> Self {
Table {
columns,
default_style: CellStyle::default(),
border_color: Color::rgb(0.0, 0.0, 0.0),
border_width: 0.5,
}
}
pub(crate) fn generate_row_ops(
&self,
row: &Row,
cursor: &mut TableCursor,
tt_fonts: &mut [TrueTypeFont],
) -> (Vec<u8>, FitResult, UsedFonts) {
let row_height = measure_row_height(row, &self.columns, &self.default_style, tt_fonts);
let bottom = cursor.rect.y - cursor.rect.height;
if cursor.current_y - row_height < bottom {
let result = if cursor.first_row {
FitResult::BoxEmpty
} else {
FitResult::BoxFull
};
return (Vec::new(), result, UsedFonts::default());
}
let mut output: Vec<u8> = Vec::new();
let mut used = UsedFonts::default();
let segments = cell_segments(&row.cells, &self.columns, cursor.rect.x);
draw_row_backgrounds_with_segments(
row,
&segments,
cursor.rect.x,
cursor.current_y,
row_height,
&self.columns,
&mut output,
);
for (cell, (cell_x, cell_width)) in row.cells.iter().zip(segments.iter()) {
render_cell(
cell,
*cell_x,
cursor.current_y,
*cell_width,
row_height,
tt_fonts,
&mut output,
&mut used,
);
}
if self.border_width > 0.0 {
draw_row_borders(
&self.columns,
&row.cells,
cursor.rect.x,
cursor.current_y,
row_height,
self.border_color,
self.border_width,
&mut output,
);
}
cursor.current_y -= row_height;
cursor.first_row = false;
(output, FitResult::Stop, used)
}
}
pub struct TableCursor {
pub(crate) rect: Rect,
pub(crate) current_y: f64,
pub(crate) first_row: bool,
}
impl TableCursor {
pub fn new(rect: &Rect) -> Self {
TableCursor {
rect: *rect,
current_y: rect.y,
first_row: true,
}
}
pub fn reset(&mut self, rect: &Rect) {
self.rect = *rect;
self.current_y = rect.y;
self.first_row = true;
}
pub fn is_first_row(&self) -> bool {
self.first_row
}
pub fn current_y(&self) -> f64 {
self.current_y
}
}
fn cell_segments(cells: &[Cell], columns: &[f64], row_x: f64) -> Vec<(f64, f64)> {
let mut segments = Vec::with_capacity(cells.len());
let mut col_idx = 0;
let mut x = row_x;
for cell in cells {
let span = cell.col_span.max(1);
let end = (col_idx + span).min(columns.len());
let width: f64 = columns[col_idx..end].iter().sum();
segments.push((x, width));
x += width;
col_idx = end;
}
segments
}
fn visible_dividers(cells: &[Cell], columns: &[f64]) -> Vec<bool> {
let n = columns.len();
if n <= 1 {
return vec![];
}
let mut visible = vec![true; n - 1];
let mut col_idx = 0;
for cell in cells {
let span = cell.col_span.max(1);
for k in col_idx..(col_idx + span).saturating_sub(1) {
if k < visible.len() {
visible[k] = false;
}
}
col_idx += span;
}
visible
}
fn measure_row_height(
row: &Row,
columns: &[f64],
default_style: &CellStyle,
tt_fonts: &[TrueTypeFont],
) -> f64 {
if let Some(h) = row.height {
return h;
}
let mut max_height = 0.0_f64;
let mut col_idx = 0;
for cell in &row.cells {
let span = cell.col_span.max(1);
let end = (col_idx + span).min(columns.len());
let cell_width: f64 = columns[col_idx..end].iter().sum();
let h = measure_cell_height(&cell.text, &cell.style, cell_width, tt_fonts);
max_height = max_height.max(h);
col_idx = end;
}
if max_height == 0.0 {
let ts = make_text_style(default_style);
line_height_for(&ts, tt_fonts) + 2.0 * default_style.padding
} else {
max_height
}
}
fn measure_cell_height(
text: &str,
style: &CellStyle,
col_width: f64,
tt_fonts: &[TrueTypeFont],
) -> f64 {
let avail_width = col_width - 2.0 * style.padding;
let ts = make_text_style(style);
let lh = line_height_for(&ts, tt_fonts);
let lines = count_lines(text, avail_width, &ts, style.word_break, tt_fonts);
lines as f64 * lh + 2.0 * style.padding
}
fn make_text_style(style: &CellStyle) -> TextStyle {
TextStyle {
font: style.font,
font_size: style.font_size,
}
}
fn count_lines(
text: &str,
avail_width: f64,
style: &TextStyle,
word_break: WordBreak,
tt_fonts: &[TrueTypeFont],
) -> usize {
if text.is_empty() {
return 1;
}
text.split('\n')
.map(|para| count_paragraph_lines(para, avail_width, style, word_break, tt_fonts))
.sum::<usize>()
.max(1)
}
fn count_paragraph_lines(
text: &str,
avail_width: f64,
style: &TextStyle,
word_break: WordBreak,
tt_fonts: &[TrueTypeFont],
) -> usize {
let text = text.trim();
if text.is_empty() {
return 1;
}
let mut lines = 1usize;
let mut line_width = 0.0_f64;
for word in text.split_whitespace() {
let word_w = measure_word(word, style, tt_fonts);
let space_w = if line_width == 0.0 {
0.0
} else {
measure_word(" ", style, tt_fonts)
};
let needed = line_width + space_w + word_w;
if needed > avail_width && line_width > 0.0 {
lines += 1;
line_width = word_w;
if word_break != WordBreak::Normal && word_w > avail_width {
lines += count_break_lines(word, avail_width, style, word_break, tt_fonts) - 1;
line_width = trailing_piece_width(word, avail_width, style, word_break, tt_fonts);
}
} else if word_break != WordBreak::Normal && word_w > avail_width {
lines += count_break_lines(word, avail_width, style, word_break, tt_fonts) - 1;
line_width = trailing_piece_width(word, avail_width, style, word_break, tt_fonts);
} else {
line_width = needed;
}
}
lines
}
fn count_break_lines(
word: &str,
avail_width: f64,
style: &TextStyle,
word_break: WordBreak,
tt_fonts: &[TrueTypeFont],
) -> usize {
break_word(word, avail_width, style, word_break, tt_fonts).len()
}
fn trailing_piece_width(
word: &str,
avail_width: f64,
style: &TextStyle,
word_break: WordBreak,
tt_fonts: &[TrueTypeFont],
) -> f64 {
break_word(word, avail_width, style, word_break, tt_fonts)
.last()
.map_or(0.0, |p| measure_word(p, style, tt_fonts))
}
fn wrap_text(
text: &str,
avail_width: f64,
style: &TextStyle,
word_break: WordBreak,
tt_fonts: &[TrueTypeFont],
) -> Vec<String> {
let mut lines: Vec<String> = Vec::new();
for para in text.split('\n') {
wrap_paragraph(
para.trim(),
avail_width,
style,
word_break,
tt_fonts,
&mut lines,
);
}
if lines.is_empty() {
lines.push(String::new());
}
lines
}
fn wrap_paragraph(
text: &str,
avail_width: f64,
style: &TextStyle,
word_break: WordBreak,
tt_fonts: &[TrueTypeFont],
out: &mut Vec<String>,
) {
if text.is_empty() {
out.push(String::new());
return;
}
let mut current_line = String::new();
let mut line_width = 0.0_f64;
for word in text.split_whitespace() {
let word_w = measure_word(word, style, tt_fonts);
let space_w = if current_line.is_empty() {
0.0
} else {
measure_word(" ", style, tt_fonts)
};
let needed = line_width + space_w + word_w;
if needed > avail_width && !current_line.is_empty() {
out.push(current_line.clone());
current_line = String::new();
line_width = 0.0;
place_word_on_line(
word,
avail_width,
style,
word_break,
tt_fonts,
&mut current_line,
&mut line_width,
out,
);
} else if word_w > avail_width && word_break != WordBreak::Normal && current_line.is_empty()
{
place_word_on_line(
word,
avail_width,
style,
word_break,
tt_fonts,
&mut current_line,
&mut line_width,
out,
);
} else {
if !current_line.is_empty() {
current_line.push(' ');
}
current_line.push_str(word);
line_width = needed;
}
}
if !current_line.is_empty() {
out.push(current_line);
}
}
fn place_word_on_line(
word: &str,
avail_width: f64,
style: &TextStyle,
word_break: WordBreak,
tt_fonts: &[TrueTypeFont],
current_line: &mut String,
line_width: &mut f64,
out: &mut Vec<String>,
) {
let word_w = measure_word(word, style, tt_fonts);
if word_w <= avail_width || word_break == WordBreak::Normal {
if !current_line.is_empty() {
current_line.push(' ');
}
current_line.push_str(word);
*line_width += word_w;
return;
}
let pieces = break_word(word, avail_width, style, word_break, tt_fonts);
let last_idx = pieces.len() - 1;
for (i, piece) in pieces.into_iter().enumerate() {
if i < last_idx {
out.push(piece);
} else {
*current_line = piece.clone();
*line_width = measure_word(&piece, style, tt_fonts);
}
}
}
fn pdf_font_name(font: FontRef, tt_fonts: &[TrueTypeFont]) -> String {
match font {
FontRef::Builtin(b) => b.pdf_name().to_string(),
FontRef::TrueType(id) => tt_fonts[id.0].pdf_name.clone(),
}
}
fn record_font(font: &FontRef, used: &mut UsedFonts) {
match font {
FontRef::Builtin(b) => {
used.builtin.insert(*b);
}
FontRef::TrueType(id) => {
used.truetype.insert(id.0);
}
}
}
fn emit_cell_text(text: &str, font: FontRef, tt_fonts: &mut [TrueTypeFont], output: &mut Vec<u8>) {
if text.is_empty() {
return;
}
match font {
FontRef::Builtin(_) => {
let escaped = escape_pdf_string(text);
output.extend_from_slice(format!("({}) Tj\n", escaped).as_bytes());
}
FontRef::TrueType(id) => {
let hex = tt_fonts[id.0].encode_text_hex(text);
output.extend_from_slice(format!("{} Tj\n", hex).as_bytes());
}
}
}
fn draw_row_backgrounds_with_segments(
row: &Row,
segments: &[(f64, f64)],
row_x: f64,
row_top: f64,
row_height: f64,
columns: &[f64],
output: &mut Vec<u8>,
) {
let row_bottom = row_top - row_height;
if let Some(bg) = row.background_color {
let total_width: f64 = columns.iter().sum();
output.extend_from_slice(
format!(
"{} {} {} rg\n{} {} {} {} re\nf\n",
format_coord(bg.r),
format_coord(bg.g),
format_coord(bg.b),
format_coord(row_x),
format_coord(row_bottom),
format_coord(total_width),
format_coord(row_height),
)
.as_bytes(),
);
}
for (cell, &(cell_x, cell_width)) in row.cells.iter().zip(segments.iter()) {
if let Some(bg) = cell.style.background_color {
output.extend_from_slice(
format!(
"{} {} {} rg\n{} {} {} {} re\nf\n",
format_coord(bg.r),
format_coord(bg.g),
format_coord(bg.b),
format_coord(cell_x),
format_coord(row_bottom),
format_coord(cell_width),
format_coord(row_height),
)
.as_bytes(),
);
}
}
}
fn draw_row_borders(
columns: &[f64],
cells: &[Cell],
row_x: f64,
row_top: f64,
row_height: f64,
border_color: Color,
border_width: f64,
output: &mut Vec<u8>,
) {
let row_bottom = row_top - row_height;
let total_width: f64 = columns.iter().sum();
output.extend_from_slice(b"q\n");
output.extend_from_slice(
format!(
"{} {} {} RG\n{} w\n",
format_coord(border_color.r),
format_coord(border_color.g),
format_coord(border_color.b),
format_coord(border_width),
)
.as_bytes(),
);
output.extend_from_slice(
format!(
"{} {} {} {} re\nS\n",
format_coord(row_x),
format_coord(row_bottom),
format_coord(total_width),
format_coord(row_height),
)
.as_bytes(),
);
let visible = visible_dividers(cells, columns);
let mut col_x = row_x;
for (k, &col_width) in columns[..columns.len().saturating_sub(1)]
.iter()
.enumerate()
{
col_x += col_width;
if visible.get(k).copied().unwrap_or(true) {
output.extend_from_slice(
format!(
"{} {} m\n{} {} l\nS\n",
format_coord(col_x),
format_coord(row_top),
format_coord(col_x),
format_coord(row_bottom),
)
.as_bytes(),
);
}
}
output.extend_from_slice(b"Q\n");
}
fn aligned_x(
line: &str,
align: TextAlign,
cell_x: f64,
col_width: f64,
padding: f64,
ts: &TextStyle,
tt_fonts: &[TrueTypeFont],
) -> f64 {
match align {
TextAlign::Left => cell_x + padding,
TextAlign::Right => {
let line_w = measure_word(line, ts, tt_fonts);
cell_x + col_width - padding - line_w
}
TextAlign::Center => {
let avail = col_width - 2.0 * padding;
let line_w = measure_word(line, ts, tt_fonts);
cell_x + padding + (avail - line_w).max(0.0) / 2.0
}
}
}
fn render_cell(
cell: &Cell,
cell_x: f64,
row_top: f64,
col_width: f64,
row_height: f64,
tt_fonts: &mut [TrueTypeFont],
output: &mut Vec<u8>,
used: &mut UsedFonts,
) {
let style = &cell.style;
let avail_width = (col_width - 2.0 * style.padding).max(0.0);
let avail_height = (row_height - 2.0 * style.padding).max(0.0);
let effective_font_size = if style.overflow == CellOverflow::Shrink {
shrink_font_size(
&cell.text,
style.font,
style.font_size,
avail_width,
avail_height,
style.word_break,
tt_fonts,
)
} else {
style.font_size
};
let ts = TextStyle {
font: style.font,
font_size: effective_font_size,
};
let lh = line_height_for(&ts, tt_fonts);
let lines = wrap_text(&cell.text, avail_width, &ts, style.word_break, tt_fonts);
output.extend_from_slice(b"q\n");
if style.overflow == CellOverflow::Clip {
let clip_bottom = row_top - row_height;
output.extend_from_slice(
format!(
"{} {} {} {} re\nW\nn\n",
format_coord(cell_x),
format_coord(clip_bottom),
format_coord(col_width),
format_coord(row_height),
)
.as_bytes(),
);
}
let first_line_y = row_top - style.padding - effective_font_size;
output.extend_from_slice(b"BT\n");
let text_color = style
.text_color
.unwrap_or_else(|| Color::rgb(0.0, 0.0, 0.0));
output.extend_from_slice(
format!(
"{} {} {} rg\n",
format_coord(text_color.r),
format_coord(text_color.g),
format_coord(text_color.b),
)
.as_bytes(),
);
let font_name = pdf_font_name(ts.font, tt_fonts);
output.extend_from_slice(
format!("/{} {} Tf\n", font_name, format_coord(effective_font_size)).as_bytes(),
);
record_font(&ts.font, used);
let align = style.text_align;
let mut current_x = cell_x + style.padding;
for (i, line) in lines.iter().enumerate() {
let line_x = aligned_x(line, align, cell_x, col_width, style.padding, &ts, tt_fonts);
if i == 0 {
output.extend_from_slice(
format!(
"{} {} Td\n",
format_coord(line_x),
format_coord(first_line_y)
)
.as_bytes(),
);
} else {
let dx = line_x - current_x;
output.extend_from_slice(
format!("{} {} Td\n", format_coord(dx), format_coord(-lh)).as_bytes(),
);
}
current_x = line_x;
emit_cell_text(line, ts.font, tt_fonts, output);
}
output.extend_from_slice(b"ET\n");
output.extend_from_slice(b"Q\n");
}
fn shrink_font_size(
text: &str,
font: FontRef,
initial_size: f64,
avail_width: f64,
avail_height: f64,
word_break: WordBreak,
tt_fonts: &[TrueTypeFont],
) -> f64 {
const MIN_FONT_SIZE: f64 = 4.0;
const STEP: f64 = 0.5;
let mut font_size = initial_size;
loop {
let ts = TextStyle { font, font_size };
let lh = line_height_for(&ts, tt_fonts);
let lines = count_lines(text, avail_width, &ts, word_break, tt_fonts);
let fits_height = lines as f64 * lh <= avail_height;
let fits_width = word_break != WordBreak::Normal
|| text
.split_whitespace()
.all(|w| measure_word(w, &ts, tt_fonts) <= avail_width);
if (fits_height && fits_width) || font_size <= MIN_FONT_SIZE {
break;
}
font_size = (font_size - STEP).max(MIN_FONT_SIZE);
}
font_size
}