use super::charset::Charset;
#[cfg(test)]
use super::tables::{ANGLE_SKEW, WEIGHT_POW, WEIGHT_POW_11, WEIGHT_POW_SHIFT_JIS};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SubstFont {
pub(crate) family: String,
pub charset: Charset,
pub(crate) weight: Option<i32>,
pub(crate) weight_cjk: Option<i32>,
pub italic_angle: i32,
pub(crate) subst_cjk: bool,
pub(crate) italic_cjk: bool,
pub is_builtin_generic: bool,
}
impl SubstFont {
#[must_use]
pub fn raw_weight(&self) -> i32 {
self.weight.unwrap_or(0)
}
#[cfg(test)]
#[must_use]
pub(crate) fn effective_weight(&self, is_cid_font: bool) -> i32 {
if self.subst_cjk && is_cid_font {
self.weight_cjk.unwrap_or(0)
} else {
self.raw_weight()
}
}
#[cfg(test)]
#[must_use]
pub(crate) fn skew(&self) -> i32 {
skew_from_angle(self.italic_angle)
}
#[cfg(test)]
#[must_use]
pub(crate) fn skew_cjk(&self) -> i32 {
skew_from_angle(if self.italic_cjk { -15 } else { 0 })
}
#[cfg(test)]
#[must_use]
pub(crate) fn effective_skew(&self, is_cid_font: bool) -> i32 {
if self.subst_cjk && is_cid_font {
self.skew_cjk()
} else {
self.skew()
}
}
#[allow(clippy::similar_names)]
#[must_use]
#[cfg(test)]
pub(crate) fn embolden_level_for_render(
&self,
is_cid_font: bool,
matrix_xx: i32,
matrix_xy: i32,
) -> Option<i32> {
if self.is_builtin_generic {
return Some(0);
}
let w = self.effective_weight(is_cid_font);
if w <= 400 {
return Some(0);
}
let index = usize::try_from((w - 400) / 10).ok()?;
let level = weight_level(index, self.charset == Charset::ShiftJis)?;
let scaled =
i64::from(level) * (i64::from(matrix_xx).abs() + i64::from(matrix_xy).abs()) / 36655;
Some(i32::try_from(scaled).unwrap_or(0))
}
#[cfg(test)]
#[must_use]
pub(crate) fn embolden_level_for_load(&self) -> i32 {
if self.is_builtin_generic {
return 0;
}
let w = self.raw_weight();
if w <= 400 {
return 0;
}
let Ok(index) = usize::try_from((w - 400) / 10) else {
return 0;
};
weight_level_for_load(index.min(99), self.charset == Charset::ShiftJis)
}
#[cfg(test)]
#[must_use]
pub(crate) fn estimated_stem_v(&self) -> i32 {
self.raw_weight() / 5
}
#[must_use]
pub(crate) fn is_actual_font_loaded(&self, base_name: &[u8]) -> bool {
let normalized: String = self
.family
.chars()
.filter(|c| *c != ' ')
.flat_map(char::to_lowercase)
.collect();
if normalized.is_empty() {
return false;
}
base_name.starts_with(normalized.as_bytes())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn configure_external(
&mut self,
face_name: String,
charset: Charset,
weight: i32,
is_italic: bool,
mut italic_angle: i32,
face_is_bold: bool,
face_is_italic: bool,
) {
self.family = face_name;
self.charset = charset;
let face_weight = if face_is_bold { 700 } else { 400 };
if weight != face_weight {
self.weight = Some(weight);
}
if is_italic && !face_is_italic {
if italic_angle == 0 {
italic_angle = -12;
} else if italic_angle.abs() < 5 {
italic_angle = 0;
}
self.italic_angle = italic_angle;
}
}
pub(crate) fn use_chrome_serif(&mut self) {
"Chrome Serif".clone_into(&mut self.family);
if let Some(w) = self.weight {
self.weight = Some(w * 4 / 5);
}
}
}
#[cfg(test)]
#[must_use]
pub fn skew_from_angle(angle: i32) -> i32 {
if angle > 0 || angle == i32::MIN {
return -58;
}
let index = angle.unsigned_abs() as usize;
ANGLE_SKEW.get(index).map_or(-58, |&s| i32::from(s))
}
#[must_use]
#[cfg(test)]
fn weight_level(index: usize, shift_jis: bool) -> Option<i32> {
if index >= 100 {
return None;
}
let table = if shift_jis {
&WEIGHT_POW_SHIFT_JIS
} else {
&WEIGHT_POW_11
};
table.get(index).map(|&v| i32::from(v))
}
#[must_use]
#[cfg(test)]
fn weight_level_for_load(index: usize, shift_jis: bool) -> i32 {
if shift_jis {
WEIGHT_POW_SHIFT_JIS
.get(index)
.map_or(0, |&v| i32::from(v) * 65536 / 36655)
} else {
WEIGHT_POW.get(index).map_or(0, |&v| i32::from(v))
}
}
#[derive(Debug, Clone, Copy)]
pub struct GlyphSpacingGate<'a> {
pub vertical: bool,
pub embedded: bool,
pub declared_widths: bool,
pub base_font_name: &'a [u8],
pub subst: Option<&'a SubstFont>,
}
#[must_use]
pub fn applies_glyph_spacing(gate: &GlyphSpacingGate<'_>) -> bool {
if gate.vertical || gate.embedded || !gate.declared_widths {
return false;
}
let lower = gate.base_font_name.to_ascii_lowercase();
if super::standard_font_index(&lower).is_some() {
return false;
}
let Some(subst) = gate.subst else {
return false;
};
!subst.is_builtin_generic && !subst.is_actual_font_loaded(&lower)
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
fn passing_gate(subst: &SubstFont) -> GlyphSpacingGate<'_> {
GlyphSpacingGate {
vertical: false,
embedded: false,
declared_widths: true,
base_font_name: b"Verdana",
subst: Some(subst),
}
}
fn nimbus() -> SubstFont {
SubstFont {
family: "Nimbus Sans".to_owned(),
..SubstFont::default()
}
}
#[test]
fn a_substitution_onto_a_different_family_takes_the_correction() {
let subst = nimbus();
assert!(applies_glyph_spacing(&passing_gate(&subst)));
}
#[test]
fn vertical_writing_declines_the_correction() {
let subst = nimbus();
let gate = GlyphSpacingGate {
vertical: true,
..passing_gate(&subst)
};
assert!(!applies_glyph_spacing(&gate));
}
#[test]
fn an_embedded_program_declines_the_correction() {
let subst = nimbus();
let gate = GlyphSpacingGate {
embedded: true,
..passing_gate(&subst)
};
assert!(!applies_glyph_spacing(&gate));
}
#[test]
fn a_font_without_declared_widths_declines_the_correction() {
let subst = nimbus();
let gate = GlyphSpacingGate {
declared_widths: false,
..passing_gate(&subst)
};
assert!(!applies_glyph_spacing(&gate));
}
#[test]
fn a_standard_fourteen_base_font_name_declines_the_correction() {
let subst = nimbus();
for name in [&b"Helvetica"[..], b"ArialMT", b"arial,bold", b"CourierNew"] {
let gate = GlyphSpacingGate {
base_font_name: name,
..passing_gate(&subst)
};
assert!(
!applies_glyph_spacing(&gate),
"{}",
String::from_utf8_lossy(name)
);
}
}
#[test]
fn a_built_in_generic_declines_the_correction() {
let subst = SubstFont {
family: "Chrome Sans".to_owned(),
is_builtin_generic: true,
..SubstFont::default()
};
assert!(!applies_glyph_spacing(&passing_gate(&subst)));
}
#[test]
fn loading_the_document_s_own_face_declines_the_correction() {
let subst = SubstFont {
family: "Verdana".to_owned(),
..SubstFont::default()
};
assert!(!applies_glyph_spacing(&passing_gate(&subst)));
let bold = GlyphSpacingGate {
base_font_name: b"Verdana,Bold",
..passing_gate(&subst)
};
assert!(!applies_glyph_spacing(&bold));
}
#[test]
fn a_font_that_was_never_substituted_declines_the_correction() {
let subst = nimbus();
let gate = GlyphSpacingGate {
subst: None,
..passing_gate(&subst)
};
assert!(!applies_glyph_spacing(&gate));
}
#[test]
fn effective_skew_matches_the_oracle() {
let mut s = SubstFont {
italic_angle: -12,
..SubstFont::default()
};
assert_eq!(s.effective_skew(false), -21);
s.subst_cjk = true;
s.italic_cjk = true;
assert_eq!(s.effective_skew(true), -27);
assert_eq!(s.effective_skew(false), -21);
}
#[test]
fn the_skew_table_saturates_outside_its_range() {
assert_eq!(skew_from_angle(0), 0);
assert_eq!(skew_from_angle(-1), -2);
assert_eq!(skew_from_angle(-29), -55);
assert_eq!(skew_from_angle(-30), -58);
assert_eq!(skew_from_angle(-1000), -58);
assert_eq!(skew_from_angle(1), -58);
assert_eq!(skew_from_angle(i32::MAX), -58);
assert_eq!(skew_from_angle(i32::MIN), -58);
}
#[test]
fn the_cjk_skew_is_a_fixed_fifteen_degrees_or_none() {
let mut s = SubstFont::default();
assert_eq!(s.skew_cjk(), 0);
s.italic_cjk = true;
assert_eq!(s.skew_cjk(), -27);
}
#[test]
fn effective_weight_switches_only_for_a_cid_font() {
let s = SubstFont {
weight: Some(700),
weight_cjk: Some(400),
subst_cjk: true,
..SubstFont::default()
};
assert_eq!(s.effective_weight(true), 400);
assert_eq!(s.effective_weight(false), 700);
let s = SubstFont {
weight: Some(700),
weight_cjk: Some(400),
..SubstFont::default()
};
assert_eq!(s.effective_weight(true), 700);
}
#[test]
fn embolden_levels_match_the_oracle() {
let mut s = SubstFont {
weight: Some(700),
..SubstFont::default()
};
assert_eq!(s.embolden_level_for_render(false, 1024, 0), Some(1));
assert_eq!(s.embolden_level_for_load(), 70);
assert_eq!(
s.embolden_level_for_render(false, 30_000_000, 30_000_000),
Some(63838)
);
s.is_builtin_generic = true;
assert_eq!(s.embolden_level_for_render(false, 1024, 0), Some(0));
assert_eq!(s.embolden_level_for_load(), 0);
}
#[test]
fn a_weight_at_or_below_four_hundred_needs_no_emboldening() {
for w in [0, 100, 400] {
let s = SubstFont {
weight: Some(w),
..SubstFont::default()
};
assert_eq!(s.embolden_level_for_render(false, 1024, 0), Some(0));
assert_eq!(s.embolden_level_for_load(), 0);
}
}
#[test]
fn the_render_path_fails_past_the_table_while_the_load_path_clamps() {
let s = SubstFont {
weight: Some(1400),
..SubstFont::default()
};
assert_eq!(
s.embolden_level_for_render(false, 1024, 0),
None,
"the render path abandons the glyph"
);
assert_eq!(
s.embolden_level_for_load(),
i32::from(WEIGHT_POW[99]),
"the load path clamps to the last entry"
);
}
#[test]
fn the_stem_estimate_is_a_fifth_of_the_weight() {
let s = SubstFont {
weight: Some(700),
..SubstFont::default()
};
assert_eq!(s.estimated_stem_v(), 140);
}
#[test]
fn is_actual_font_loaded_is_a_loose_prefix_test() {
let s = SubstFont {
family: "Times New Roman".to_owned(),
..SubstFont::default()
};
assert!(s.is_actual_font_loaded(b"timesnewroman,bold"));
assert!(s.is_actual_font_loaded(b"timesnewromanps-bold"));
assert!(!s.is_actual_font_loaded(b"arial,bold"));
let book = SubstFont {
family: "Book".to_owned(),
..SubstFont::default()
};
assert!(book.is_actual_font_loaded(b"bookman"));
assert!(!SubstFont::default().is_actual_font_loaded(b"anything"));
}
#[test]
fn the_weight_sentinel_survives_a_matching_face() {
let mut s = SubstFont::default();
s.configure_external(
"Arial".to_owned(),
Charset::Ansi,
400,
false,
0,
false,
false,
);
assert_eq!(s.weight, None);
assert_eq!(s.raw_weight(), 0);
assert_eq!(s.embolden_level_for_load(), 0);
let mut s = SubstFont::default();
s.configure_external(
"Arial".to_owned(),
Charset::Ansi,
700,
false,
0,
false,
false,
);
assert_eq!(s.weight, Some(700));
let mut s = SubstFont::default();
s.configure_external(
"Arial".to_owned(),
Charset::Ansi,
700,
false,
0,
true,
false,
);
assert_eq!(s.weight, None);
}
#[test]
fn the_three_italic_angle_cases() {
let mut s = SubstFont::default();
s.configure_external("F".to_owned(), Charset::Ansi, 400, true, 0, false, false);
assert_eq!(s.italic_angle, -12);
for angle in [-4, 4, 1] {
let mut s = SubstFont::default();
s.configure_external(
"F".to_owned(),
Charset::Ansi,
400,
true,
angle,
false,
false,
);
assert_eq!(s.italic_angle, 0, "angle {angle}");
}
let mut s = SubstFont::default();
s.configure_external("F".to_owned(), Charset::Ansi, 400, true, -20, false, false);
assert_eq!(s.italic_angle, -20);
let mut s = SubstFont::default();
s.configure_external("F".to_owned(), Charset::Ansi, 400, true, 0, false, true);
assert_eq!(s.italic_angle, 0);
}
#[test]
fn chrome_serif_scales_the_weight_by_four_fifths() {
let mut s = SubstFont {
weight: Some(500),
..SubstFont::default()
};
s.use_chrome_serif();
assert_eq!(s.family, "Chrome Serif");
assert_eq!(s.weight, Some(400));
let mut s = SubstFont::default();
s.use_chrome_serif();
assert_eq!(s.weight, None);
}
#[test]
fn the_weight_pow_11_table_has_three_non_monotonic_entries() {
assert_eq!(WEIGHT_POW_11[52], 43, "dips below the 47 before it");
assert_eq!(WEIGHT_POW_11[51], 46);
assert_eq!(WEIGHT_POW_11[59], 45, "dips below the 48 before it");
assert_eq!(WEIGHT_POW_11[58], 48);
assert_eq!(WEIGHT_POW_11[63], 46, "dips below the 50 before it");
assert_eq!(WEIGHT_POW_11[62], 50);
let descents = WEIGHT_POW_11.windows(2).filter(|w| w[1] < w[0]).count();
assert_eq!(descents, 3);
}
#[test]
fn the_other_two_weight_tables_are_monotonic() {
for (name, table) in [
("kWeightPow", &WEIGHT_POW),
("kWeightPowShiftJis", &WEIGHT_POW_SHIFT_JIS),
] {
assert!(
table.windows(2).all(|w| w[1] >= w[0]),
"{name} should be non-decreasing"
);
}
}
#[test]
fn the_shift_jis_arm_reads_a_different_table() {
let s = SubstFont {
weight: Some(700),
charset: Charset::ShiftJis,
..SubstFont::default()
};
assert_eq!(s.embolden_level_for_render(false, 36655, 0), Some(96));
assert_eq!(s.embolden_level_for_load(), 96 * 65536 / 36655);
}
}