use crate::FontResolverFn;
use crate::font::blob::{CffFontBlob, OpenTypeFontBlob};
use crate::font::generated::{glyph_names, metrics, standard, symbol, zapf_dings};
use crate::font::true_type::{Width, read_encoding, read_widths};
use crate::font::{
Encoding, FontData, FontQuery, glyph_name_to_unicode, normalized_glyph_name, stretch_glyph,
strip_subset_prefix,
};
use kurbo::BezPath;
use pdf_syntax::object::Dict;
use pdf_syntax::object::Name;
use pdf_syntax::object::dict::keys::{
BASE_FONT, FONT_DESC, FONT_FAMILY, FONT_WEIGHT, ITALIC_ANGLE, MISSING_WIDTH,
};
use skrifa::raw::TableProvider;
use skrifa::{GlyphId, GlyphId16};
use std::cell::RefCell;
use std::collections::HashMap;
#[derive(Copy, Clone, Debug)]
pub enum StandardFont {
Helvetica,
HelveticaBold,
HelveticaOblique,
HelveticaBoldOblique,
Courier,
CourierBold,
CourierOblique,
CourierBoldOblique,
TimesRoman,
TimesBold,
TimesItalic,
TimesBoldItalic,
ZapfDingBats,
Symbol,
}
impl StandardFont {
pub(crate) fn code_to_name(&self, code: u8) -> Option<&'static str> {
match self {
Self::Symbol => symbol::get(code),
Self::ZapfDingBats => zapf_dings::get(code),
_ => standard::get(code),
}
}
pub(crate) fn get_width(&self, mut name: &str) -> Option<f32> {
if name == ".notdef" {
return Some(250.0);
}
name = normalized_glyph_name(name);
match self {
Self::Helvetica => metrics::HELVETICA.get(name).copied(),
Self::HelveticaBold => metrics::HELVETICA_BOLD.get(name).copied(),
Self::HelveticaOblique => metrics::HELVETICA_OBLIQUE.get(name).copied(),
Self::HelveticaBoldOblique => metrics::HELVETICA_BOLD_OBLIQUE.get(name).copied(),
Self::Courier => metrics::COURIER.get(name).copied(),
Self::CourierBold => metrics::COURIER_BOLD.get(name).copied(),
Self::CourierOblique => metrics::COURIER_OBLIQUE.get(name).copied(),
Self::CourierBoldOblique => metrics::COURIER_BOLD_OBLIQUE.get(name).copied(),
Self::TimesRoman => metrics::TIMES_ROMAN.get(name).copied(),
Self::TimesBold => metrics::TIMES_BOLD.get(name).copied(),
Self::TimesItalic => metrics::TIMES_ITALIC.get(name).copied(),
Self::TimesBoldItalic => metrics::TIMES_BOLD_ITALIC.get(name).copied(),
Self::ZapfDingBats => metrics::ZAPF_DING_BATS.get(name).copied(),
Self::Symbol => metrics::SYMBOL.get(name).copied(),
}
}
pub(crate) fn as_str(&self) -> &'static str {
match self {
Self::Helvetica => "Helvetica",
Self::HelveticaBold => "Helvetica Bold",
Self::HelveticaOblique => "Helvetica Oblique",
Self::HelveticaBoldOblique => "Helvetica Bold Oblique",
Self::Courier => "Courier",
Self::CourierBold => "Courier Bold",
Self::CourierOblique => "Courier Oblique",
Self::CourierBoldOblique => "Courier Bold Oblique",
Self::TimesRoman => "Times Roman",
Self::TimesBold => "Times Bold",
Self::TimesItalic => "Times Italic",
Self::TimesBoldItalic => "Times Bold Italic",
Self::ZapfDingBats => "Zapf Dingbats",
Self::Symbol => "Symbol",
}
}
pub fn postscript_name(&self) -> &'static str {
match self {
Self::Helvetica => "Helvetica",
Self::HelveticaBold => "Helvetica-Bold",
Self::HelveticaOblique => "Helvetica-Oblique",
Self::HelveticaBoldOblique => "Helvetica-BoldOblique",
Self::Courier => "Courier",
Self::CourierBold => "Courier-Bold",
Self::CourierOblique => "Courier-Oblique",
Self::CourierBoldOblique => "Courier-BoldOblique",
Self::TimesRoman => "Times-Roman",
Self::TimesBold => "Times-Bold",
Self::TimesItalic => "Times-Italic",
Self::TimesBoldItalic => "Times-BoldItalic",
Self::ZapfDingBats => "ZapfDingbats",
Self::Symbol => "Symbol",
}
}
pub(crate) fn is_bold(&self) -> bool {
matches!(
self,
Self::HelveticaBold
| Self::HelveticaBoldOblique
| Self::CourierBold
| Self::CourierBoldOblique
| Self::TimesBold
| Self::TimesBoldItalic
)
}
pub(crate) fn is_italic(&self) -> bool {
matches!(
self,
Self::HelveticaOblique
| Self::HelveticaBoldOblique
| Self::CourierOblique
| Self::CourierBoldOblique
| Self::TimesItalic
| Self::TimesBoldItalic
)
}
pub(crate) fn is_serif(&self) -> bool {
matches!(
self,
Self::TimesRoman | Self::TimesBold | Self::TimesItalic | Self::TimesBoldItalic
)
}
pub(crate) fn is_monospace(&self) -> bool {
matches!(
self,
Self::Courier | Self::CourierBold | Self::CourierOblique | Self::CourierBoldOblique
)
}
#[cfg(feature = "embed-fonts")]
pub fn get_font_data(&self) -> (FontData, u32) {
use std::sync::Arc;
let data = match self {
Self::Helvetica => &include_bytes!("../../assets/FoxitSans.pfb")[..],
Self::HelveticaBold => &include_bytes!("../../assets/FoxitSansBold.pfb")[..],
Self::HelveticaOblique => &include_bytes!("../../assets/FoxitSansItalic.pfb")[..],
Self::HelveticaBoldOblique => {
&include_bytes!("../../assets/FoxitSansBoldItalic.pfb")[..]
}
Self::Courier => &include_bytes!("../../assets/FoxitFixed.pfb")[..],
Self::CourierBold => &include_bytes!("../../assets/FoxitFixedBold.pfb")[..],
Self::CourierOblique => &include_bytes!("../../assets/FoxitFixedItalic.pfb")[..],
Self::CourierBoldOblique => {
&include_bytes!("../../assets/FoxitFixedBoldItalic.pfb")[..]
}
Self::TimesRoman => &include_bytes!("../../assets/FoxitSerif.pfb")[..],
Self::TimesBold => &include_bytes!("../../assets/FoxitSerifBold.pfb")[..],
Self::TimesItalic => &include_bytes!("../../assets/FoxitSerifItalic.pfb")[..],
Self::TimesBoldItalic => &include_bytes!("../../assets/FoxitSerifBoldItalic.pfb")[..],
Self::ZapfDingBats => &include_bytes!("../../assets/FoxitDingbats.pfb")[..],
Self::Symbol => {
include_bytes!("../../assets/FoxitSymbol.pfb")
}
};
(Arc::new(data), 0)
}
}
enum StandardFontFamily {
Helvetica,
Courier,
Times,
}
fn standard_font_alias(name: &str) -> Option<StandardFont> {
match name {
"ArialMT" | "Arial" => Some(StandardFont::Helvetica),
"Arial-BoldMT" | "Arial,Bold" | "Arial-Bold" => Some(StandardFont::HelveticaBold),
"Arial-ItalicMT" | "Arial,Italic" | "Arial-Italic" => Some(StandardFont::HelveticaOblique),
"Arial-BoldItalicMT" | "Arial,BoldItalic" | "Arial-BoldItalic" => {
Some(StandardFont::HelveticaBoldOblique)
}
"TimesNewRomanPSMT" | "TimesNewRoman" | "TimesNewRomanPS" => Some(StandardFont::TimesRoman),
"TimesNewRomanPS-BoldMT"
| "TimesNewRoman-Bold"
| "TimesNewRomanPS-Bold"
| "TimesNewRoman,Bold" => Some(StandardFont::TimesBold),
"TimesNewRomanPS-ItalicMT"
| "TimesNewRoman-Italic"
| "TimesNewRomanPS-Italic"
| "TimesNewRoman,Italic" => Some(StandardFont::TimesItalic),
"TimesNewRomanPS-BoldItalicMT"
| "TimesNewRoman-BoldItalic"
| "TimesNewRomanPS-BoldItalic"
| "TimesNewRoman,BoldItalic" => Some(StandardFont::TimesBoldItalic),
"CourierNewPSMT" | "CourierNew" => Some(StandardFont::Courier),
"CourierNewPS-BoldMT" | "CourierNew-Bold" | "CourierNewPS-Bold" => {
Some(StandardFont::CourierBold)
}
"CourierNewPS-ItalicMT" | "CourierNew-Italic" | "CourierNewPS-Italic" => {
Some(StandardFont::CourierOblique)
}
"CourierNewPS-BoldItalicMT" | "CourierNew-BoldItalic" | "CourierNewPS-BoldItalic" => {
Some(StandardFont::CourierBoldOblique)
}
_ => None,
}
}
pub(crate) fn select_standard_font(
dict: &Dict<'_>,
descriptor: &Dict<'_>,
) -> Option<(StandardFont, bool)> {
let base_font = dict.get::<Name>(BASE_FONT)?;
let name = strip_subset_prefix(base_font.as_str());
match name {
"Helvetica" => return Some((StandardFont::Helvetica, true)),
"Helvetica-Bold" => return Some((StandardFont::HelveticaBold, true)),
"Helvetica-Oblique" => return Some((StandardFont::HelveticaOblique, true)),
"Helvetica-BoldOblique" => return Some((StandardFont::HelveticaBoldOblique, true)),
"Courier" => return Some((StandardFont::Courier, true)),
"Courier-Bold" => return Some((StandardFont::CourierBold, true)),
"Courier-Oblique" => return Some((StandardFont::CourierOblique, true)),
"Courier-BoldOblique" => return Some((StandardFont::CourierBoldOblique, true)),
"Times-Roman" => return Some((StandardFont::TimesRoman, true)),
"Times-Bold" => return Some((StandardFont::TimesBold, true)),
"Times-Italic" => return Some((StandardFont::TimesItalic, true)),
"Times-BoldItalic" => return Some((StandardFont::TimesBoldItalic, true)),
"Symbol" => return Some((StandardFont::Symbol, true)),
"ZapfDingbats" => return Some((StandardFont::ZapfDingBats, true)),
_ => {}
}
if let Some(alias) = standard_font_alias(name) {
return Some((alias, false));
}
let lower = name.to_ascii_lowercase();
let family_field = descriptor
.get::<Name>(FONT_FAMILY)
.map(|n| n.as_str().to_ascii_lowercase())
.unwrap_or_default();
let is_bold = descriptor.get::<u32>(FONT_WEIGHT).is_some_and(|w| w >= 600)
|| lower.contains("bold")
|| lower.contains("demi")
|| family_field.contains("bold")
|| family_field.contains("demi");
let is_italic = descriptor
.get::<f32>(ITALIC_ANGLE)
.is_some_and(|a| a < -5.0 || a > 5.0)
|| lower.contains("italic")
|| lower.contains("oblique")
|| family_field.contains("italic")
|| family_field.contains("oblique");
let haystack = if family_field.is_empty() {
lower.clone()
} else {
format!("{lower} {family_field}")
};
let (family, exact) = if haystack.contains("helvetica") {
(Some(StandardFontFamily::Helvetica), true) } else if haystack.contains("arial") || haystack.contains("sans") {
(Some(StandardFontFamily::Helvetica), false) } else if haystack.contains("courier") {
(Some(StandardFontFamily::Courier), true) } else if haystack.contains("mono") {
(Some(StandardFontFamily::Courier), false) } else if haystack.contains("times") {
(Some(StandardFontFamily::Times), true) } else if haystack.contains("serif") {
(Some(StandardFontFamily::Times), false) } else if haystack.contains("zapfdingbats") || haystack.contains("dingbats") {
return Some((StandardFont::ZapfDingBats, false));
} else {
(None, false)
};
let font = match (family?, is_bold, is_italic) {
(StandardFontFamily::Helvetica, false, false) => StandardFont::Helvetica,
(StandardFontFamily::Helvetica, true, false) => StandardFont::HelveticaBold,
(StandardFontFamily::Helvetica, false, true) => StandardFont::HelveticaOblique,
(StandardFontFamily::Helvetica, true, true) => StandardFont::HelveticaBoldOblique,
(StandardFontFamily::Courier, false, false) => StandardFont::Courier,
(StandardFontFamily::Courier, true, false) => StandardFont::CourierBold,
(StandardFontFamily::Courier, false, true) => StandardFont::CourierOblique,
(StandardFontFamily::Courier, true, true) => StandardFont::CourierBoldOblique,
(StandardFontFamily::Times, false, false) => StandardFont::TimesRoman,
(StandardFontFamily::Times, true, false) => StandardFont::TimesBold,
(StandardFontFamily::Times, false, true) => StandardFont::TimesItalic,
(StandardFontFamily::Times, true, true) => StandardFont::TimesBoldItalic,
};
Some((font, exact))
}
#[derive(Debug)]
pub(crate) enum StandardFontBlob {
Cff(CffFontBlob),
Otf(OpenTypeFontBlob, HashMap<String, GlyphId>),
}
impl StandardFontBlob {
pub(crate) fn from_data(data: FontData, index: u32) -> Option<Self> {
if let Some(blob) = CffFontBlob::new(data.clone()) {
Some(Self::new_cff(blob))
} else {
OpenTypeFontBlob::new(data, index).map(Self::new_otf)
}
}
pub(crate) fn new_cff(blob: CffFontBlob) -> Self {
Self::Cff(blob)
}
pub(crate) fn new_otf(blob: OpenTypeFontBlob) -> Self {
let mut glyph_names = HashMap::new();
if let Ok(post) = blob.font_ref().post() {
for i in 0..blob.num_glyphs() {
if let Some(str) = post.glyph_name(GlyphId16::new(i)) {
glyph_names.insert(str.to_string(), GlyphId::new(i as u32));
}
}
}
Self::Otf(blob, glyph_names)
}
}
impl StandardFontBlob {
pub(crate) fn name_to_glyph(&self, name: &str) -> Option<GlyphId> {
match self {
Self::Cff(blob) => blob
.table()
.glyph_index_by_name(name)
.map(|g| GlyphId::new(g.0 as u32)),
Self::Otf(_, glyph_names) => glyph_names.get(name).copied(),
}
}
pub(crate) fn unicode_to_glyph(&self, code: u32) -> Option<GlyphId> {
match self {
Self::Cff(_) => None,
Self::Otf(blob, _) => blob
.font_ref()
.cmap()
.ok()
.and_then(|c| c.map_codepoint(code)),
}
}
pub(crate) fn advance_width(&self, glyph: GlyphId) -> Option<f32> {
match self {
Self::Cff(_) => None,
Self::Otf(blob, _) => blob.glyph_metrics().advance_width(glyph),
}
}
pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath {
if glyph == GlyphId::NOTDEF {
return BezPath::new();
}
match self {
Self::Cff(blob) => blob.outline_glyph(glyph),
Self::Otf(blob, _) => blob.outline_glyph(glyph),
}
}
}
#[derive(Debug)]
pub(crate) struct StandardKind {
base_font: StandardFont,
base_font_blob: StandardFontBlob,
encoding: Encoding,
widths: Vec<Width>,
missing_width: Option<f32>,
fallback: bool,
glyph_to_code: RefCell<HashMap<GlyphId, u8>>,
encodings: HashMap<u8, String>,
}
impl StandardKind {
pub(crate) fn new(dict: &Dict<'_>, resolver: &FontResolverFn) -> Option<Self> {
let descriptor = dict.get::<Dict<'_>>(FONT_DESC).unwrap_or_default();
let (font, exact) = select_standard_font(dict, &descriptor)?;
Self::new_with_standard(dict, font, !exact, resolver)
}
pub(crate) fn new_with_standard(
dict: &Dict<'_>,
base_font: StandardFont,
fallback: bool,
resolver: &FontResolverFn,
) -> Option<Self> {
let descriptor = dict.get::<Dict<'_>>(FONT_DESC).unwrap_or_default();
let (widths, missing_width) = read_widths(dict, &descriptor)?;
let missing_width = descriptor
.contains_key(MISSING_WIDTH)
.then_some(missing_width);
let (mut encoding, encoding_map) = read_encoding(dict);
if matches!(base_font, StandardFont::Symbol | StandardFont::ZapfDingBats) {
encoding = Encoding::BuiltIn;
}
let (blob, index) = resolver(&FontQuery::Standard(base_font))?;
let base_font_blob = StandardFontBlob::from_data(blob, index)?;
Some(Self {
base_font,
base_font_blob,
widths,
missing_width,
encodings: encoding_map,
glyph_to_code: RefCell::new(HashMap::new()),
fallback,
encoding,
})
}
fn code_to_ps_name(&self, code: u8) -> Option<&str> {
let bf = self.base_font;
self.encodings
.get(&code)
.map(String::as_str)
.or_else(|| match self.encoding {
Encoding::BuiltIn => bf.code_to_name(code),
_ => self.encoding.map_code(code),
})
}
pub(crate) fn map_code(&self, code: u8) -> GlyphId {
let result = self
.code_to_ps_name(code)
.and_then(|c| {
self.base_font_blob.name_to_glyph(c).or_else(|| {
glyph_names::get(c).and_then(|c| {
self.base_font_blob
.unicode_to_glyph(c.chars().nth(0).unwrap() as u32)
})
})
})
.unwrap_or(GlyphId::NOTDEF);
self.glyph_to_code.borrow_mut().insert(result, code);
result
}
pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath {
let path = self.base_font_blob.outline_glyph(glyph);
if let Some(code) = self.glyph_to_code.borrow().get(&glyph).copied()
&& let Some(actual_width) = self.base_font_blob.advance_width(glyph).or_else(|| {
self.code_to_ps_name(code)
.and_then(|name| self.base_font.get_width(name))
})
{
let should_width = if self.fallback {
if let Some(Width::Value(w)) = self.widths.get(code as usize).copied() {
w
} else {
return path;
}
} else if let Some(w) = self
.code_to_ps_name(code)
.and_then(|name| self.base_font.get_width(name))
{
w
} else {
return path;
};
return stretch_glyph(path, should_width, actual_width);
}
path
}
pub(crate) fn glyph_width(&self, code: u8) -> Option<f32> {
match self.widths.get(code as usize).copied() {
Some(Width::Value(w)) => Some(w),
Some(Width::Missing) => self.missing_width.or_else(|| {
self.code_to_ps_name(code)
.and_then(|c| self.base_font.get_width(c))
}),
_ => self
.code_to_ps_name(code)
.and_then(|c| self.base_font.get_width(c)),
}
}
pub(crate) fn char_code_to_unicode(&self, code: u8) -> Option<char> {
self.code_to_ps_name(code).and_then(glyph_name_to_unicode)
}
pub(crate) fn is_italic(&self) -> bool {
self.base_font.is_italic()
}
pub(crate) fn is_bold(&self) -> bool {
self.base_font.is_bold()
}
pub(crate) fn is_serif(&self) -> bool {
self.base_font.is_serif()
}
pub(crate) fn is_monospace(&self) -> bool {
self.base_font.is_monospace()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_widths(entries: &[(u8, f32)]) -> Vec<Width> {
let mut widths = vec![Width::Missing; 256];
for (code, width) in entries {
widths[*code as usize] = Width::Value(*width);
}
widths
}
fn build_standard_kind(widths: Vec<Width>, missing_width: Option<f32>) -> StandardKind {
let (data, index) = StandardFont::Helvetica.get_font_data();
let base_font_blob =
StandardFontBlob::from_data(data, index).expect("standard font data should parse");
StandardKind {
base_font: StandardFont::Helvetica,
base_font_blob,
encoding: Encoding::WinAnsi,
widths,
missing_width,
fallback: true,
glyph_to_code: RefCell::new(HashMap::new()),
encodings: HashMap::new(),
}
}
#[test]
fn glyph_width_falls_back_to_base_metrics_when_missing_width_is_absent() {
let font = build_standard_kind(build_widths(&[(b'A', 600.0)]), None);
assert_eq!(font.glyph_width(b'A'), Some(600.0));
assert_eq!(
font.glyph_width(b'B'),
StandardFont::Helvetica.get_width("B")
);
}
#[test]
fn glyph_width_respects_explicit_zero_missing_width() {
let font = build_standard_kind(build_widths(&[(b'A', 600.0)]), Some(0.0));
assert_eq!(font.glyph_width(b'A'), Some(600.0));
assert_eq!(font.glyph_width(b'B'), Some(0.0));
}
#[test]
fn arial_aliases_resolve_to_helvetica_family() {
assert!(matches!(
standard_font_alias("ArialMT"),
Some(StandardFont::Helvetica)
));
assert!(matches!(
standard_font_alias("Arial-BoldMT"),
Some(StandardFont::HelveticaBold)
));
assert!(matches!(
standard_font_alias("Arial-ItalicMT"),
Some(StandardFont::HelveticaOblique)
));
assert!(matches!(
standard_font_alias("Arial-BoldItalicMT"),
Some(StandardFont::HelveticaBoldOblique)
));
}
#[test]
fn times_new_roman_aliases_resolve_to_times_family() {
assert!(matches!(
standard_font_alias("TimesNewRomanPSMT"),
Some(StandardFont::TimesRoman)
));
assert!(matches!(
standard_font_alias("TimesNewRomanPS-BoldMT"),
Some(StandardFont::TimesBold)
));
assert!(matches!(
standard_font_alias("TimesNewRomanPS-ItalicMT"),
Some(StandardFont::TimesItalic)
));
assert!(matches!(
standard_font_alias("TimesNewRomanPS-BoldItalicMT"),
Some(StandardFont::TimesBoldItalic)
));
}
#[test]
fn courier_new_aliases_resolve_to_courier_family() {
assert!(matches!(
standard_font_alias("CourierNewPSMT"),
Some(StandardFont::Courier)
));
assert!(matches!(
standard_font_alias("CourierNewPS-BoldMT"),
Some(StandardFont::CourierBold)
));
assert!(matches!(
standard_font_alias("CourierNewPS-ItalicMT"),
Some(StandardFont::CourierOblique)
));
assert!(matches!(
standard_font_alias("CourierNewPS-BoldItalicMT"),
Some(StandardFont::CourierBoldOblique)
));
}
#[test]
fn unknown_names_do_not_alias() {
assert!(standard_font_alias("LiberationSans").is_none());
assert!(standard_font_alias("CenturySchoolbook").is_none());
assert!(standard_font_alias("").is_none());
}
}