use crate::style::computed::FontFamily;
use crate::{parser::ttf::TtfFont, system_fonts, text};
use std::collections::HashMap;
static HELVETICA_WIDTHS: [u16; 95] = [
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556, 333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556, 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, ];
static HELVETICA_BOLD_WIDTHS: [u16; 95] = [
278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611, 975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556, 333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611, 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, ];
const DEFAULT_WIDTH: u16 = 556;
pub(crate) fn ascender_ratio(font_family: &FontFamily) -> f32 {
match font_family {
FontFamily::Helvetica | FontFamily::Custom(_) => 0.718,
FontFamily::TimesRoman => 0.683,
FontFamily::Courier => 0.629,
}
}
pub(crate) fn descender_ratio(font_family: &FontFamily) -> f32 {
match font_family {
FontFamily::Helvetica | FontFamily::Custom(_) => 0.207,
FontFamily::TimesRoman => 0.217,
FontFamily::Courier => 0.157,
}
}
pub(crate) fn font_metrics_ratios(
font_family: &FontFamily,
bold: bool,
italic: bool,
custom_fonts: &HashMap<String, TtfFont>,
) -> (f32, f32) {
if let FontFamily::Custom(name) = font_family {
if let Some((_, ttf)) = system_fonts::find_font(custom_fonts, name, bold, italic) {
let metrics = ttf.pdf_vertical_metrics();
return (
metrics.ascender_ratio(ttf.units_per_em),
metrics.descender_ratio(ttf.units_per_em),
);
}
}
(ascender_ratio(font_family), descender_ratio(font_family))
}
pub(crate) fn normal_line_height_factor(
font_family: &FontFamily,
bold: bool,
italic: bool,
custom_fonts: &HashMap<String, TtfFont>,
) -> f32 {
if matches!(font_family, FontFamily::Custom(_)) {
if let Some(height) = text::custom_font_line_height(font_family, bold, italic, custom_fonts)
{
return height;
}
let (ascender, descender) = font_metrics_ratios(font_family, bold, italic, custom_fonts);
return (ascender + descender).max(1.0);
}
1.2
}
const COURIER_WIDTH: u16 = 600;
pub(crate) fn char_width(ch: char, font_size: f32, font_family: &FontFamily, bold: bool) -> f32 {
let afm = match font_family {
FontFamily::Courier => COURIER_WIDTH,
FontFamily::Helvetica | FontFamily::Custom(_) => helvetica_char_afm(ch, bold),
FontFamily::TimesRoman => {
helvetica_char_afm(ch, bold)
}
};
afm as f32 / 1000.0 * font_size
}
pub(crate) fn str_width(s: &str, font_size: f32, font_family: &FontFamily, bold: bool) -> f32 {
s.chars()
.map(|c| char_width(c, font_size, font_family, bold))
.sum()
}
pub(crate) fn pdf_font_name(base_family: &str, bold: bool, italic: bool) -> &'static str {
let normalized = base_family.trim();
let is_times = normalized.eq_ignore_ascii_case("Times-Roman")
|| normalized.eq_ignore_ascii_case("Times")
|| normalized.eq_ignore_ascii_case("Times-Bold")
|| normalized.eq_ignore_ascii_case("Times-Italic")
|| normalized.eq_ignore_ascii_case("Times-BoldItalic")
|| normalized.eq_ignore_ascii_case("Times New Roman");
let is_courier = normalized.eq_ignore_ascii_case("Courier")
|| normalized.eq_ignore_ascii_case("Courier-Bold")
|| normalized.eq_ignore_ascii_case("Courier-Oblique")
|| normalized.eq_ignore_ascii_case("Courier-BoldOblique")
|| normalized.eq_ignore_ascii_case("Courier New");
if is_times {
match (bold, italic) {
(true, true) => "Times-BoldItalic",
(true, false) => "Times-Bold",
(false, true) => "Times-Italic",
(false, false) => "Times-Roman",
}
} else if is_courier {
match (bold, italic) {
(true, true) => "Courier-BoldOblique",
(true, false) => "Courier-Bold",
(false, true) => "Courier-Oblique",
(false, false) => "Courier",
}
} else {
match (bold, italic) {
(true, true) => "Helvetica-BoldOblique",
(true, false) => "Helvetica-Bold",
(false, true) => "Helvetica-Oblique",
(false, false) => "Helvetica",
}
}
}
#[cfg(test)]
fn pdf_font_name_for_family(font_family: &FontFamily, bold: bool, italic: bool) -> &'static str {
let base_family = match font_family {
FontFamily::Helvetica | FontFamily::Custom(_) => "Helvetica",
FontFamily::TimesRoman => "Times-Roman",
FontFamily::Courier => "Courier",
};
pdf_font_name(base_family, bold, italic)
}
fn helvetica_char_afm(ch: char, bold: bool) -> u16 {
let code = ch as u32;
if (32..=126).contains(&code) {
let idx = (code - 32) as usize;
if bold {
HELVETICA_BOLD_WIDTHS[idx]
} else {
HELVETICA_WIDTHS[idx]
}
} else if is_cjk_char(code) || is_fullwidth_char(code) {
1000
} else if is_emoji_char(code) {
1000
} else if (0x0590..=0x08FF).contains(&code) {
if bold { 600 } else { 556 }
} else if (0x0370..=0x03FF).contains(&code) {
if bold { 600 } else { 556 }
} else if (0x2000..=0x206F).contains(&code) {
match code {
0x2013 => 500, 0x2014 => 1000, 0x2018 | 0x2019 => 222, 0x201C | 0x201D => 333, 0x2026 => 1000, _ => DEFAULT_WIDTH,
}
} else if (0x2500..=0x257F).contains(&code) {
600
} else {
DEFAULT_WIDTH
}
}
fn is_cjk_char(code: u32) -> bool {
matches!(code,
0x4E00..=0x9FFF | 0x3400..=0x4DBF | 0x3000..=0x303F | 0x3040..=0x309F | 0x30A0..=0x30FF | 0x31F0..=0x31FF | 0xAC00..=0xD7AF | 0xF900..=0xFAFF | 0x20000..=0x2A6DF )
}
fn is_fullwidth_char(code: u32) -> bool {
(0xFF01..=0xFF60).contains(&code) || (0xFFE0..=0xFFE6).contains(&code)
}
fn is_emoji_char(code: u32) -> bool {
matches!(code,
0x1F600..=0x1F64F | 0x1F300..=0x1F5FF | 0x1F680..=0x1F6FF | 0x1F1E0..=0x1F1FF | 0x2600..=0x26FF | 0x2700..=0x27BF | 0x1F900..=0x1F9FF | 0x1FA00..=0x1FA6F | 0x1FA70..=0x1FAFF | 0xFE00..=0xFE0F | 0x200D )
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn helvetica_space_width() {
let w = char_width(' ', 10.0, &FontFamily::Helvetica, false);
assert!((w - 2.78).abs() < 0.01);
}
#[test]
fn helvetica_bold_a_wider_than_regular() {
let regular = char_width('A', 12.0, &FontFamily::Helvetica, false);
let bold = char_width('A', 12.0, &FontFamily::Helvetica, true);
assert!(bold > regular);
}
#[test]
fn courier_fixed_width() {
let w1 = char_width('i', 10.0, &FontFamily::Courier, false);
let w2 = char_width('W', 10.0, &FontFamily::Courier, false);
assert!((w1 - w2).abs() < f32::EPSILON);
}
#[test]
fn str_width_hello() {
let w = str_width("Hello", 10.0, &FontFamily::Helvetica, false);
assert!((w - 22.78).abs() < 0.01);
}
#[test]
fn non_ascii_uses_default() {
let w = char_width('\u{00E9}', 10.0, &FontFamily::Helvetica, false);
assert!((w - 5.56).abs() < 0.01);
}
#[test]
fn helvetica_uppercase_wider() {
let w_upper = char_width('W', 12.0, &FontFamily::Helvetica, false);
let w_lower = char_width('i', 12.0, &FontFamily::Helvetica, false);
assert!(
w_upper > w_lower,
"W ({w_upper}) should be wider than i ({w_lower})"
);
}
#[test]
fn ascender_ratio_helvetica() {
let r = ascender_ratio(&FontFamily::Helvetica);
assert!((r - 0.718).abs() < f32::EPSILON);
}
#[test]
fn descender_ratio_helvetica() {
let r = descender_ratio(&FontFamily::Helvetica);
assert!((r - 0.207).abs() < f32::EPSILON);
}
#[test]
fn ascender_plus_descender_less_than_one() {
for family in &[
FontFamily::Helvetica,
FontFamily::TimesRoman,
FontFamily::Courier,
] {
let sum = ascender_ratio(family) + descender_ratio(family);
assert!(
sum < 1.0,
"ascender + descender should be < 1.0 em for {family:?}"
);
}
}
#[test]
fn bold_wider_than_regular() {
let regular = char_width('b', 12.0, &FontFamily::Helvetica, false);
let bold = char_width('b', 12.0, &FontFamily::Helvetica, true);
assert!(
bold > regular,
"Bold 'b' ({bold}) should be wider than regular 'b' ({regular})"
);
}
#[test]
fn pdf_font_name_helvetica_variants() {
assert_eq!(
pdf_font_name_for_family(&FontFamily::Helvetica, false, false),
"Helvetica"
);
assert_eq!(
pdf_font_name_for_family(&FontFamily::Helvetica, true, false),
"Helvetica-Bold"
);
assert_eq!(
pdf_font_name_for_family(&FontFamily::Helvetica, false, true),
"Helvetica-Oblique"
);
assert_eq!(
pdf_font_name_for_family(&FontFamily::Helvetica, true, true),
"Helvetica-BoldOblique"
);
}
#[test]
fn pdf_font_name_times_variants() {
assert_eq!(
pdf_font_name_for_family(&FontFamily::TimesRoman, false, false),
"Times-Roman"
);
assert_eq!(
pdf_font_name_for_family(&FontFamily::TimesRoman, true, false),
"Times-Bold"
);
assert_eq!(
pdf_font_name_for_family(&FontFamily::TimesRoman, false, true),
"Times-Italic"
);
assert_eq!(
pdf_font_name_for_family(&FontFamily::TimesRoman, true, true),
"Times-BoldItalic"
);
}
#[test]
fn pdf_font_name_courier_variants() {
assert_eq!(
pdf_font_name_for_family(&FontFamily::Courier, false, false),
"Courier"
);
assert_eq!(
pdf_font_name_for_family(&FontFamily::Courier, true, true),
"Courier-BoldOblique"
);
}
#[test]
fn pdf_font_name_custom_falls_back_to_helvetica() {
assert_eq!(
pdf_font_name_for_family(&FontFamily::Custom("MyFont".into()), false, false),
"Helvetica"
);
assert_eq!(
pdf_font_name_for_family(&FontFamily::Custom("MyFont".into()), true, true),
"Helvetica-BoldOblique"
);
}
#[test]
fn normal_line_height_factor_helvetica_is_1_2() {
let custom_fonts = HashMap::new();
let factor = normal_line_height_factor(&FontFamily::Helvetica, false, false, &custom_fonts);
assert!(
(factor - 1.2).abs() < 0.001,
"Helvetica normal line-height should be 1.2 (Chrome parity), got {factor}"
);
}
#[test]
fn normal_line_height_factor_helvetica_bold_is_1_2() {
let custom_fonts = HashMap::new();
let factor = normal_line_height_factor(&FontFamily::Helvetica, true, false, &custom_fonts);
assert!(
(factor - 1.2).abs() < 0.001,
"Helvetica-Bold normal line-height should be 1.2, got {factor}"
);
}
#[test]
fn normal_line_height_factor_standard_fonts_all_1_2() {
let custom_fonts = HashMap::new();
for family in &[
FontFamily::Helvetica,
FontFamily::TimesRoman,
FontFamily::Courier,
] {
for bold in [false, true] {
let factor = normal_line_height_factor(family, bold, false, &custom_fonts);
assert!(
(factor - 1.2).abs() < 0.001,
"{family:?} bold={bold} normal line-height should be 1.2, got {factor}"
);
}
}
}
}