#[cfg(feature = "html")]
mod html_tokenizer;
#[cfg(feature = "html")]
pub mod html;
use ttf_parser::Face;
use crate::{
Document, FontHandle, Result,
document::{glyph_advance_pt, wrap_paragraph},
};
#[derive(Clone, Copy, Debug)]
pub struct Margins {
pub top: f32,
pub right: f32,
pub bottom: f32,
pub left: f32,
}
impl Margins {
pub fn uniform(pt: f32) -> Self {
Margins {
top: pt,
right: pt,
bottom: pt,
left: pt,
}
}
pub fn a4_standard() -> Self {
Margins::uniform(56.7)
}
}
#[derive(Clone, Debug)]
pub struct HeaderFooter {
pub left: Option<String>,
pub center: Option<String>,
pub right: Option<String>,
pub font_size: f32,
pub color: crate::Color,
}
impl Default for HeaderFooter {
fn default() -> Self {
HeaderFooter {
left: None,
center: None,
right: None,
font_size: 9.0,
color: crate::Color::Rgb([0.3, 0.3, 0.3]),
}
}
}
impl HeaderFooter {
pub fn page_number() -> Self {
HeaderFooter {
center: Some("{{page}} / {{total}}".into()),
..Default::default()
}
}
}
pub struct FlowOptions {
pub page_size: (f32, f32),
pub margins: Margins,
pub body_font_size: f32,
pub heading_size_scale: [f32; 6],
pub line_height_factor: f32,
pub paragraph_spacing: f32,
pub table_key_ratio: f32,
pub max_pages: u32,
pub header: Option<HeaderFooter>,
pub footer: Option<HeaderFooter>,
pub auto_bookmarks: bool,
pub heading_font_bytes: Option<Vec<u8>>,
pub code_font_bytes: Option<Vec<u8>>,
pub code_background: Option<[f32; 3]>,
}
impl Default for FlowOptions {
fn default() -> Self {
FlowOptions {
page_size: (595.0, 842.0),
margins: Margins::a4_standard(),
body_font_size: 11.0,
heading_size_scale: [2.0, 1.6, 1.3, 1.1, 1.0, 0.9],
line_height_factor: 1.4,
paragraph_spacing: 6.0,
table_key_ratio: 0.3,
max_pages: 2000,
header: None,
footer: None,
auto_bookmarks: true,
heading_font_bytes: None,
code_font_bytes: None,
code_background: None,
}
}
}
#[derive(Clone, Debug)]
pub struct InlineSpan {
pub text: String,
pub bold: bool,
pub italic: bool,
pub color: crate::Color,
}
impl InlineSpan {
pub fn plain(text: impl Into<String>) -> Self {
InlineSpan {
text: text.into(),
bold: false,
italic: false,
color: crate::Color::Rgb([0.0; 3]),
}
}
pub fn bold(text: impl Into<String>) -> Self {
InlineSpan {
text: text.into(),
bold: true,
italic: false,
color: crate::Color::Rgb([0.0; 3]),
}
}
pub fn italic(text: impl Into<String>) -> Self {
InlineSpan {
text: text.into(),
bold: false,
italic: true,
color: crate::Color::Rgb([0.0; 3]),
}
}
pub fn colored(text: impl Into<String>, color: impl Into<crate::Color>) -> Self {
InlineSpan {
text: text.into(),
bold: false,
italic: false,
color: color.into(),
}
}
}
pub struct FlowDocument {
inner: Document,
body_font: FontHandle,
body_font_bytes: Vec<u8>,
heading_font: Option<FontHandle>,
heading_font_bytes: Option<Vec<u8>>,
code_font: Option<FontHandle>,
code_font_bytes: Option<Vec<u8>>,
options: FlowOptions,
current_page: u32,
content_y: f32,
outline_entries: Vec<(String, u32, f32, u8)>,
}
impl FlowDocument {
pub fn new(font_bytes: impl Into<Vec<u8>>, options: FlowOptions) -> Result<Self> {
let font_bytes: Vec<u8> = font_bytes.into();
let mut inner = Document::new(options.page_size)?;
let body_font = inner.embed_font(&font_bytes)?;
let (heading_font, heading_font_bytes) = if let Some(bytes) = &options.heading_font_bytes {
let handle = inner.embed_font(bytes)?;
(Some(handle), Some(bytes.clone()))
} else {
(None, None)
};
let (code_font, code_font_bytes) = if let Some(bytes) = &options.code_font_bytes {
let handle = inner.embed_font(bytes)?;
(Some(handle), Some(bytes.clone()))
} else {
(None, None)
};
Ok(FlowDocument {
inner,
body_font,
body_font_bytes: font_bytes,
heading_font,
heading_font_bytes,
code_font,
code_font_bytes,
options,
current_page: 1,
content_y: 0.0,
outline_entries: Vec::new(),
})
}
fn content_width(&self) -> f32 {
self.options.page_size.0 - self.options.margins.left - self.options.margins.right
}
fn content_height(&self) -> f32 {
self.options.page_size.1 - self.options.margins.top - self.options.margins.bottom
}
fn pdf_baseline_y(&self, content_y: f32, font_size: f32) -> f32 {
self.options.page_size.1 - self.options.margins.top - content_y - font_size
}
fn pdf_top_y(&self, content_y: f32) -> f32 {
self.options.page_size.1 - self.options.margins.top - content_y
}
fn measure_lines(
&self,
text: &str,
font_size: f32,
width: f32,
font_bytes: &[u8],
) -> Vec<String> {
match Face::parse(font_bytes, 0) {
Ok(face) => text
.split('\n')
.flat_map(|para| wrap_paragraph(para, &face, font_size, width))
.collect(),
Err(_) => text.lines().map(str::to_owned).collect(),
}
}
fn ensure_space(&mut self, height: f32) -> Result<()> {
if self.content_y > 0.0 && self.content_y + height > self.content_height() + 0.1 {
let n = self.inner.page_count();
if n >= self.options.max_pages {
return Err(crate::Error::InvalidInput(format!(
"document exceeds max_pages limit of {}",
self.options.max_pages
)));
}
self.inner.insert_blank_page(n, self.options.page_size)?;
self.current_page = n + 1;
self.content_y = 0.0;
}
Ok(())
}
pub fn push_heading(&mut self, text: &str, level: u8) -> Result<()> {
let text = text.trim();
if text.is_empty() {
return Ok(());
}
let level = level.clamp(1, 6) as usize;
let font_size = self.options.body_font_size * self.options.heading_size_scale[level - 1];
let line_h = font_size * self.options.line_height_factor;
let font_bytes = self
.heading_font_bytes
.as_deref()
.unwrap_or(&self.body_font_bytes);
let lines = self.measure_lines(text, font_size, self.content_width(), font_bytes);
let block_h = lines.len() as f32 * line_h;
let pre_spacing = if self.content_y > 0.0 {
self.options.paragraph_spacing * 1.5
} else {
0.0
};
self.ensure_space(pre_spacing + block_h)?;
if self.content_y > 0.0 {
self.content_y += pre_spacing;
}
if self.options.auto_bookmarks {
let bm_y = self.pdf_top_y(self.content_y);
let bm_page = self.current_page;
self.outline_entries
.push((text.to_owned(), bm_page, bm_y, level as u8));
}
let x = self.options.margins.left;
let font = self.heading_font.unwrap_or(self.body_font);
let current_page = self.current_page;
for line in &lines {
let y = self.pdf_baseline_y(self.content_y, font_size);
self.inner.page(current_page)?.add_text(
line,
font,
[x, y],
font_size,
[0.0, 0.0, 0.0],
)?;
self.content_y += line_h;
}
self.content_y += self.options.paragraph_spacing;
Ok(())
}
pub fn push_paragraph(&mut self, text: &str) -> Result<()> {
let text = text.trim();
if text.is_empty() {
return Ok(());
}
let font_size = self.options.body_font_size;
let line_h = font_size * self.options.line_height_factor;
let lines =
self.measure_lines(text, font_size, self.content_width(), &self.body_font_bytes);
let x = self.options.margins.left;
let font = self.body_font;
for line in &lines {
self.ensure_space(line_h)?;
let current_page = self.current_page;
let y = self.pdf_baseline_y(self.content_y, font_size);
self.inner.page(current_page)?.add_text(
line,
font,
[x, y],
font_size,
[0.0, 0.0, 0.0],
)?;
self.content_y += line_h;
}
self.content_y += self.options.paragraph_spacing;
Ok(())
}
pub fn push_paragraph_styled(&mut self, spans: &[InlineSpan]) -> Result<()> {
let non_empty: Vec<&InlineSpan> = spans.iter().filter(|s| !s.text.is_empty()).collect();
if non_empty.is_empty() {
return Ok(());
}
let font_size = self.options.body_font_size;
let line_h = font_size * self.options.line_height_factor;
let content_w = self.content_width();
let font_bytes_owned = self.body_font_bytes.clone();
let face: Option<Face<'_>> = Face::parse(&font_bytes_owned, 0).ok();
let font = self.body_font;
let mut char_spans: Vec<(char, usize)> = Vec::new();
for (i, span) in non_empty.iter().enumerate() {
for ch in span.text.chars() {
char_spans.push((ch, i));
}
}
let full_text: String = char_spans.iter().map(|(ch, _)| ch).collect();
let line_strings = match face.as_ref() {
Some(f) => full_text
.split('\n')
.flat_map(|p| crate::document::wrap_paragraph(p, f, font_size, content_w))
.collect::<Vec<_>>(),
None => full_text.lines().map(str::to_owned).collect(),
};
let mut char_cursor = 0usize;
for line_str in &line_strings {
let line_len = line_str.chars().count();
self.ensure_space(line_h)?;
let y = self.pdf_baseline_y(self.content_y, font_size);
let mut x = self.options.margins.left;
let current_page = self.current_page;
let mut run_start = char_cursor;
while run_start < char_cursor + line_len {
let span_idx = char_spans[run_start].1;
let mut run_end = run_start + 1;
while run_end < char_cursor + line_len && char_spans[run_end].1 == span_idx {
run_end += 1;
}
let run_text: String = char_spans[run_start..run_end]
.iter()
.map(|(ch, _)| ch)
.collect();
let span = non_empty[span_idx];
self.inner.page(current_page)?.add_text_styled(
&run_text,
font,
[x, y],
font_size,
span.color,
span.bold,
span.italic,
)?;
if let Some(ref f) = face {
x += run_text
.chars()
.map(|ch| {
crate::document::glyph_advance_pt(f, ch, font_size)
.unwrap_or(font_size * 0.5)
})
.sum::<f32>();
}
run_start = run_end;
}
char_cursor += line_len;
if char_cursor < char_spans.len() {
let next_ch = char_spans[char_cursor].0;
if next_ch == ' ' || next_ch == '\n' {
char_cursor += 1;
}
}
self.content_y += line_h;
}
self.content_y += self.options.paragraph_spacing;
Ok(())
}
pub fn push_key_value_table(&mut self, rows: &[(&str, &str)]) -> Result<()> {
if rows.is_empty() {
return Ok(());
}
let content_w = self.content_width();
let key_w = content_w * self.options.table_key_ratio;
let val_w = content_w - key_w;
let font_size = self.options.body_font_size;
let line_h = font_size * self.options.line_height_factor;
let cell_pad = 4.0_f32;
let inner_key_w = (key_w - cell_pad * 2.0).max(1.0);
let inner_val_w = (val_w - cell_pad * 2.0).max(1.0);
let border_color = [0.7_f32, 0.7, 0.7];
let border_lw = 0.5_f32;
let x_left = self.options.margins.left;
let x_divider = x_left + key_w;
let x_right = x_left + content_w;
let x_val = x_left + key_w + cell_pad;
let last_idx = rows.len() - 1;
for (idx, (key, val)) in rows.iter().enumerate() {
let key = key.trim();
let val = val.trim();
let key_lines = self.measure_lines(key, font_size, inner_key_w, &self.body_font_bytes);
let val_lines = self.measure_lines(val, font_size, inner_val_w, &self.body_font_bytes);
let row_lines = key_lines.len().max(val_lines.len()).max(1);
let row_h = row_lines as f32 * line_h + cell_pad * 2.0;
self.ensure_space(row_h)?;
let row_top_y = self.pdf_top_y(self.content_y);
self.content_y += row_h;
let row_bot_y = self.pdf_top_y(self.content_y);
let page_num = self.current_page;
let font = self.body_font;
{
let mut page = self.inner.page(page_num)?;
page.add_line(
[x_left, row_top_y],
[x_right, row_top_y],
border_color,
border_lw,
1.0,
)?;
for (i, line) in key_lines.iter().enumerate() {
let y = row_top_y - cell_pad - font_size - i as f32 * line_h;
page.add_text(
line,
font,
[x_left + cell_pad, y],
font_size,
[0.0, 0.0, 0.0],
)?;
}
for (i, line) in val_lines.iter().enumerate() {
let y = row_top_y - cell_pad - font_size - i as f32 * line_h;
page.add_text(line, font, [x_val, y], font_size, [0.0, 0.0, 0.0])?;
}
page.add_line(
[x_divider, row_top_y],
[x_divider, row_bot_y],
border_color,
border_lw,
1.0,
)?;
if idx == last_idx {
page.add_line(
[x_left, row_bot_y],
[x_right, row_bot_y],
border_color,
border_lw,
1.0,
)?;
}
}
}
self.content_y += self.options.paragraph_spacing;
Ok(())
}
pub fn push_list(&mut self, items: &[&str], ordered: bool) -> Result<()> {
for (i, item) in items.iter().enumerate() {
let bullet = if ordered {
format!("{}. {}", i + 1, item.trim())
} else {
format!("\u{2022} {}", item.trim()) };
self.push_paragraph(&bullet)?;
}
Ok(())
}
pub fn push_code_block(&mut self, text: &str) -> Result<()> {
let text = text.trim();
if text.is_empty() {
return Ok(());
}
let font_size = self.options.body_font_size;
let line_h = font_size * self.options.line_height_factor;
let font_bytes = self
.code_font_bytes
.as_deref()
.unwrap_or(&self.body_font_bytes);
let lines = self.measure_lines(text, font_size, self.content_width(), font_bytes);
let block_h = lines.len() as f32 * line_h;
let pre_spacing = if self.content_y > 0.0 {
self.options.paragraph_spacing
} else {
0.0
};
self.ensure_space(pre_spacing + block_h)?;
if self.content_y > 0.0 {
self.content_y += pre_spacing;
}
let x = self.options.margins.left;
let font = self.code_font.unwrap_or(self.body_font);
let current_page = self.current_page;
let y_top = self.pdf_top_y(self.content_y);
let y_bottom = y_top - block_h;
if let Some(bg_color) = self.options.code_background {
let right_x = self.options.page_size.0 - self.options.margins.right;
let padding = 2.0;
self.inner.page(current_page)?.add_rect(
[
x - padding,
y_bottom - padding,
right_x - x + 2.0 * padding,
block_h + 2.0 * padding,
],
bg_color,
1.0,
)?;
}
for line in &lines {
let y = self.pdf_baseline_y(self.content_y, font_size);
self.inner.page(current_page)?.add_text(
line,
font,
[x, y],
font_size,
[0.0, 0.0, 0.0],
)?;
self.content_y += line_h;
}
self.content_y += self.options.paragraph_spacing;
Ok(())
}
pub fn push_page_break(&mut self) -> Result<()> {
let n = self.inner.page_count();
self.inner.insert_blank_page(n, self.options.page_size)?;
self.current_page = n + 1;
self.content_y = 0.0;
Ok(())
}
pub fn render(mut self) -> Result<Vec<u8>> {
let total_pages = self.inner.page_count();
let body_font = self.body_font;
let font_bytes_owned: Vec<u8> = self.body_font_bytes.clone();
let face: Option<Face<'_>> = Face::parse(&font_bytes_owned, 0).ok();
if let Some(ref hdr) = self.options.header.clone() {
for pg in 1..=total_pages {
render_hf_on_page(
&mut self.inner,
pg,
hdr,
total_pages,
true,
body_font,
self.options.page_size,
self.options.margins,
face.as_ref(),
)?;
}
}
if let Some(ref ftr) = self.options.footer.clone() {
for pg in 1..=total_pages {
render_hf_on_page(
&mut self.inner,
pg,
ftr,
total_pages,
false,
body_font,
self.options.page_size,
self.options.margins,
face.as_ref(),
)?;
}
}
for (title, page, y, level) in self.outline_entries.drain(..) {
self.inner.add_outline_item(&title, page, y, level)?;
}
self.inner.save_to_bytes()
}
}
fn hf_subst(tmpl: &str, page: u32, total: u32) -> String {
tmpl.replace("{{page}}", &page.to_string())
.replace("{{total}}", &total.to_string())
}
fn hf_measure(face: Option<&Face<'_>>, text: &str, font_size: f32) -> f32 {
match face {
Some(f) => text
.chars()
.map(|ch| glyph_advance_pt(f, ch, font_size).unwrap_or(font_size * 0.5))
.sum(),
None => text.chars().count() as f32 * font_size * 0.5,
}
}
#[allow(clippy::too_many_arguments)]
fn render_hf_on_page(
inner: &mut Document,
page_num: u32,
hf: &HeaderFooter,
total_pages: u32,
is_header: bool,
font: FontHandle,
page_size: (f32, f32),
margins: Margins,
face: Option<&Face<'_>>,
) -> Result<()> {
let fs = if hf.font_size > 0.0 {
hf.font_size
} else {
9.0
};
let color = hf.color;
let margin_left = margins.left;
let margin_right = margins.right;
let content_w = page_size.0 - margin_left - margin_right;
let y = if is_header {
page_size.1 - margins.top * 0.5
} else {
margins.bottom * 0.5
};
if let Some(ref tmpl) = hf.left {
let text = hf_subst(tmpl, page_num, total_pages);
inner
.page(page_num)?
.add_text(&text, font, [margin_left, y], fs, color)?;
}
if let Some(ref tmpl) = hf.center {
let text = hf_subst(tmpl, page_num, total_pages);
let w = hf_measure(face, &text, fs);
let x = margin_left + (content_w - w) / 2.0;
inner
.page(page_num)?
.add_text(&text, font, [x, y], fs, color)?;
}
if let Some(ref tmpl) = hf.right {
let text = hf_subst(tmpl, page_num, total_pages);
let w = hf_measure(face, &text, fs);
let x = page_size.0 - margin_right - w;
inner
.page(page_num)?
.add_text(&text, font, [x, y], fs, color)?;
}
Ok(())
}