use super::*;
use crate::layout::elements::{LayoutNode, LayoutVisitor, TextBlock};
pub(super) fn run_vertical_align_shift(run: &TextRun, parent_font_size: f32) -> f32 {
if run.inline_box.is_some() {
return 0.0;
}
run.glyph_baseline_shift(parent_font_size)
}
pub(super) fn run_line_box_baseline_shift(run: &TextRun, parent_font_size: f32) -> f32 {
if run.inline_box.is_some() {
return 0.0;
}
run.vertical_align_shift(parent_font_size)
}
pub(super) fn run_line_height_for_vertical_align(run: &TextRun) -> f32 {
let factor = if run.line_height_factor.is_finite() {
run.line_height_factor.max(0.0)
} else {
1.2
};
run.line_height_font_size() * factor
}
pub(super) fn is_drop_cap_run(run: &TextRun) -> bool {
run.inline_box.is_none() && run.metadata.is_drop_cap
}
pub(super) fn run_glyph_top(run: &TextRun, custom_fonts: &HashMap<String, TtfFont>) -> f32 {
let ch = run.text.chars().find(|c| !c.is_whitespace());
if let (Some(ch), FontFamily::Custom(name)) = (ch, &run.font_family)
&& let Some((_, ttf)) = crate::system_fonts::find_font(
custom_fonts,
name,
run.bold,
run.font_style.is_slanted(),
)
&& let Some(ratio) = ttf.glyph_top_ratio(ch)
{
return ratio * run.font_size;
}
let (ascender_ratio, _) = crate::fonts::font_metrics_ratios(
&run.font_family,
run.bold,
run.font_style.is_slanted(),
custom_fonts,
);
ascender_ratio * run.font_size
}
pub(super) fn drop_cap_baseline_shift(
run: &TextRun,
line_text_top: f32,
custom_fonts: &HashMap<String, TtfFont>,
) -> f32 {
if !is_drop_cap_run(run) {
return 0.0;
}
let cap_top = run_glyph_top(run, custom_fonts);
(line_text_top - cap_top).min(0.0)
}
pub(super) fn line_text_top(line: &TextLine, custom_fonts: &HashMap<String, TtfFont>) -> f32 {
line.runs
.iter()
.filter(|r| r.inline_box.is_none() && !is_drop_cap_run(r) && !r.text.trim().is_empty())
.map(|r| run_glyph_top(r, custom_fonts))
.fold(0.0f32, f32::max)
}
#[derive(Clone, Copy)]
pub(super) struct LineBoxMetrics {
pub(super) ascender: f32,
pub(super) descender: f32,
pub(super) half_leading: f32,
}
pub(super) fn line_shifted_text_extents(
line: &TextLine,
parent_font_size: f32,
custom_fonts: &HashMap<String, TtfFont>,
) -> (f32, f32) {
let rep_factor = line
.runs
.iter()
.filter(|r| r.inline_box.is_none() && !is_drop_cap_run(r))
.map(|r| r.line_height_factor)
.fold(0.0f32, f32::max);
let rep_factor = if rep_factor > 0.0 { rep_factor } else { 1.2 };
let mut above = 0.0f32;
let mut below = 0.0f32;
for run in line.runs.iter().filter(|r| r.inline_box.is_none()) {
if is_drop_cap_run(run) {
continue;
}
let (asc_r, desc_r) = crate::fonts::font_metrics_ratios(
&run.font_family,
run.bold,
run.font_style.is_slanted(),
custom_fonts,
);
let logical_font_size = run.line_height_font_size();
let asc = asc_r * logical_font_size;
let desc = desc_r * logical_font_size;
let factor = if run.line_height_factor.is_finite() {
run.line_height_factor
} else {
rep_factor
};
let half = ((run.line_height_font_size() * factor - (asc + desc)) / 2.0).max(0.0);
let shift = run_line_box_baseline_shift(run, parent_font_size);
above = above.max(asc + half + shift);
below = below.max(desc + half - shift);
}
(above, below)
}
pub(super) fn line_authored_text_extents(
line: &TextLine,
custom_fonts: &HashMap<String, TtfFont>,
) -> (f32, f32) {
line.runs
.iter()
.filter(|r| r.inline_box.is_none())
.filter(|r| !is_drop_cap_run(r))
.fold((0.0f32, 0.0f32), |(above, below), run| {
let (asc_r, desc_r) = crate::fonts::font_metrics_ratios(
&run.font_family,
run.bold,
run.font_style.is_slanted(),
custom_fonts,
);
let logical_font_size = run.line_height_font_size();
let asc = asc_r * logical_font_size;
let desc = desc_r * logical_font_size;
let half = (run_line_height_for_vertical_align(run) - (asc + desc)) / 2.0;
(above.max(asc + half), below.max(desc + half))
})
}
pub(super) fn line_box_metrics(
line: &TextLine,
custom_fonts: &HashMap<String, TtfFont>,
) -> LineBoxMetrics {
line_box_metrics_with_resolved_baseline(line, custom_fonts, true)
}
pub(super) fn page_margin_line_box_metrics(
line: &TextLine,
custom_fonts: &HashMap<String, TtfFont>,
) -> LineBoxMetrics {
line_box_metrics_with_resolved_baseline(line, custom_fonts, false)
}
fn line_box_metrics_with_resolved_baseline(
line: &TextLine,
custom_fonts: &HashMap<String, TtfFont>,
use_resolved_baseline: bool,
) -> LineBoxMetrics {
if use_resolved_baseline && let Some(baseline_ascent) = line.baseline_ascent {
return LineBoxMetrics {
ascender: baseline_ascent,
descender: (line.height - baseline_ascent).max(0.0),
half_leading: 0.0,
};
}
let parent_font_size = crate::layout::text::line_primary_font_size(&line.runs);
let (ascender, descender) = line
.runs
.iter()
.filter(|r| r.inline_box.is_none())
.filter(|r| !is_drop_cap_run(r))
.fold((0.0f32, 0.0f32), |(max_ascender, max_descender), run| {
let (ascender_ratio, descender_ratio) = crate::fonts::font_metrics_ratios(
&run.font_family,
run.bold,
run.font_style.is_slanted(),
custom_fonts,
);
(
max_ascender.max(ascender_ratio * run.line_height_font_size()),
max_descender.max(descender_ratio * run.line_height_font_size()),
)
});
let strut_half_leading = (line.height - (ascender + descender)) / 2.0;
let has_text_shift = line.runs.iter().any(|run| {
run.inline_box.is_none() && run_line_box_baseline_shift(run, parent_font_size) != 0.0
});
let (mut above, mut below) = if has_text_shift {
line_shifted_text_extents(line, parent_font_size, custom_fonts)
} else {
let authored = line_authored_text_extents(line, custom_fonts);
if authored.0 + authored.1 > 0.0 {
authored
} else {
(
ascender + strut_half_leading,
descender + strut_half_leading,
)
}
};
let authored_total = above + below;
if authored_total > 0.0 && line.height > authored_total {
below += line.height - authored_total;
}
let line_x_height = line_primary_x_height_ratio(&line.runs, custom_fonts) * parent_font_size;
for run in &line.runs {
if let Some(inline) = run.inline_box.as_deref()
&& matches!(
inline.vertical_align,
VerticalAlign::Baseline
| VerticalAlign::Sub
| VerticalAlign::Super
| VerticalAlign::Length(_)
| VerticalAlign::Percent(_)
| VerticalAlign::Middle
)
{
let box_ascent = inline.baseline_ascent.unwrap_or(inline.height);
let box_descent = (inline.height - box_ascent).max(0.0);
let (box_above, box_below) = match inline.vertical_align {
VerticalAlign::Sub => (
box_ascent - parent_font_size * SUB_SHIFT_RATIO,
box_descent + parent_font_size * SUB_SHIFT_RATIO,
),
VerticalAlign::Super => (
box_ascent + parent_font_size * SUPER_SHIFT_RATIO,
box_descent - parent_font_size * SUPER_SHIFT_RATIO,
),
VerticalAlign::Length(v) => (box_ascent + v, box_descent - v),
VerticalAlign::Percent(p) => {
let shift = run_line_height_for_vertical_align(run) * p;
(box_ascent + shift, box_descent - shift)
}
VerticalAlign::Middle => (
inline.height / 2.0 + line_x_height / 2.0,
inline.height / 2.0 - line_x_height / 2.0,
),
_ => (box_ascent, box_descent),
};
above = above.max(box_above.max(0.0));
below = below.max(box_below.max(0.0));
}
}
LineBoxMetrics {
ascender: above,
descender: below,
half_leading: 0.0,
}
}
pub(super) fn flex_cell_align(cell: &FlexCell, align_items: AlignItems) -> AlignItems {
cell.effective_cross_alignment(align_items)
}
pub(super) fn flex_cell_baseline(
cell: &FlexCell,
custom_fonts: &HashMap<String, TtfFont>,
) -> Option<f32> {
let mut prior = 0.0;
let last = cell
.lines
.iter()
.filter(|line| line.runs.iter().any(|run| !run.text.is_empty()))
.inspect(|line| prior += line.height)
.last();
let Some(last) = last else {
return cell
.nested_elements
.iter()
.find_map(|element| element.atomic_inline_baseline())
.map(|baseline| cell.border.top.width + cell.padding.top + baseline.baseline_offset());
};
prior -= last.height;
let metrics = line_box_metrics(last, custom_fonts);
Some(cell.border.top.width + cell.padding.top + prior + metrics.half_leading + metrics.ascender)
}
pub(super) fn flex_line_max_baseline(
cells: &[FlexCell],
line_id: FlexLineId,
align_items: AlignItems,
custom_fonts: &HashMap<String, TtfFont>,
) -> Option<f32> {
cells
.iter()
.filter(|cell| {
cell.line_id == line_id && flex_cell_align(cell, align_items) == AlignItems::Baseline
})
.filter_map(|cell| flex_cell_baseline(cell, custom_fonts))
.reduce(f32::max)
}
pub(super) fn upright_vertical_line_metrics(
line: &TextLine,
custom_fonts: &HashMap<String, TtfFont>,
) -> LineBoxMetrics {
let (ascender, descender) = line
.runs
.iter()
.filter(|r| r.inline_box.is_none())
.filter(|r| !is_drop_cap_run(r))
.fold((0.0f32, 0.0f32), |(max_ascender, max_descender), run| {
let (ascender_ratio, descender_ratio) =
if crate::text::contains_cjk_vertical_text(&run.text) {
crate::text::upright_vertical_font_metrics(run, custom_fonts).map_or_else(
|| {
crate::fonts::font_metrics_ratios(
&run.font_family,
run.bold,
run.font_style.is_slanted(),
custom_fonts,
)
},
|metrics| metrics.line_ratios(),
)
} else {
crate::fonts::font_metrics_ratios(
&run.font_family,
run.bold,
run.font_style.is_slanted(),
custom_fonts,
)
};
(
max_ascender.max(ascender_ratio * run.font_size),
max_descender.max(descender_ratio * run.font_size),
)
});
if ascender + descender == 0.0 {
return line_box_metrics(line, custom_fonts);
}
let square_unit = line.runs.iter().any(|run| {
run.metadata.text_combine_upright.is_active()
|| (run.inline_box.is_none()
&& run
.text
.chars()
.any(|ch| !ch.is_ascii() && !ch.is_whitespace()))
});
let half_leading = (line.height - (ascender + descender)) / 2.0;
let half_leading = if square_unit {
half_leading
} else {
half_leading.max(0.0)
};
LineBoxMetrics {
ascender: ascender + half_leading,
descender: descender + half_leading,
half_leading: 0.0,
}
}
pub(super) fn line_text_content_extents(
line: &TextLine,
custom_fonts: &HashMap<String, TtfFont>,
) -> (f32, f32) {
line.runs
.iter()
.filter(|r| r.inline_box.is_none())
.filter(|r| !is_drop_cap_run(r))
.fold((0.0f32, 0.0f32), |(max_ascent, max_descent), run| {
let (ascender_ratio, descender_ratio) = crate::fonts::font_metrics_ratios(
&run.font_family,
run.bold,
run.font_style.is_slanted(),
custom_fonts,
);
(
max_ascent.max(ascender_ratio * run.font_size),
max_descent.max(descender_ratio * run.font_size),
)
})
}
pub(super) fn estimate_line_width_with_fonts(
line: &TextLine,
custom_fonts: &HashMap<String, TtfFont>,
) -> f32 {
line.runs
.iter()
.map(|r| {
let text_w = estimate_run_width_with_fonts(r, custom_fonts);
text_w + r.padding.horizontal()
})
.sum()
}
pub(crate) fn sanitize_pdf_name(name: &str) -> String {
name.chars()
.filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
.collect()
}
pub(super) fn line_text_content(line: &TextLine) -> String {
line.runs.iter().map(|r| r.text.as_str()).collect()
}
pub(super) fn fixed_textblock_flow_adjustments(elements: &[(f32, LayoutNode)]) -> Vec<f32> {
let mut adjustment = 0.0;
elements
.iter()
.map(|(_, element)| {
let current = adjustment;
if !element.is_page_paint_continuation() {
adjustment += fixed_textblock_flow_overage(element);
}
current
})
.collect()
}
pub(super) fn element_uses_flow_y_adjustment(element: &dyn LayoutElement) -> bool {
if element.is_page_paint_continuation() {
return false;
}
struct UsesAdjustment(bool);
impl LayoutVisitor for UsesAdjustment {
fn visit_text_block(&mut self, element: &TextBlock) {
self.0 = !element.positioning.scheme.is_absolute();
}
}
let mut uses = UsesAdjustment(true);
element.accept(&mut uses);
uses.0
}
pub(super) fn fixed_textblock_flow_overage(element: &dyn LayoutElement) -> f32 {
#[derive(Default)]
struct Overage(f32);
impl LayoutVisitor for Overage {
fn visit_text_block(&mut self, element: &TextBlock) {
if !element.box_model.size.height.is_definite() {
return;
}
let Some(block_height) = element.box_model.size.height.used() else {
return;
};
if element.positioning.scheme.is_absolute()
|| element.flow.float != Float::None
|| element.clipping.rect.is_some()
{
return;
}
let text_height = element.lines.iter().map(|line| line.height).sum::<f32>();
let content_height = element.box_model.padding.vertical() + text_height;
self.0 = (content_height - block_height).max(0.0);
}
}
let mut overage = Overage::default();
element.accept(&mut overage);
overage.0
}
pub(super) fn text_block_total_height(
lines: &[TextLine],
padding: EdgeSizes,
block_height: Option<f32>,
_clips: bool,
) -> f32 {
let text_height: f32 = lines.iter().map(|line| line.height).sum();
let content_h = padding.vertical() + text_height;
block_height.unwrap_or(content_h)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn page_margin_metrics_do_not_reuse_a_source_flow_baseline() {
let line = TextLine {
runs: vec![TextRun {
text: "margin".to_string(),
font_size: 9.0,
font_family: FontFamily::Helvetica,
line_height_factor: 1.5,
..Default::default()
}],
height: 13.5,
baseline_ascent: Some(9.75),
..Default::default()
};
let fonts = HashMap::new();
assert_eq!(line_box_metrics(&line, &fonts).ascender, 9.75);
assert!((page_margin_line_box_metrics(&line, &fonts).ascender - 9.049_5).abs() < 0.000_1);
}
#[test]
fn upright_metrics_keep_ascii_in_its_one_em_slot() {
let line = TextLine {
runs: vec![TextRun {
text: "12".to_string(),
font_size: 24.0,
font_family: FontFamily::Helvetica,
metadata: crate::layout::engine::TextRunMetadata {
text_combine_upright: crate::style::computed::TextCombineUpright::All,
..Default::default()
},
..Default::default()
}],
height: 20.0,
..Default::default()
};
let fonts = HashMap::new();
let metrics = upright_vertical_line_metrics(&line, &fonts);
assert!((metrics.ascender + metrics.descender - line.height).abs() < f32::EPSILON);
}
}