mod charset;
mod db;
#[cfg(all(feature = "system-fonts", not(target_arch = "wasm32")))]
mod probe;
mod standard;
mod style;
mod substfont;
mod tables;
pub use charset::{Charset, charset_from_unicode};
pub(crate) use charset::{CodePage, PitchFamily};
#[cfg(test)]
pub(crate) use db::FaceInfo;
pub(crate) use db::{CroscoreDb, FaceHandle, FontDb, SystemFontDb, TestFontDb};
#[cfg(test)]
pub(crate) use standard::ALL_STANDARD_FONTS;
pub use standard::StandardFont;
pub use standard::canonical_font_name;
pub(crate) use standard::{standard_font_data, standard_font_index};
pub(crate) use style::{
NARROW_FAMILY, font_family, is_narrow_font_name, parse_styles, strip_subset_prefix, style_bits,
style_type, subst_name, tt_normalize,
};
pub use substfont::SubstFont;
pub(crate) use substfont::{GlyphSpacingGate, applies_glyph_spacing};
use crate::FontFlags;
use crate::glyphs::{Face, GlyphSource};
use pdfrum_common::{DiagKind, Diagnostics, Severity};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, OnceLock};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FontRequest {
pub name: Vec<u8>,
pub is_truetype: bool,
pub flags: FontFlags,
pub weight: i32,
pub italic_angle: i32,
pub code_page: CodePage,
pub vertical: bool,
}
impl Default for FontRequest {
fn default() -> Self {
Self {
name: Vec::new(),
is_truetype: false,
flags: FontFlags::DEFAULT,
weight: 400,
italic_angle: 0,
code_page: CodePage::DefAnsi,
vertical: false,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SubstitutionOptions {
pub skip_font_enumeration: bool,
pub font_dirs: Vec<PathBuf>,
pub croscore_font_names: bool,
pub system_fonts: bool,
}
#[must_use]
pub fn croscore_name(face: &str) -> String {
let has = |needle: &str| face.contains(needle);
let base = if has("Arial") || has("Calibri") || has("Helvetica") {
"Arimo"
} else if face.is_empty() || has("Times") {
"Tinos"
} else if has("Courier") {
"Cousine"
} else {
return face.to_owned();
};
let mut out = base.to_owned();
if has("Bold") {
out.push_str(" Bold");
}
if has("Italic") || has("Oblique") {
out.push_str(" Italic");
}
out
}
pub struct Substitution {
pub glyphs: GlyphSource,
pub subst: SubstFont,
#[cfg(test)]
pub standard: Option<StandardFont>,
}
#[must_use]
pub fn resolve(
req: &FontRequest,
db: &impl FontDb,
opts: &SubstitutionOptions,
diags: &mut Diagnostics,
) -> Substitution {
if opts.croscore_font_names {
return resolve_inner(req, &CroscoreDb::new(db), opts, diags, false);
}
resolve_inner(req, db, opts, diags, false)
}
#[must_use]
pub fn resolve_with_options(
req: &FontRequest,
opts: &SubstitutionOptions,
diags: &mut Diagnostics,
) -> Substitution {
if opts.font_dirs.is_empty() && !opts.system_fonts {
return resolve(req, &TestFontDb::new(), opts, diags);
}
let db = scanned(ScanKey::of(&opts.font_dirs));
resolve(req, db.as_ref(), opts, diags)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum ScanKey {
System,
Dirs(Vec<PathBuf>),
}
impl ScanKey {
fn of(dirs: &[PathBuf]) -> Self {
if dirs.is_empty() {
Self::System
} else {
Self::Dirs(dirs.to_vec())
}
}
fn dirs(&self) -> &[PathBuf] {
match self {
Self::System => &[],
Self::Dirs(dirs) => dirs,
}
}
}
fn scanned(key: ScanKey) -> Arc<SystemFontDb> {
static SCANS: OnceLock<Mutex<HashMap<ScanKey, Arc<SystemFontDb>>>> = OnceLock::new();
let scans = SCANS.get_or_init(|| Mutex::new(HashMap::new()));
if let Ok(cache) = scans.lock()
&& let Some(db) = cache.get(&key)
{
return Arc::clone(db);
}
let db = Arc::new(SystemFontDb::scan(key.dirs()));
match scans.lock() {
Ok(mut cache) => Arc::clone(cache.entry(key).or_insert(db)),
Err(_) => db,
}
}
#[allow(clippy::too_many_lines)]
fn resolve_inner(
req: &FontRequest,
db: &impl FontDb,
opts: &SubstitutionOptions,
diags: &mut Diagnostics,
retried: bool,
) -> Substitution {
let mut subst = SubstFont::default();
let mut weight = if req.weight == 0 { 400 } else { req.weight };
let mut italic_angle = req.italic_angle;
if !req.flags.uses_extern_attr() {
weight = 400;
italic_angle = 0;
}
let name = subst_name(&req.name, req.is_truetype);
if name == b"Symbol" && !req.is_truetype {
"Chrome Symbol".clone_into(&mut subst.family);
subst.charset = Charset::Symbol;
return terminal(
Some(StandardFont::Symbol),
weight,
italic_angle,
PitchFamily::default(),
subst,
diags,
);
}
if name == b"ZapfDingbats" {
"Chrome Dingbats".clone_into(&mut subst.family);
subst.charset = Charset::Symbol;
return terminal(
Some(StandardFont::Dingbats),
weight,
italic_angle,
PitchFamily::default(),
subst,
diags,
);
}
let (mut family, style_str, has_comma) = style::split_style(&name);
let std_font = if has_comma {
standard_font_index(&family)
} else {
standard_font_index(&name)
};
let mut has_hyphen = false;
let (mut n_style, pitch_family, mut base_font) =
if let Some(sf) = std_font.filter(|f| f.index() < 12) {
(
style_from_standard_font(sf),
PitchFamily::from_standard_font(sf),
Some(sf),
)
} else {
let mut style_out = style_bits::NORMAL;
let mut style_str = style_str.clone();
if !has_comma {
if let Some(p) = family.iter().rposition(|c| *c == b'-') {
style_str = family.get(p + 1..).unwrap_or_default().to_vec();
family.truncate(p);
has_hyphen = true;
}
}
if !has_hyphen
&& let Some(sr) = std::str::from_utf8(&family)
.ok()
.and_then(|f| style_type(f, true))
{
family.truncate(family.len().saturating_sub(sr.name.len()));
style_out |= sr.style;
}
let _ = style_str;
(style_out, PitchFamily::from_flags(req.flags), None)
};
let old_weight = weight;
if n_style & style_bits::FORCE_BOLD != 0 {
weight = 700;
}
let style_source = if has_comma {
style_str
} else {
suffix_after_hyphen(&name, has_hyphen)
};
let parsed = parse_styles(&style_source, weight, n_style);
let mut is_style_available = parsed.is_style_available;
if parsed.abort {
family.clone_from(&name);
base_font = None;
} else {
weight = parsed.weight;
n_style = parsed.style;
}
if db.faces().is_empty() {
return terminal(
base_font,
old_weight,
italic_angle,
pitch_family,
subst,
diags,
);
}
let charset = request_charset(req.code_page, base_font, req.flags);
let is_cjk = charset.is_cjk();
let mut is_italic = n_style & style_bits::ITALIC != 0;
let family_str = String::from_utf8_lossy(&family).into_owned();
let mut family_str = match font_family(n_style, &family_str) {
Some(f) => f.to_owned(),
None => family_str,
};
let name_str = String::from_utf8_lossy(&name).into_owned();
let mut matched = db.match_installed(&tt_normalize(&family_str));
if matched.is_none()
&& family_str != name_str
&& !has_comma
&& (!has_hyphen || !is_style_available)
{
matched = db.match_installed(&tt_normalize(&name_str));
}
let mut pitch_family = pitch_family;
if matched.is_none() && base_font.is_none() {
if is_cjk {
subst.subst_cjk = true;
if n_style != 0 {
subst.weight_cjk = Some(weight);
}
if n_style & style_bits::ITALIC != 0 {
subst.italic_cjk = true;
}
} else {
if style::is_third_party_font(&family_str) {
pitch_family = PitchFamily(pitch_family.0 & !PitchFamily::ROMAN);
} else {
is_italic = italic_angle != 0;
if !opts.skip_font_enumeration {
weight = old_weight;
}
}
if is_narrow_font_name(&name_str) {
NARROW_FAMILY.clone_into(&mut family_str);
}
}
if req.flags.is_italic() {
is_italic = true;
}
} else {
italic_angle = 0;
if n_style & style_bits::FORCE_BOLD == 0 {
weight = 400;
}
if let Some(m) = &matched {
family_str.clone_from(m);
}
if let Some(bf) = base_font {
let adjusted = adjust_base_font_for_style(bf, n_style);
base_font = Some(adjusted);
canonical_font_name(adjusted).clone_into(&mut family_str);
}
}
let _ = &mut is_style_available;
if let Some(h) = db.find_font(weight, is_italic, charset, pitch_family, &family_str, true)
&& let Some(s) = external(db, h, weight, is_italic, italic_angle, charset, &mut subst)
{
return Substitution {
glyphs: s,
subst,
#[cfg(test)]
standard: base_font,
};
}
if is_cjk {
is_italic = italic_angle != 0;
weight = old_weight;
}
if let Some(m) = &matched {
return match db.font_by_name(m) {
None => terminal(
base_font,
old_weight,
italic_angle,
pitch_family,
subst,
diags,
),
Some(h) => {
match external(db, h, weight, is_italic, italic_angle, charset, &mut subst) {
Some(s) => Substitution {
glyphs: s,
subst,
#[cfg(test)]
standard: base_font,
},
None => terminal(
base_font,
old_weight,
italic_angle,
pitch_family,
subst,
diags,
),
}
}
};
}
if charset == Charset::Symbol {
if name == b"Symbol" {
"Chrome Symbol".clone_into(&mut subst.family);
subst.charset = Charset::Symbol;
return terminal(
Some(StandardFont::Symbol),
old_weight,
italic_angle,
pitch_family,
subst,
diags,
);
}
if !retried {
let retry = FontRequest {
name: family.clone(),
flags: req.flags.without(FontFlags::SYMBOLIC),
weight,
italic_angle,
code_page: CodePage::DefAnsi,
..req.clone()
};
return resolve_inner(&retry, db, opts, diags, true);
}
}
if charset == Charset::Ansi {
return terminal(
base_font,
old_weight,
italic_angle,
pitch_family,
subst,
diags,
);
}
let by_charset = db
.faces()
.iter()
.position(|f| f.charsets.contains(&charset))
.map(FaceHandle::from_index);
match by_charset {
None => terminal(
base_font,
old_weight,
italic_angle,
pitch_family,
subst,
diags,
),
Some(h) => {
if let Some(s) = external(db, h, weight, is_italic, italic_angle, charset, &mut subst) {
Substitution {
glyphs: s,
subst,
#[cfg(test)]
standard: base_font,
}
} else {
diags.record(Severity::Suspicious, DiagKind::FontSubstitutionFailed, None);
Substitution {
glyphs: GlyphSource::None,
subst,
#[cfg(test)]
standard: base_font,
}
}
}
}
}
#[must_use]
pub fn style_from_standard_font(f: StandardFont) -> u32 {
let pos = f.index() % 4;
let mut style = style_bits::NORMAL;
if pos == 1 || pos == 2 {
style |= style_bits::FORCE_BOLD;
}
if pos / 2 != 0 {
style |= style_bits::ITALIC;
}
style
}
#[must_use]
pub fn adjust_base_font_for_style(base: StandardFont, style: u32) -> StandardFont {
if style == style_bits::NORMAL || !base.is_stylable() {
return base;
}
let bold = style & style_bits::FORCE_BOLD != 0;
let italic = style & style_bits::ITALIC != 0;
let offset = match (bold, italic) {
(true, true) => 2,
(true, false) => 1,
(false, true) => 3,
(false, false) => 0,
};
StandardFont::from_index(base.index() + offset).unwrap_or(base)
}
#[must_use]
fn request_charset(cp: CodePage, base: Option<StandardFont>, flags: FontFlags) -> Charset {
if cp != CodePage::DefAnsi {
return Charset::from_code_page(cp);
}
if flags.is_symbolic() && base.is_none() {
return Charset::Symbol;
}
Charset::Ansi
}
fn suffix_after_hyphen(name: &[u8], has_hyphen: bool) -> Vec<u8> {
if !has_hyphen {
return Vec::new();
}
name.iter()
.rposition(|c| *c == b'-')
.and_then(|p| name.get(p + 1..))
.unwrap_or_default()
.to_vec()
}
fn external(
db: &impl FontDb,
h: FaceHandle,
weight: i32,
is_italic: bool,
italic_angle: i32,
charset: Charset,
subst: &mut SubstFont,
) -> Option<GlyphSource> {
let (bytes, index) = db.face_bytes(h)?;
let face = Face::new(bytes, index)?;
let info = db.faces().get(h.index())?;
let name = if info.name.is_empty() {
face.display_name().unwrap_or_default()
} else {
info.name.clone()
};
subst.configure_external(
name,
charset,
weight,
is_italic,
italic_angle,
info.styles & style_bits::FORCE_BOLD != 0,
info.styles & style_bits::ITALIC != 0,
);
Some(GlyphSource::Fontations(face))
}
fn terminal(
base_font: Option<StandardFont>,
weight: i32,
italic_angle: i32,
pitch_family: PitchFamily,
mut subst: SubstFont,
diags: &mut Diagnostics,
) -> Substitution {
if let Some(f) = base_font {
let glyphs = builtin_standard(f);
if !glyphs.is_some() {
diags.record(Severity::Suspicious, DiagKind::FontSubstitutionFailed, None);
}
return Substitution {
glyphs,
subst,
#[cfg(test)]
standard: Some(f),
};
}
subst.is_builtin_generic = true;
subst.italic_angle = italic_angle;
if weight != 0 {
subst.weight = Some(weight);
}
let serif = pitch_family.has(PitchFamily::ROMAN);
let (glyphs, family) = builtin_generic(serif);
if serif {
subst.use_chrome_serif();
} else {
family.clone_into(&mut subst.family);
}
if !glyphs.is_some() {
diags.record(Severity::Suspicious, DiagKind::FontSubstitutionFailed, None);
}
Substitution {
glyphs,
subst,
#[cfg(test)]
standard: None,
}
}
fn builtin_standard(f: StandardFont) -> GlyphSource {
static FACES: OnceLock<[GlyphSource; 14]> = OnceLock::new();
let faces = FACES.get_or_init(|| {
std::array::from_fn(|i| {
let Some(f) = StandardFont::from_index(i) else {
return GlyphSource::None;
};
let bytes: Arc<[u8]> = Arc::from(standard_font_data(f));
Face::new(bytes, 0).map_or(GlyphSource::None, GlyphSource::Fontations)
})
});
faces.get(f.index()).cloned().unwrap_or_default()
}
#[must_use]
pub fn builtin_generic(serif: bool) -> (GlyphSource, &'static str) {
static SANS: OnceLock<GlyphSource> = OnceLock::new();
static SERIF: OnceLock<GlyphSource> = OnceLock::new();
let (cell, bytes, family) = if serif {
(
&SERIF,
&include_bytes!("../../fontdata/FoxitSerifMM.pfb")[..],
"Chrome Serif",
)
} else {
(
&SANS,
&include_bytes!("../../fontdata/FoxitSansMM.pfb")[..],
"Chrome Sans",
)
};
let source = cell.get_or_init(|| {
let font = pdfrum_type1::Type1Font::parse(
bytes,
&pdfrum_common::Limits::default(),
&mut Diagnostics::with_limit(0),
);
match font {
Ok(f) => GlyphSource::Type1(Arc::new(f)),
Err(_) => GlyphSource::None,
}
});
(source.clone(), family)
}
#[cfg(test)]
#[path = "subst_tests.rs"]
mod tests;