use crate::ast::{ListStyleType, Node, NodeKind};
use crate::generator::text::{
annotate_runs_with_urls, collect_inline_segments, estimate_children_height,
};
use crate::text::{
FONT_CONTEXT, LAYOUT_CONTEXT, TextAlign, TextStyle, create_text_layout,
layout_text_with_contexts,
};
use crate::visual::{Color, StrokeStyle, VisualElement};
use vello_cpu::kurbo::{Point, Rect};
use super::DocumentGenerator;
use crate::generator::box_model::BgStyle;
fn is_inline_node(kind: &NodeKind) -> bool {
matches!(
kind,
NodeKind::Text { .. }
| NodeKind::Strong { .. }
| NodeKind::Emphasis { .. }
| NodeKind::InlineCode { .. }
| NodeKind::Link { .. }
| NodeKind::Delete { .. }
| NodeKind::Subscript { .. }
| NodeKind::Superscript { .. }
| NodeKind::Span { .. }
| NodeKind::LineBreak
)
}
impl DocumentGenerator {
pub(crate) fn layout_paragraph(&mut self, children: &[Node], style: &crate::ast::Style) {
for child in children {
if let NodeKind::Image { src, alt, title: _ } = &child.kind {
self.layout_image(src, alt, &child.style);
}
}
let segments = collect_inline_segments(children);
if segments.is_empty() {
return;
}
let total_text: String = segments.iter().map(|(t, _)| t.as_str()).collect();
let combined: Vec<(&str, &TextStyle)> =
segments.iter().map(|(t, s)| (t.as_str(), s)).collect();
let (indent, available_width, _content_start_y, prev_indent) =
crate::generator::box_model::begin_box(
&mut self.page_context,
self.layout_ctx.current_indent,
style,
);
let available_width = available_width.max(1.0);
let bg_color = style.background_color;
let has_border = style.border.is_any_visible();
let border_color = style.border.top.color;
let border_width = style.border.max_width();
let border_radius = style.border.radius;
let align = match style.text_align {
crate::ast::TextAlign::Left => TextAlign::Left,
crate::ast::TextAlign::Center => TextAlign::Center,
crate::ast::TextAlign::Right => TextAlign::Right,
crate::ast::TextAlign::Justify => TextAlign::Left,
};
let first_line_indent = style.text_indent_em * style.font_size_pt;
let mut border_segment_start = self.page_context.current_y;
let settings = self.page_context.settings.clone();
let content_x = settings.content_x();
let content_y = settings.content_y();
let content_width = settings.content_width();
let content_height = settings.content_height();
let bg_left = content_x + prev_indent + style.margin.left;
let bg_right = content_x + content_width - style.margin.right;
let draw_segment_bg_and_border =
|ctx: &mut crate::generator::PageContext, seg_start: f32, seg_end: f32| {
if seg_end <= seg_start {
return;
}
let abs_top = content_y + seg_start - style.padding.top;
let abs_bottom = content_y + seg_end + style.padding.bottom;
if let Some(color) = bg_color {
ctx.add_element_before_text(VisualElement::Rect {
rect: Rect::new(
bg_left as f64,
abs_top as f64,
bg_right as f64,
abs_bottom as f64,
),
style: crate::visual::FillStrokeStyle {
fill: Some(color),
stroke: None,
},
});
}
if has_border {
let bx = bg_left - border_width;
let by = abs_top - border_width;
let bw = bg_right - bg_left + 2.0 * border_width;
let bh = abs_bottom - abs_top + 2.0 * border_width;
draw_border_inline(
ctx,
bx,
by,
bw,
bh,
border_color,
border_width,
border_radius,
);
}
};
FONT_CONTEXT.with(|font_cx| {
LAYOUT_CONTEXT.with(|layout_cx| {
let mut fcx = font_cx.borrow_mut();
let mut lcx = layout_cx.borrow_mut();
let layout = layout_text_with_contexts(
&combined,
Some(available_width as f64),
align,
&mut fcx,
&mut lcx,
);
let mut lines = layout.lines;
annotate_runs_with_urls(&mut lines, &total_text, &segments);
let base_x_offset = indent;
let mut current_page_y = self.page_context.current_y;
let mut is_first_line = true;
for line in lines {
let line_bottom_rel = current_page_y + line.line_height;
if line_bottom_rel > content_height && !self.page_context.is_empty() {
draw_segment_bg_and_border(
&mut self.page_context,
border_segment_start,
current_page_y,
);
self.page_context.finalize_current_page();
self.page_context.start_new_page();
current_page_y = 0.0;
border_segment_start = 0.0;
is_first_line = false;
}
let x_offset = if is_first_line {
is_first_line = false;
base_x_offset + first_line_indent
} else {
base_x_offset
};
let line_abs_left = content_x + x_offset + line.bounds.x0 as f32;
let line_abs_top = content_y + current_page_y;
let line_width = line.bounds.width() as f32;
let bounds = Rect::new(
line_abs_left as f64,
line_abs_top as f64,
(line_abs_left + line_width) as f64,
(line_abs_top + line.line_height) as f64,
);
self.page_context.add_element(VisualElement::TextLine {
runs: line.runs,
bounds,
line_height: line.line_height,
});
current_page_y += line.line_height;
}
let final_y = current_page_y;
draw_segment_bg_and_border(&mut self.page_context, border_segment_start, final_y);
let consumed = final_y - self.page_context.current_y;
self.page_context.consume_height(consumed);
})
});
crate::generator::box_model::end_box_content(&mut self.page_context, style);
}
pub(crate) fn layout_heading(
&mut self,
level: u8,
children: &[Node],
style: &crate::ast::Style,
) {
let segments = collect_inline_segments(children);
if segments.is_empty() {
return;
}
let total_text: String = segments.iter().map(|(t, _)| t.as_str()).collect();
let combined: Vec<(&str, &TextStyle)> =
segments.iter().map(|(t, s)| (t.as_str(), s)).collect();
let align = match style.text_align {
crate::ast::TextAlign::Left => TextAlign::Left,
crate::ast::TextAlign::Center => TextAlign::Center,
crate::ast::TextAlign::Right => TextAlign::Right,
crate::ast::TextAlign::Justify => TextAlign::Left,
};
let (indent, available_width, content_start_y, prev_indent) =
crate::generator::box_model::begin_box(
&mut self.page_context,
self.layout_ctx.current_indent,
style,
);
let available_width = available_width.max(1.0);
let (mut lines, total_line_height) = FONT_CONTEXT.with(|font_cx| {
LAYOUT_CONTEXT.with(|layout_cx| {
let mut fcx = font_cx.borrow_mut();
let mut lcx = layout_cx.borrow_mut();
let layout = layout_text_with_contexts(
&combined,
Some(available_width as f64),
align,
&mut fcx,
&mut lcx,
);
let total: f32 = layout.lines.iter().map(|l| l.line_height).sum();
(layout.lines, total)
})
});
annotate_runs_with_urls(&mut lines, &total_text, &segments);
let _total_height = total_line_height
+ style.margin.top
+ style.padding.top
+ style.padding.bottom
+ style.margin.bottom;
{
let remaining = self.page_context.remaining_height();
if total_line_height + style.padding.bottom > remaining && !self.page_context.is_empty()
{
self.page_context.start_new_page();
}
}
let content_x = self.page_context.settings.content_x();
let content_y = self.page_context.settings.content_y();
let x_position = content_x + indent;
let page_number = self.page_context.current_page.index + 1;
let y_position = content_y + self.page_context.current_y;
self.layout_ctx.record_heading(
level,
total_text.clone(),
page_number,
x_position,
y_position,
);
let bg = style.background_color.map(|color| BgStyle {
color,
padding: style.padding,
margin: style.margin,
});
let splittable = true;
let prev = self.layout_ctx.current_indent;
self.layout_ctx.current_indent = indent;
self.place_text_lines(lines, splittable, bg.as_ref(), 0.0);
self.layout_ctx.current_indent = prev;
if style.border.is_any_visible() {
}
crate::generator::box_model::end_box(
&mut self.page_context,
style,
content_start_y,
prev_indent,
);
}
pub(crate) fn layout_code(&mut self, code: &str, style: &crate::ast::Style) {
if code.is_empty() {
return;
}
let code_style = crate::ast::computed_style_to_text_style(style);
let (indent, available_width, _content_start_y, prev_indent) =
crate::generator::box_model::begin_box(
&mut self.page_context,
self.layout_ctx.current_indent,
style,
);
let available_width = available_width.max(1.0);
let (lines, _total_line_height) = FONT_CONTEXT.with(|font_cx| {
LAYOUT_CONTEXT.with(|layout_cx| {
let mut fcx = font_cx.borrow_mut();
let mut lcx = layout_cx.borrow_mut();
let max_width = if style.white_space == crate::ast::WhiteSpace::Pre {
None
} else {
Some(available_width as f64)
};
let layout = layout_text_with_contexts(
&[(code, &code_style)],
max_width,
TextAlign::Left,
&mut fcx,
&mut lcx,
);
let total: f32 = layout.lines.iter().map(|l| l.line_height).sum();
(layout.lines, total)
})
});
let content_x = self.page_context.settings.content_x();
let content_y = self.page_context.settings.content_y();
let code_x = 8.0;
let settings = self.page_context.settings.clone();
let content_height = settings.content_height();
let code_bg_color = style
.background_color
.unwrap_or_else(|| Color::new(245, 245, 245));
let has_border = style.border.is_any_visible();
let border_color = style.border.top.color;
let border_width = style.border.max_width();
let border_radius = style.border.radius;
let bg_left = content_x + prev_indent + style.margin.left;
let bg_right = content_x + settings.content_width() - style.margin.right;
let mut segment_start_y = self.page_context.current_y;
let draw_bg_and_border_segment =
|ctx: &mut crate::generator::PageContext, seg_start: f32, seg_end: f32| {
if seg_end <= seg_start {
return;
}
let abs_top = content_y + seg_start - style.padding.top;
let abs_bottom = content_y + seg_end + style.padding.bottom;
ctx.add_element_before_text(VisualElement::Rect {
rect: Rect::new(
bg_left as f64,
abs_top as f64,
bg_right as f64,
abs_bottom as f64,
),
style: crate::visual::FillStrokeStyle {
fill: Some(code_bg_color),
stroke: None,
},
});
if has_border {
let bx = bg_left - border_width;
let by = abs_top - border_width;
let bw = bg_right - bg_left + 2.0 * border_width;
let bh = abs_bottom - abs_top + 2.0 * border_width;
draw_border_inline(
ctx,
bx,
by,
bw,
bh,
border_color,
border_width,
border_radius,
);
}
};
let mut page_groups: Vec<Vec<usize>> = Vec::new();
let mut current_lines: Vec<usize> = Vec::new();
let mut cursor_y = self.page_context.current_y;
for (i, line) in lines.iter().enumerate() {
let line_bottom_rel = cursor_y + line.line_height;
if line_bottom_rel > content_height && !current_lines.is_empty() {
page_groups.push(std::mem::take(&mut current_lines));
cursor_y = 0.0;
}
current_lines.push(i);
cursor_y += line.line_height;
}
if !current_lines.is_empty() {
page_groups.push(current_lines);
}
let num_groups = page_groups.len();
for (group_idx, line_indices) in page_groups.iter().enumerate() {
let is_last = group_idx == num_groups - 1;
let group_height: f32 = line_indices.iter().map(|&i| lines[i].line_height).sum();
if group_idx > 0 {
}
let page_current_y = self.page_context.current_y;
let mut line_y = page_current_y;
for &line_idx in line_indices {
let line = &lines[line_idx];
let line_abs_left = content_x + indent + code_x + line.bounds.x0 as f32;
let line_abs_top = content_y + line_y;
let line_width = line.bounds.width() as f32;
let bounds = Rect::new(
line_abs_left as f64,
line_abs_top as f64,
(line_abs_left + line_width) as f64,
(line_abs_top + line.line_height) as f64,
);
self.page_context.add_element(VisualElement::TextLine {
runs: line.runs.clone(),
bounds,
line_height: line.line_height,
});
line_y += line.line_height;
}
if is_last {
let final_y = self.page_context.current_y + group_height;
draw_bg_and_border_segment(&mut self.page_context, segment_start_y, final_y);
self.page_context.consume_height(group_height);
} else {
let group_end_y = page_current_y + group_height;
draw_bg_and_border_segment(&mut self.page_context, segment_start_y, group_end_y);
self.page_context.consume_height(group_height);
self.page_context.start_new_page();
segment_start_y = 0.0; }
}
crate::generator::box_model::end_box_content(&mut self.page_context, style);
}
pub(crate) fn layout_image(&mut self, src: &str, alt: &str, style: &crate::ast::Style) {
let content_width = self.page_context.settings.content_width();
let content_x = self.page_context.settings.content_x();
let content_y = self.page_context.settings.content_y();
let image_result = crate::generator::image::load_image(src);
const PDF_DPI: f32 = 72.0;
const DEFAULT_IMAGE_DPI: f32 = 96.0;
let (pixel_width, pixel_height, image_data, image_format, image_dpi) = match &image_result {
Some(result) => {
let format = crate::generator::image::format_to_string(result.format);
let dpi = result.dpi.unwrap_or((DEFAULT_IMAGE_DPI, DEFAULT_IMAGE_DPI));
(
result.width,
result.height,
Some(result.data.clone()),
format,
dpi,
)
}
None => {
let pw = (content_width * DEFAULT_IMAGE_DPI / PDF_DPI) as u32;
let ph = (content_width * 0.75 * DEFAULT_IMAGE_DPI / PDF_DPI) as u32;
(
pw,
ph,
None,
"jpeg".to_string(),
(DEFAULT_IMAGE_DPI, DEFAULT_IMAGE_DPI),
)
}
};
let dpi_x = image_dpi.0;
let native_width = pixel_width as f32 * PDF_DPI / dpi_x;
let density_at_content_width = pixel_width as f32 / content_width * PDF_DPI;
let (display_width, display_height) = if (density_at_content_width >= 96.0
&& native_width < content_width)
|| native_width > content_width
{
(
content_width,
pixel_height as f32 * content_width / pixel_width as f32,
)
} else {
(
native_width,
pixel_height as f32 * native_width / pixel_width as f32,
)
};
let caption_style = TextStyle {
color: crate::visual::Color::new(102, 102, 102),
font_family: style.font_family.clone(),
font_size: 9.0,
font_weight: "normal".to_string(),
font_style: "normal".to_string(),
align: TextAlign::Center,
url: None,
decoration: crate::text::TextDecoration::None,
baseline_shift: 0.0,
background_color: None,
};
let label_height = if !alt.is_empty() {
FONT_CONTEXT.with(|font_cx| {
LAYOUT_CONTEXT.with(|layout_cx| {
let mut fcx = font_cx.borrow_mut();
let mut lcx = layout_cx.borrow_mut();
let layout = layout_text_with_contexts(
&[(alt, &caption_style)],
Some(display_width as f64),
TextAlign::Center,
&mut fcx,
&mut lcx,
);
layout.height as f32 + 4.0
})
})
} else {
0.0
};
let (_indent, _available_width, content_start_y, prev_indent) =
crate::generator::box_model::begin_box(
&mut self.page_context,
self.layout_ctx.current_indent,
style,
);
let total_content_height = display_height + label_height;
let total_height_needed = total_content_height + style.padding.bottom + style.margin.bottom;
let remaining = self.page_context.remaining_height();
let fits_on_current = display_width <= content_width && total_height_needed <= remaining;
let (target_width, target_height) = if fits_on_current {
(display_width, display_height)
} else {
let scale_w = content_width / display_width;
let scaled_h = display_height * scale_w;
let scaled_total = scaled_h + label_height + style.padding.bottom + style.margin.bottom;
if scaled_total <= remaining {
(content_width, scaled_h)
} else {
if !self.page_context.is_empty() {
self.page_context.start_new_page();
}
let remaining_h = self.page_context.remaining_height();
let total_on_new_page =
display_height + label_height + style.padding.bottom + style.margin.bottom;
if total_on_new_page <= remaining_h {
(display_width, display_height)
} else {
let scale_w = content_width / display_width;
let scale_h =
(remaining_h - label_height - style.padding.bottom - style.margin.bottom)
/ display_height;
let scale = scale_w.min(scale_h.max(0.1));
(display_width * scale, display_height * scale)
}
}
};
let top = content_y + self.page_context.current_y;
let left = content_x + (content_width - target_width) / 2.0;
self.page_context.add_element(VisualElement::Image {
position: Point::new(left as f64, top as f64),
size: vello_cpu::kurbo::Vec2::new(target_width as f64, target_height as f64),
pixel_size: (pixel_width, pixel_height),
data: image_data.unwrap_or_default(),
format: image_format,
alt: alt.to_string(),
});
let image_bottom = top + target_height;
if !alt.is_empty() {
FONT_CONTEXT.with(|font_cx| {
LAYOUT_CONTEXT.with(|layout_cx| {
let mut fcx = font_cx.borrow_mut();
let mut lcx = layout_cx.borrow_mut();
let layout = layout_text_with_contexts(
&[(alt, &caption_style)],
Some(target_width as f64),
TextAlign::Center,
&mut fcx,
&mut lcx,
);
let label_top = image_bottom + 4.0;
for line in layout.lines.iter() {
let line_y = label_top + line.bounds.y0 as f32;
let line_width = line.bounds.width() as f32;
let line_x0 = line.bounds.x0 as f32;
let bounds = Rect::new(
(left + line_x0) as f64,
line_y as f64,
(left + line_x0 + line_width) as f64,
(line_y + line.line_height) as f64,
);
self.page_context.add_element(VisualElement::TextLine {
runs: line.runs.clone(),
bounds,
line_height: line.line_height,
});
}
let actual_label_height = layout.height as f32;
self.page_context
.consume_height(target_height + 4.0 + actual_label_height);
})
});
} else {
self.page_context.consume_height(target_height);
}
crate::generator::box_model::end_box(
&mut self.page_context,
style,
content_start_y,
prev_indent,
);
}
pub(crate) fn layout_list(
&mut self,
children: &[Node],
ordered: bool,
start: u32,
style: &crate::ast::Style,
) {
self.page_context.consume_height(style.margin.top);
let list_indent = style
.list_indent_pt
.unwrap_or_else(|| crate::ast::calculate_list_indent(style.font_size_pt));
self.with_additional_indent(list_indent, |s| {
s.layout_list_with_indent(children, ordered, start, style);
});
self.page_context.consume_height(style.margin.bottom);
}
fn layout_list_with_indent(
&mut self,
children: &[Node],
ordered: bool,
start: u32,
style: &crate::ast::Style,
) {
const MARKER_GAP: f32 = 6.0;
let marker_area = if ordered {
self.calculate_ordered_marker_width(children, start, style.list_style_type)
} else {
10.0
};
let marker_base = self.layout_ctx.current_indent; let content_indent = marker_base + marker_area + MARKER_GAP;
for (index, item) in children.iter().enumerate() {
let (item_children, is_task, checked) = match &item.kind {
NodeKind::ListItem { children } => (children, false, false),
NodeKind::TaskListItem { checked, children } => (children, true, *checked),
_ => continue,
};
if item_children.is_empty() {
continue;
}
let marker = if is_task {
if checked {
"☑".to_string()
} else {
"⬜".to_string()
}
} else if ordered {
ordered_marker(style.list_style_type, start + index as u32)
} else {
unordered_marker(style.list_style_type)
};
self.with_indent(content_indent, |s| {
let mut is_first = true;
let mut inline_buf: Vec<Node> = Vec::new();
let flush_inline = |s: &mut Self, buf: &mut Vec<Node>, marker: &str| {
if buf.is_empty() {
return;
}
let para = Node::new(
NodeKind::Paragraph {
children: std::mem::take(buf),
},
style.clone(),
true,
);
s.layout_list_item_first_child(¶, style, marker);
};
for grandchild in item_children {
if is_inline_node(&grandchild.kind) {
inline_buf.push(grandchild.clone());
is_first = false;
continue;
}
flush_inline(&mut *s, &mut inline_buf, &marker);
if is_first {
s.layout_list_item_first_child(grandchild, style, &marker);
is_first = false;
} else {
s.layout_node_with_indent(grandchild, style);
}
}
flush_inline(&mut *s, &mut inline_buf, &marker);
});
}
}
fn calculate_ordered_marker_width(
&self,
children: &[Node],
start: u32,
list_style: ListStyleType,
) -> f32 {
let item_count = children.len() as u32;
let max_number = start + item_count - 1;
let max_marker = ordered_marker(list_style, max_number);
let marker_style = crate::ast::list_marker_style();
let text_style = crate::ast::computed_style_to_text_style(&marker_style);
let layout = create_text_layout(&max_marker, &text_style, None);
let width = layout.width as f32;
(width + 4.0).clamp(12.0, 30.0) }
fn layout_list_item_first_child(
&mut self,
node: &Node,
item_style: &crate::ast::Style,
marker: &str,
) {
const MARKER_GAP: f32 = 6.0;
let content_x = self.page_context.settings.content_x();
let content_y = self.page_context.settings.content_y();
let marker_style = crate::ast::list_marker_style();
let text_style = crate::ast::computed_style_to_text_style(&marker_style);
let marker_layout = create_text_layout(marker, &text_style, None);
let marker_line = marker_layout.lines.first();
let marker_advance = marker_line
.and_then(|line| line.runs.first().map(|r| r.advance))
.unwrap_or(0.0);
let content_indent = self.layout_ctx.current_indent;
let marker_left_base = content_x + content_indent - MARKER_GAP - marker_advance;
match &node.kind {
NodeKind::Paragraph { children } => {
for child in children {
if let NodeKind::Image { src, alt, title: _ } = &child.kind {
self.layout_image(src, alt, &child.style);
}
}
let segments = collect_inline_segments(children);
if segments.is_empty() {
if let Some(m_line) = marker_line {
let line_y = content_y + self.page_context.current_y;
let bounds = Rect::new(
marker_left_base as f64,
line_y as f64,
(marker_left_base + marker_advance) as f64,
(line_y + m_line.line_height) as f64,
);
self.page_context.add_element(VisualElement::TextLine {
runs: m_line.runs.clone(),
bounds,
line_height: m_line.line_height,
});
self.page_context.consume_height(m_line.line_height);
}
return;
}
let total_text: String = segments.iter().map(|(t, _)| t.as_str()).collect();
let combined: Vec<(&str, &TextStyle)> =
segments.iter().map(|(t, s)| (t.as_str(), s)).collect();
let available_width = self.page_context.settings.content_width() - content_indent;
let margin_bottom = item_style.margin.bottom;
FONT_CONTEXT.with(|font_cx| {
LAYOUT_CONTEXT.with(|layout_cx| {
let mut fcx = font_cx.borrow_mut();
let mut lcx = layout_cx.borrow_mut();
let layout = layout_text_with_contexts(
&combined,
Some(available_width as f64),
TextAlign::Left,
&mut fcx,
&mut lcx,
);
let mut lines = layout.lines;
annotate_runs_with_urls(&mut lines, &total_text, &segments);
if let Some(first_line) = lines.first() {
let first_height = first_line
.line_height
.max(marker_line.as_ref().map(|m| m.line_height).unwrap_or(0.0));
if first_height > self.page_context.remaining_height()
&& !self.page_context.is_empty()
{
self.page_context.start_new_page();
}
}
if let (Some(m_line), Some(first_line)) = (marker_line, lines.first()) {
let line_y = content_y + self.page_context.current_y;
let marker_baseline =
m_line.runs.first().map(|r| r.baseline_y).unwrap_or(0.0);
let content_baseline =
first_line.runs.first().map(|r| r.baseline_y).unwrap_or(0.0);
let baseline_offset = content_baseline - marker_baseline;
let marker_y = line_y + baseline_offset;
let marker_bounds = Rect::new(
marker_left_base as f64,
marker_y as f64,
(marker_left_base + marker_advance) as f64,
(marker_y + m_line.line_height) as f64,
);
self.page_context.add_element(VisualElement::TextLine {
runs: m_line.runs.clone(),
bounds: marker_bounds,
line_height: m_line.line_height,
});
let content_left =
content_x + content_indent + first_line.bounds.x0 as f32;
let content_bounds = Rect::new(
content_left as f64,
line_y as f64,
(content_left + first_line.bounds.width() as f32) as f64,
(line_y + first_line.line_height) as f64,
);
self.page_context.add_element(VisualElement::TextLine {
runs: first_line.runs.clone(),
bounds: content_bounds,
line_height: first_line.line_height,
});
let first_line_height = first_line
.line_height
.max(m_line.line_height + baseline_offset);
self.page_context.consume_height(first_line_height);
let settings = self.page_context.settings.clone();
let content_height = settings.content_height();
let mut current_page_y = self.page_context.current_y;
for line in lines.iter().skip(1) {
let line_bottom_rel = current_page_y + line.line_height;
if line_bottom_rel > content_height && !self.page_context.is_empty()
{
self.page_context.finalize_current_page();
self.page_context.start_new_page();
current_page_y = 0.0;
}
let line_abs_left =
content_x + content_indent + line.bounds.x0 as f32;
let line_abs_top = content_y + current_page_y;
let bounds = Rect::new(
line_abs_left as f64,
line_abs_top as f64,
(line_abs_left + line.bounds.width() as f32) as f64,
(line_abs_top + line.line_height) as f64,
);
self.page_context.add_element(VisualElement::TextLine {
runs: line.runs.clone(),
bounds,
line_height: line.line_height,
});
current_page_y += line.line_height;
}
let consumed =
current_page_y - self.page_context.current_y + margin_bottom;
self.page_context.consume_height(consumed);
}
})
});
}
NodeKind::Text { .. }
| NodeKind::Strong { .. }
| NodeKind::Emphasis { .. }
| NodeKind::Link { .. }
| NodeKind::InlineCode { .. } => {
let margin_bottom = item_style.margin.bottom;
let fake_children = vec![node.clone()];
let segments = collect_inline_segments(&fake_children);
if segments.is_empty() {
if let Some(m_line) = marker_line {
let line_y = content_y + self.page_context.current_y;
let bounds = Rect::new(
marker_left_base as f64,
line_y as f64,
(marker_left_base + marker_advance) as f64,
(line_y + m_line.line_height) as f64,
);
self.page_context.add_element(VisualElement::TextLine {
runs: m_line.runs.clone(),
bounds,
line_height: m_line.line_height,
});
self.page_context.consume_height(m_line.line_height);
}
return;
}
let total_text: String = segments.iter().map(|(t, _)| t.as_str()).collect();
let combined: Vec<(&str, &TextStyle)> =
segments.iter().map(|(t, s)| (t.as_str(), s)).collect();
let available_width = self.page_context.settings.content_width() - content_indent;
FONT_CONTEXT.with(|font_cx| {
LAYOUT_CONTEXT.with(|layout_cx| {
let mut fcx = font_cx.borrow_mut();
let mut lcx = layout_cx.borrow_mut();
let layout = layout_text_with_contexts(
&combined,
Some(available_width as f64),
TextAlign::Left,
&mut fcx,
&mut lcx,
);
let mut lines = layout.lines;
annotate_runs_with_urls(&mut lines, &total_text, &segments);
if let (Some(m_line), Some(first_line)) = (marker_line, lines.first()) {
let first_height = first_line.line_height.max(m_line.line_height);
if first_height > self.page_context.remaining_height()
&& !self.page_context.is_empty()
{
self.page_context.start_new_page();
}
let line_y = content_y + self.page_context.current_y;
let marker_baseline =
m_line.runs.first().map(|r| r.baseline_y).unwrap_or(0.0);
let content_baseline =
first_line.runs.first().map(|r| r.baseline_y).unwrap_or(0.0);
let baseline_offset = content_baseline - marker_baseline;
let marker_y = line_y + baseline_offset;
let marker_bounds = Rect::new(
marker_left_base as f64,
marker_y as f64,
(marker_left_base + marker_advance) as f64,
(marker_y + m_line.line_height) as f64,
);
self.page_context.add_element(VisualElement::TextLine {
runs: m_line.runs.clone(),
bounds: marker_bounds,
line_height: m_line.line_height,
});
let content_left =
content_x + content_indent + first_line.bounds.x0 as f32;
let content_bounds = Rect::new(
content_left as f64,
line_y as f64,
(content_left + first_line.bounds.width() as f32) as f64,
(line_y + first_line.line_height) as f64,
);
self.page_context.add_element(VisualElement::TextLine {
runs: first_line.runs.clone(),
bounds: content_bounds,
line_height: first_line.line_height,
});
let first_line_height = first_line
.line_height
.max(m_line.line_height + baseline_offset);
self.page_context.consume_height(first_line_height);
let settings = self.page_context.settings.clone();
let content_height = settings.content_height();
let mut current_page_y = self.page_context.current_y;
for line in lines.iter().skip(1) {
let line_bottom_rel = current_page_y + line.line_height;
if line_bottom_rel > content_height && !self.page_context.is_empty()
{
self.page_context.finalize_current_page();
self.page_context.start_new_page();
current_page_y = 0.0;
}
let line_abs_left =
content_x + content_indent + line.bounds.x0 as f32;
let line_abs_top = content_y + current_page_y;
let bounds = Rect::new(
line_abs_left as f64,
line_abs_top as f64,
(line_abs_left + line.bounds.width() as f32) as f64,
(line_abs_top + line.line_height) as f64,
);
self.page_context.add_element(VisualElement::TextLine {
runs: line.runs.clone(),
bounds,
line_height: line.line_height,
});
current_page_y += line.line_height;
}
let consumed =
current_page_y - self.page_context.current_y + margin_bottom;
self.page_context.consume_height(consumed);
}
})
});
}
_ => {
self.layout_node(node);
}
}
}
pub(crate) fn layout_node_with_indent(&mut self, node: &Node, style: &crate::ast::Style) {
match &node.kind {
NodeKind::Paragraph { children } => {
self.layout_paragraph(children, &node.style);
}
NodeKind::List {
ordered: child_ordered,
children,
start: child_start,
} => {
let list_indent = crate::ast::calculate_list_indent(style.font_size_pt);
self.with_additional_indent(list_indent, |s| {
s.layout_list_with_indent(
children,
*child_ordered,
child_start.unwrap_or(1),
style,
);
});
}
_ => {
self.layout_node(node);
}
}
}
pub(crate) fn layout_blockquote(&mut self, children: &[Node], style: &crate::ast::Style) {
let content_x = self.page_context.settings.content_x();
let content_y = self.page_context.settings.content_y();
let estimated_height = estimate_children_height(children)
+ 16.0
+ style.margin.top
+ style.padding.top
+ style.padding.bottom
+ style.margin.bottom;
if estimated_height > self.page_context.remaining_height() && !self.page_context.is_empty()
{
self.page_context.start_new_page();
}
let (_indent, _available_width, content_start_y, prev_indent) =
crate::generator::box_model::begin_box(
&mut self.page_context,
self.layout_ctx.current_indent,
style,
);
let border_color = if style.border.left.is_visible() {
style.border.left.color
} else {
Color::new(200, 200, 200)
};
let border_left_x = content_x;
let border_right_x = content_x + 4.0;
let mut border_segment_start = self.page_context.current_y;
let draw_border_segment =
|ctx: &mut crate::generator::PageContext, start: f32, end: f32| {
if end <= start {
return;
}
let abs_top = content_y + start;
let abs_bottom = content_y + end;
ctx.add_element(VisualElement::Rect {
rect: Rect::new(
border_left_x as f64,
abs_top as f64,
border_right_x as f64,
abs_bottom as f64,
),
style: crate::visual::FillStrokeStyle {
fill: Some(border_color),
stroke: None,
},
});
};
macro_rules! with_paging_check {
($gen:expr, $op:expr) => {{
let pre_y = $gen.page_context.current_y;
$op;
let post_y = $gen.page_context.current_y;
if post_y < pre_y {
draw_border_segment(
&mut $gen.page_context,
border_segment_start,
pre_y,
);
border_segment_start = post_y;
}
}};
}
with_paging_check!(self, self.page_context.consume_height(8.0));
self.with_indent(24.0, |s| {
for child in children {
with_paging_check!(s, s.layout_node(child));
}
});
with_paging_check!(self, self.page_context.consume_height(8.0));
let pre_end_y = self.page_context.current_y;
crate::generator::box_model::end_box(
&mut self.page_context,
style,
content_start_y,
prev_indent,
);
let post_end_y = self.page_context.current_y;
if post_end_y < pre_end_y {
draw_border_segment(&mut self.page_context, border_segment_start, pre_end_y);
border_segment_start = post_end_y;
}
let final_current_y = self.page_context.current_y;
draw_border_segment(
&mut self.page_context,
border_segment_start,
final_current_y,
);
}
pub(crate) fn layout_container(
&mut self,
children: &[Node],
style: &crate::ast::Style,
centered: bool,
) {
let (_indent, _available_width, content_start_y, prev_indent) =
crate::generator::box_model::begin_box(
&mut self.page_context,
self.layout_ctx.current_indent,
style,
);
let content_x = self.page_context.settings.content_x();
let content_y = self.page_context.settings.content_y();
let content_width = self.page_context.settings.content_width();
let bg_color = style.background_color;
let has_border = style.border.is_any_visible();
let border_color = style.border.top.color;
let border_width = style.border.max_width();
let border_radius = style.border.radius;
let bg_left = content_x + prev_indent + style.margin.left;
let bg_right = content_x + content_width - style.margin.right;
let mut border_segment_start = self.page_context.current_y;
let draw_segment_bg_and_border =
|ctx: &mut crate::generator::PageContext, seg_start: f32, seg_end: f32| {
if seg_end <= seg_start {
return;
}
let abs_top = content_y + seg_start - style.padding.top;
let abs_bottom = content_y + seg_end + style.padding.bottom;
if let Some(color) = bg_color {
ctx.add_element_before_text(VisualElement::Rect {
rect: Rect::new(
bg_left as f64,
abs_top as f64,
bg_right as f64,
abs_bottom as f64,
),
style: crate::visual::FillStrokeStyle {
fill: Some(color),
stroke: None,
},
});
}
if has_border {
let bx = bg_left - border_width;
let by = abs_top - border_width;
let bw = bg_right - bg_left + 2.0 * border_width;
let bh = abs_bottom - abs_top + 2.0 * border_width;
draw_border_inline(
ctx,
bx,
by,
bw,
bh,
border_color,
border_width,
border_radius,
);
}
};
macro_rules! with_paging_check {
($gen:expr, $op:expr) => {{
let pre_y = $gen.page_context.current_y;
$op;
let post_y = $gen.page_context.current_y;
if post_y < pre_y {
draw_segment_bg_and_border(&mut $gen.page_context, border_segment_start, pre_y);
border_segment_start = post_y;
}
}};
}
for child in children {
let mut child = child.clone();
if centered {
child.style.text_align = crate::ast::TextAlign::Center;
}
with_paging_check!(self, self.layout_node(&child));
}
let pre_end_y = self.page_context.current_y;
crate::generator::box_model::end_box_content(&mut self.page_context, style);
let post_end_y = self.page_context.current_y;
if post_end_y < pre_end_y {
draw_segment_bg_and_border(&mut self.page_context, border_segment_start, pre_end_y);
border_segment_start = post_end_y;
}
let final_y = self.page_context.current_y;
draw_segment_bg_and_border(&mut self.page_context, border_segment_start, final_y);
let _ = content_start_y;
}
pub(crate) fn layout_thematic_break(&mut self, style: &crate::ast::Style) {
let content_x = self.page_context.settings.content_x();
let content_width = self.page_context.settings.content_width();
let content_y = self.page_context.settings.content_y();
let total_height =
1.0 + style.margin.top + style.padding.top + style.padding.bottom + style.margin.bottom;
if total_height > self.page_context.remaining_height() && !self.page_context.is_empty() {
self.page_context.start_new_page();
}
let (_indent, _available_width, content_start_y, prev_indent) =
crate::generator::box_model::begin_box(
&mut self.page_context,
self.layout_ctx.current_indent,
style,
);
let line_y = content_y + self.page_context.current_y;
let line = VisualElement::Line {
start: Point::new(content_x as f64, line_y as f64),
end: Point::new((content_x + content_width) as f64, line_y as f64),
style: StrokeStyle {
color: Color::new(200, 200, 200),
width: 1.0,
},
};
self.page_context.add_element(line);
self.page_context.consume_height(1.0);
crate::generator::box_model::end_box(
&mut self.page_context,
style,
content_start_y,
prev_indent,
);
}
pub(crate) fn layout_table(&mut self, node: &Node) {
use crate::generator::table::{compute_layout_info, generate_rows};
let content_width = self.page_context.settings.content_width();
let content_x = self.page_context.settings.content_x();
let content_y = self.page_context.settings.content_y();
let table_border_w = node.style.table_border_width_pt;
let layout = compute_layout_info(node, content_width);
if layout.num_rows == 0 || layout.num_cols == 0 {
return;
}
let (indent, _available_width, _content_start_y, prev_indent) =
crate::generator::box_model::begin_box(
&mut self.page_context,
self.layout_ctx.current_indent,
&node.style,
);
let table_horizontal_offset = indent;
let table_bg_color = node.style.background_color;
let settings = &self.page_context.settings;
let bg_left = content_x + prev_indent + node.style.margin.left;
let bg_right = content_x + settings.content_width() - node.style.margin.right;
let table_total_width: f32 = layout.col_widths.iter().sum::<f32>().max(0.0);
let mut row_idx = 0;
while row_idx < layout.num_rows {
let remaining = self.page_context.remaining_height();
let mut chunk_height = 0.0_f32;
let mut end_idx = row_idx;
let pre_allocated_border = table_border_w;
while end_idx < layout.num_rows
&& chunk_height
+ layout.row_heights[end_idx]
+ table_border_w
+ pre_allocated_border
<= remaining
{
chunk_height += layout.row_heights[end_idx] + table_border_w;
end_idx += 1;
}
if end_idx == row_idx {
if !self.page_context.is_empty() {
self.page_context.start_new_page();
continue;
}
end_idx = row_idx + 1;
chunk_height = layout.row_heights[row_idx] + table_border_w + pre_allocated_border;
}
chunk_height = chunk_height.max(0.0);
let page_y = content_y + self.page_context.current_y;
let page_x = content_x + table_horizontal_offset;
let elements = generate_rows(node, &layout, row_idx, end_idx, &node.style);
if let Some(bg_color) = table_bg_color {
let chunk_top = page_y;
let mut chunk_table_height = 0.0_f32;
for i in row_idx..end_idx {
chunk_table_height += layout.row_heights[i];
}
chunk_table_height += (end_idx - row_idx + 1) as f32 * table_border_w;
let chunk_bottom = page_y + chunk_table_height;
self.page_context
.add_element_before_text(crate::visual::VisualElement::Rect {
rect: Rect::new(
bg_left as f64,
chunk_top as f64,
bg_right as f64,
chunk_bottom as f64,
),
style: crate::visual::FillStrokeStyle {
fill: Some(bg_color),
stroke: None,
},
});
}
for element in elements {
let shifted =
crate::generator::shift_element(element, page_x as f64, page_y as f64);
self.page_context.add_element(shifted);
}
self.page_context.consume_height(chunk_height);
row_idx = end_idx;
if row_idx < layout.num_rows {
self.page_context.start_new_page();
}
}
let _ = table_total_width;
crate::generator::box_model::end_box_content(&mut self.page_context, &node.style);
}
}
fn ordered_marker(style: ListStyleType, number: u32) -> String {
match style {
ListStyleType::Decimal => format!("{}.", number),
ListStyleType::DecimalLeadingZero => format!("{:02}.", number),
ListStyleType::LowerRoman => format!("{}.", to_roman(number).to_lowercase()),
ListStyleType::UpperRoman => format!("{}.", to_roman(number)),
ListStyleType::LowerAlpha => format!("{}.", to_alpha(number).to_lowercase()),
ListStyleType::UpperAlpha => format!("{}.", to_alpha(number)),
ListStyleType::None => String::new(),
_ => format!("{}.", number), }
}
fn unordered_marker(style: ListStyleType) -> String {
match style {
ListStyleType::Disc => "•".to_string(),
ListStyleType::Circle => "○".to_string(),
ListStyleType::Square => "■".to_string(),
ListStyleType::None => String::new(),
_ => "•".to_string(), }
}
fn to_roman(mut num: u32) -> String {
let values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
let numerals = [
"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I",
];
let mut result = String::new();
for (i, &v) in values.iter().enumerate() {
while num >= v {
result.push_str(numerals[i]);
num -= v;
}
}
result
}
fn to_alpha(num: u32) -> String {
let mut n = num;
let mut result = String::new();
while n > 0 {
n -= 1;
result.insert(0, char::from_u32(65 + (n % 26)).unwrap_or('A'));
n /= 26;
}
result
}
#[allow(clippy::too_many_arguments)]
fn draw_border_inline(
ctx: &mut crate::generator::PageContext,
x: f32,
y: f32,
width: f32,
height: f32,
border_color: Color,
border_width: f32,
radius: f32,
) {
if radius > 0.0 {
let r = radius as f64;
ctx.add_element(VisualElement::RoundedRect {
rect: Rect::new(x as f64, y as f64, (x + width) as f64, (y + height) as f64),
radii: (r, r, r, r),
style: crate::visual::FillStrokeStyle {
fill: None,
stroke: Some(crate::visual::Stroke::new(
border_color,
border_width as f64,
)),
},
});
} else {
let x = x as f64;
let y = y as f64;
let w = width as f64;
let h = height as f64;
let bw = border_width as f64;
ctx.add_element(VisualElement::Rect {
rect: Rect::new(x, y, x + w, y + bw),
style: crate::visual::FillStrokeStyle {
fill: Some(border_color),
stroke: None,
},
});
ctx.add_element(VisualElement::Rect {
rect: Rect::new(x, y + h - bw, x + w, y + h),
style: crate::visual::FillStrokeStyle {
fill: Some(border_color),
stroke: None,
},
});
ctx.add_element(VisualElement::Rect {
rect: Rect::new(x, y, x + bw, y + h),
style: crate::visual::FillStrokeStyle {
fill: Some(border_color),
stroke: None,
},
});
ctx.add_element(VisualElement::Rect {
rect: Rect::new(x + w - bw, y, x + w, y + h),
style: crate::visual::FillStrokeStyle {
fill: Some(border_color),
stroke: None,
},
});
}
}