use std::collections::HashMap;
use pdf_writer::Content;
use crate::model::{Footnote, LineSpacing, Paragraph, Run};
use super::RenderContext;
use super::color::stroke_segment;
use super::helpers::{effective_space_after, effective_space_before};
use super::layout::{
LineOpts, LinkTagger, TextLine, boxed_line_ascent, build_lines, is_text_empty, lines_height,
os2_strike, render_paragraph_lines, size_lines_by_own_runs, tallest_run_metrics,
};
use super::list_label::render_list_label;
use super::resolve_line_h;
use super::tagging::NoteTagger;
pub(super) fn note_anchor(endnote: bool, id: u32) -> String {
format!("{} {id}", if endnote { "endnote" } else { "footnote" })
}
fn substitute_ref_marks(runs: &[Run], display_num: &str) -> Vec<Run> {
runs.iter()
.map(|run| {
if run.is_footnote_ref_mark || run.is_endnote_ref_mark {
let mut r = run.clone();
r.text = display_num.to_string();
r
} else {
run.clone()
}
})
.collect()
}
struct ParagraphLayout {
font_size: f32,
line_height: f32,
ascender_ratio: f32,
lines: Vec<TextLine>,
}
impl ParagraphLayout {
fn height(&self) -> f32 {
lines_height(
&self.lines,
self.line_height,
(self.font_size * self.ascender_ratio, 0.0),
)
}
}
fn layout_paragraph(
runs: &[Run],
para: &Paragraph,
line_spacing: LineSpacing,
ctx: &RenderContext,
text_width: f32,
first_line_hanging: f32,
) -> Option<ParagraphLayout> {
if is_text_empty(runs) {
return None;
}
let (fs, tallest_lhr, tallest_ar) = tallest_run_metrics(runs, ctx.fonts);
let lh = resolve_line_h(line_spacing, fs, tallest_lhr);
let mut lines = build_lines(
runs,
ctx,
text_width,
ctx.cjk(true, para.alignment),
&LineOpts {
tab_stops: ¶.tab_stops,
indent_left: para.indent_left,
indent_right: para.indent_right,
hanging: first_line_hanging,
..Default::default()
},
);
let ascender_ratio = tallest_ar.unwrap_or(0.75);
if !matches!(line_spacing, LineSpacing::Exact(_)) {
size_lines_by_own_runs(&mut lines, ctx.fonts, line_spacing, lh, fs * ascender_ratio);
}
if lines.is_empty() {
return None;
}
Some(ParagraphLayout {
font_size: fs,
line_height: lh,
ascender_ratio,
lines,
})
}
fn empty_paragraph_line_h(para: &Paragraph, ls: LineSpacing, ctx: &RenderContext) -> f32 {
let (fs, lhr, _) = tallest_run_metrics(¶.runs, ctx.fonts);
resolve_line_h(ls, fs, lhr)
}
pub(super) fn footnote_height(
id: u32,
footnotes: &HashMap<u32, Footnote>,
ctx: &RenderContext,
text_width: f32,
) -> f32 {
let Some(footnote) = footnotes.get(&id) else {
return 0.0;
};
let mark = ctx.footnote_marks.get(&id).map_or("1", String::as_str);
let mut total = 0.0f32;
let mut prev_space_after = 0.0f32;
let mut prev_para = None;
for (i, para) in footnote.paragraphs.iter().enumerate() {
let ls = para.line_spacing.unwrap_or(ctx.doc_line_spacing);
let para_text_width = (text_width - para.indent_left - para.indent_right).max(1.0);
let hanging = super::compute_text_hanging(para, 0.0, ctx.fonts);
let runs = substitute_ref_marks(¶.runs, mark);
let layout = layout_paragraph(&runs, para, ls, ctx, para_text_width, hanging);
if layout.is_none() && para.paragraph_mark_vanish {
continue;
}
if i > 0 {
total += f32::max(prev_space_after, effective_space_before(para, prev_para));
}
total += layout.as_ref().map_or_else(
|| empty_paragraph_line_h(para, ls, ctx),
ParagraphLayout::height,
);
prev_space_after = effective_space_after(para, footnote.paragraphs.get(i + 1));
prev_para = Some(para);
}
total + prev_space_after
}
pub(crate) struct NoteSeparator {
pub(super) height: f32,
rule: (f32, f32),
}
impl NoteSeparator {
pub(super) fn new(
separator: Option<&Paragraph>,
fonts: &HashMap<String, crate::fonts::FontEntry>,
doc_line_spacing: LineSpacing,
) -> Self {
let Some(p) = separator else {
return Self {
height: 12.0,
rule: (3.0, 0.5),
};
};
let ls = p.line_spacing.unwrap_or(doc_line_spacing);
let (fs, lhr, ar) = tallest_run_metrics(&p.runs, fonts);
let line_h = resolve_line_h(ls, fs, lhr);
let baseline = boxed_line_ascent(ls, line_h, fs, lhr, ar, &p.runs, fonts)
.unwrap_or(fs * ar.unwrap_or(0.8));
let (top, thickness) = p
.runs
.first()
.and_then(|r| fonts.get(&crate::fonts::font_key(r)))
.and_then(|e| os2_strike(e, fs))
.unwrap_or((fs * 0.25, 0.5));
Self {
height: line_h + p.space_after,
rule: (baseline - top + thickness / 2.0, thickness),
}
}
}
pub(super) fn render_page_footnotes(
content: &mut Content,
fn_ids: &[u32],
footnotes: &HashMap<u32, Footnote>,
footnote_display_order: &HashMap<u32, String>,
ctx: &RenderContext,
margin_left: f32,
margin_bottom: f32,
text_width: f32,
gradient_specs: &mut Vec<super::GradientSpec>,
notes: NoteTagger<'_>,
) -> Vec<(u32, f32)> {
if fn_ids.is_empty() {
return Vec::new();
}
let total_fn_height: f32 = fn_ids
.iter()
.map(|&id| footnote_height(id, footnotes, ctx, text_width))
.sum();
let fn_y = margin_bottom + total_fn_height;
let separator = &ctx.note_separator;
let (rule_centre, thickness) = separator.rule;
let sep_y = fn_y + separator.height - rule_centre;
draw_note_separator(content, margin_left, sep_y, text_width, thickness);
render_notes_downward(
content,
fn_y,
fn_ids,
footnotes,
footnote_display_order,
ctx,
margin_left,
text_width,
gradient_specs,
notes,
)
}
fn draw_note_separator(
content: &mut Content,
margin_left: f32,
sep_y: f32,
text_width: f32,
thickness: f32,
) {
let sep_width = 144.0f32.min(text_width);
stroke_segment(
content,
(margin_left, sep_y),
(margin_left + sep_width, sep_y),
thickness,
None,
);
}
pub(super) fn render_endnotes_inline(
content: &mut Content,
top_y: f32,
en_ids: &[u32],
endnotes: &HashMap<u32, Footnote>,
endnote_display_order: &HashMap<u32, String>,
ctx: &RenderContext,
margin_left: f32,
text_width: f32,
gradient_specs: &mut Vec<super::GradientSpec>,
notes: NoteTagger<'_>,
) -> Vec<(u32, f32)> {
if en_ids.is_empty() {
return Vec::new();
}
let sep_y = top_y;
draw_note_separator(content, margin_left, sep_y, text_width, 0.5);
let fn_y = sep_y - 9.0;
render_notes_downward(
content,
fn_y,
en_ids,
endnotes,
endnote_display_order,
ctx,
margin_left,
text_width,
gradient_specs,
notes,
)
}
#[allow(clippy::too_many_arguments)]
fn render_notes_downward(
content: &mut Content,
mut fn_y: f32,
fn_ids: &[u32],
footnotes: &HashMap<u32, Footnote>,
footnote_display_order: &HashMap<u32, String>,
ctx: &RenderContext,
margin_left: f32,
text_width: f32,
gradient_specs: &mut Vec<super::GradientSpec>,
notes: NoteTagger<'_>,
) -> Vec<(u32, f32)> {
let mut tops = Vec::new();
for fn_id in fn_ids {
let Some(footnote) = footnotes.get(fn_id) else {
continue;
};
tops.push((*fn_id, fn_y));
let note = notes.tags.note(notes.endnote, *fn_id, super::tagging::ROOT);
let display_num = footnote_display_order
.get(fn_id)
.cloned()
.unwrap_or_else(|| "1".to_string());
let mut prev_space_after = 0.0f32;
let mut prev_para = None;
for (pi, para) in footnote.paragraphs.iter().enumerate() {
let runs = substitute_ref_marks(¶.runs, &display_num);
let ls = para.line_spacing.unwrap_or(ctx.doc_line_spacing);
let para_text_x = margin_left + para.indent_left;
let para_text_width = (text_width - para.indent_left - para.indent_right).max(1.0);
let hanging = super::compute_text_hanging(para, 0.0, ctx.fonts);
let layout = layout_paragraph(&runs, para, ls, ctx, para_text_width, hanging);
if layout.is_none() && para.paragraph_mark_vanish {
continue;
}
if pi > 0 {
fn_y -= f32::max(prev_space_after, effective_space_before(para, prev_para));
}
if let Some(layout) = layout {
let baseline_y = fn_y - layout.font_size * layout.ascender_ratio;
let line_count = layout.lines.len();
let p = notes.tags.add(note, "P");
notes.tags.begin(content, notes.page, p);
render_list_label(
content,
para,
ctx.fonts,
para_text_x - para.indent_hanging,
baseline_y,
layout.font_size,
);
render_paragraph_lines(
content,
&layout.lines,
¶.alignment,
para_text_x,
para_text_width,
baseline_y,
layout.line_height,
(layout.font_size * layout.ascender_ratio, 0.0),
line_count,
0,
notes.links,
hanging,
ctx.fonts,
None,
gradient_specs,
None,
None,
Some(LinkTagger::new(notes.tags, notes.page, p)),
);
super::tagging::Tags::end(content);
fn_y -= layout.height();
} else {
fn_y -= empty_paragraph_line_h(para, ls, ctx);
}
prev_space_after = effective_space_after(para, footnote.paragraphs.get(pi + 1));
prev_para = Some(para);
}
fn_y -= prev_space_after;
}
tops
}