use lo_core::{
Block, Heading, Inline, ListBlock, ListItem, Paragraph, PdfDocument, PdfFont, Table,
TextDocument,
};
const PAGE_W: f32 = 595.0;
const PAGE_H: f32 = 842.0;
const MARGIN_L: f32 = 50.0;
const MARGIN_R: f32 = 50.0;
const MARGIN_T: f32 = 60.0;
const MARGIN_B: f32 = 60.0;
pub fn render_document_pdf(doc: &TextDocument) -> Vec<u8> {
let mut pdf = PdfDocument::new();
let mut page_index = pdf.add_page(PAGE_W, PAGE_H);
let mut y = PAGE_H - MARGIN_T;
{
let page = pdf.page_mut(page_index).expect("first page");
page.text(MARGIN_L, y, 22.0, PdfFont::HelveticaBold, &doc.meta.title);
}
y -= 32.0;
for block in &doc.body {
match block {
Block::Heading(Heading { level, content }) => {
let size = match *level {
1 => 20.0,
2 => 18.0,
3 => 16.0,
_ => 14.0,
};
y = ensure_room(&mut pdf, &mut page_index, y, size + 10.0);
let lines = wrap_text(&content.to_plain_text(), PAGE_W - MARGIN_L - MARGIN_R, size);
for line in lines {
let page = pdf.page_mut(page_index).expect("page");
page.text(MARGIN_L, y, size, PdfFont::HelveticaBold, &line);
y -= size + 6.0;
}
y -= 4.0;
}
Block::Paragraph(paragraph) => {
y = render_paragraph(
&mut pdf,
&mut page_index,
y,
MARGIN_L,
paragraph,
12.0,
PdfFont::Helvetica,
);
y -= 6.0;
}
Block::List(ListBlock { ordered, items }) => {
for (index, item) in items.iter().enumerate() {
y = ensure_room(&mut pdf, &mut page_index, y, 18.0);
let marker = if *ordered {
format!("{}.", index + 1)
} else {
"•".to_string()
};
{
let page = pdf.page_mut(page_index).expect("page");
page.text(MARGIN_L + 6.0, y, 12.0, PdfFont::Helvetica, &marker);
}
y = render_list_item(&mut pdf, &mut page_index, y, item);
y -= 2.0;
}
y -= 4.0;
}
Block::Table(table) => {
y = render_table(&mut pdf, &mut page_index, y, table);
y -= 8.0;
}
Block::Image(image) => {
y = ensure_room(&mut pdf, &mut page_index, y, 24.0);
let label = format!("[image: {}]", image.alt);
let page = pdf.page_mut(page_index).expect("page");
page.text(MARGIN_L, y, 11.0, PdfFont::HelveticaOblique, &label);
y -= 18.0;
}
Block::Section(section) => {
y = ensure_room(&mut pdf, &mut page_index, y, 18.0);
{
let page = pdf.page_mut(page_index).expect("page");
page.text(
MARGIN_L,
y,
13.0,
PdfFont::HelveticaBold,
&format!("[{}]", section.name),
);
}
y -= 18.0;
for nested in §ion.blocks {
if let Block::Paragraph(paragraph) = nested {
y = render_paragraph(
&mut pdf,
&mut page_index,
y,
MARGIN_L,
paragraph,
12.0,
PdfFont::Helvetica,
);
y -= 4.0;
}
}
}
Block::HorizontalRule => {
y = ensure_room(&mut pdf, &mut page_index, y, 12.0);
let page = pdf.page_mut(page_index).expect("page");
page.line(MARGIN_L, y, PAGE_W - MARGIN_R, y);
y -= 10.0;
}
Block::PageBreak => {
page_index = pdf.add_page(PAGE_W, PAGE_H);
y = PAGE_H - MARGIN_T;
}
}
}
pdf.finish()
}
fn render_list_item(
pdf: &mut PdfDocument,
page_index: &mut usize,
mut y: f32,
item: &ListItem,
) -> f32 {
for block in &item.blocks {
if let Block::Paragraph(paragraph) = block {
y = render_paragraph(
pdf,
page_index,
y,
MARGIN_L + 18.0,
paragraph,
12.0,
PdfFont::Helvetica,
);
}
}
y
}
fn render_paragraph(
pdf: &mut PdfDocument,
page_index: &mut usize,
mut y: f32,
x: f32,
paragraph: &Paragraph,
size: f32,
font: PdfFont,
) -> f32 {
let width = PAGE_W - x - MARGIN_R;
let lines = wrap_text(¶graph_to_plain(paragraph), width, size);
for line in lines {
y = ensure_room(pdf, page_index, y, size + 3.0);
let page = pdf.page_mut(*page_index).expect("page");
page.text(x, y, size, font, &line);
y -= size + 3.0;
}
y
}
fn paragraph_to_plain(paragraph: &Paragraph) -> String {
let mut out = String::new();
for span in ¶graph.spans {
match span {
Inline::Text(text) | Inline::Bold(text) | Inline::Italic(text) | Inline::Code(text) => {
out.push_str(text)
}
Inline::Link { label, .. } => out.push_str(label),
Inline::LineBreak => out.push('\n'),
}
}
out
}
fn render_table(pdf: &mut PdfDocument, page_index: &mut usize, mut y: f32, table: &Table) -> f32 {
let cols = table
.rows
.iter()
.map(|row| row.cells.len())
.max()
.unwrap_or(1)
.max(1);
let table_w = PAGE_W - MARGIN_L - MARGIN_R;
let col_w = table_w / cols as f32;
for row in &table.rows {
let cell_texts: Vec<String> = row
.cells
.iter()
.map(|cell| {
cell.paragraphs
.iter()
.map(paragraph_to_plain)
.collect::<Vec<_>>()
.join("\n")
})
.collect();
let wrapped = cell_texts
.iter()
.map(|cell| wrap_text(cell, col_w - 8.0, 10.0))
.collect::<Vec<_>>();
let max_lines = wrapped
.iter()
.map(|lines| lines.len())
.max()
.unwrap_or(1)
.max(1);
let row_h = max_lines as f32 * 13.0 + 8.0;
y = ensure_room(pdf, page_index, y, row_h + 2.0);
let top = y;
let bottom = y - row_h;
{
let page = pdf.page_mut(*page_index).expect("page");
for col in 0..=cols {
let x = MARGIN_L + col as f32 * col_w;
page.line(x, top, x, bottom);
}
page.line(MARGIN_L, top, PAGE_W - MARGIN_R, top);
page.line(MARGIN_L, bottom, PAGE_W - MARGIN_R, bottom);
for (col_index, cell_lines) in wrapped.iter().enumerate() {
let cell_x = MARGIN_L + col_index as f32 * col_w + 4.0;
let mut line_y = top - 12.0;
for line in cell_lines {
page.text(cell_x, line_y, 10.0, PdfFont::Helvetica, line);
line_y -= 13.0;
}
}
}
y = bottom - 4.0;
}
y
}
fn ensure_room(pdf: &mut PdfDocument, page_index: &mut usize, y: f32, needed: f32) -> f32 {
if y - needed >= MARGIN_B {
y
} else {
*page_index = pdf.add_page(PAGE_W, PAGE_H);
PAGE_H - MARGIN_T
}
}
fn wrap_text(text: &str, max_width: f32, font_size: f32) -> Vec<String> {
let approx_char_w = (font_size * 0.52).max(5.0);
let max_chars = ((max_width / approx_char_w).floor() as usize).max(8);
let mut lines = Vec::new();
for paragraph_line in text.split('\n') {
let mut current = String::new();
for word in paragraph_line.split_whitespace() {
let candidate = if current.is_empty() {
word.to_string()
} else {
format!("{current} {word}")
};
if candidate.chars().count() <= max_chars {
current = candidate;
} else {
if !current.is_empty() {
lines.push(current);
}
if word.chars().count() <= max_chars {
current = word.to_string();
} else {
let mut chunk = String::new();
for ch in word.chars() {
chunk.push(ch);
if chunk.chars().count() >= max_chars {
lines.push(std::mem::take(&mut chunk));
}
}
current = chunk;
}
}
}
if !current.is_empty() {
lines.push(current);
} else if paragraph_line.is_empty() {
lines.push(String::new());
}
}
if lines.is_empty() {
lines.push(String::new());
}
lines
}