use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::path;
use std::sync::Arc;
use crate::error::{Context as _, Error, ErrorKind};
use crate::render;
use crate::style::Style;
use crate::Mm;
#[derive(Debug)]
pub struct FontCache {
pub(crate) fonts: Vec<FontData>,
pdf_fonts: Vec<printpdf::IndirectFontRef>,
default_font_family: Option<FontFamily<Font>>,
embedded_font_cache: HashMap<*const Vec<u8>, printpdf::IndirectFontRef>,
}
impl FontCache {
pub fn new(default_font_family: FontFamily<FontData>) -> FontCache {
let mut font_cache = FontCache {
fonts: Vec::new(),
pdf_fonts: Vec::new(),
default_font_family: None,
embedded_font_cache: HashMap::new(),
};
font_cache.default_font_family = Some(font_cache.add_font_family(default_font_family));
font_cache
}
pub fn add_font(&mut self, font_data: FontData) -> Font {
let is_builtin = match &font_data.raw_data {
RawFontData::Builtin(_) => true,
RawFontData::Embedded(_) => false,
};
let font = Font::new(self.fonts.len(), is_builtin, &font_data.rt_font);
self.fonts.push(font_data);
font
}
pub fn add_font_family(&mut self, family: FontFamily<FontData>) -> FontFamily<Font> {
FontFamily {
regular: self.add_font(family.regular),
bold: self.add_font(family.bold),
italic: self.add_font(family.italic),
bold_italic: self.add_font(family.bold_italic),
}
}
pub fn load_pdf_fonts(&mut self, renderer: &render::Renderer) -> Result<(), Error> {
self.pdf_fonts.clear();
self.embedded_font_cache.clear();
for font in &self.fonts {
let pdf_font = match &font.raw_data {
RawFontData::Builtin(builtin) => renderer.add_builtin_font(*builtin)?,
RawFontData::Embedded(data) => {
let data_ptr = Arc::as_ptr(data);
if let Some(cached_font_ref) = self.embedded_font_cache.get(&data_ptr) {
cached_font_ref.clone()
} else {
let font_ref = renderer.add_embedded_font(data.as_ref())?;
self.embedded_font_cache.insert(data_ptr, font_ref.clone());
font_ref
}
}
};
self.pdf_fonts.push(pdf_font);
}
Ok(())
}
pub fn default_font_family(&self) -> FontFamily<Font> {
self.default_font_family
.expect("Invariant violated: no default font family for FontCache")
}
pub fn get_pdf_font(&self, font: Font) -> Option<&printpdf::IndirectFontRef> {
self.pdf_fonts.get(font.idx)
}
pub fn get_rt_font(&self, font: Font) -> &rusttype::Font<'static> {
&self.fonts[font.idx].rt_font
}
}
#[derive(Clone, Debug)]
pub struct FontData {
rt_font: rusttype::Font<'static>,
raw_data: RawFontData,
glyph_id_map: Option<Arc<GlyphIdMap>>,
}
impl FontData {
pub fn new(data: Vec<u8>, builtin: Option<printpdf::BuiltinFont>) -> Result<FontData, Error> {
let raw_data = if let Some(builtin) = builtin {
RawFontData::Builtin(builtin)
} else {
RawFontData::Embedded(Arc::new(data.clone()))
};
let rt_font = rusttype::Font::from_bytes(data).context("Failed to read rusttype font")?;
if rt_font.units_per_em() == 0 {
Err(Error::new(
"The font is not scalable",
ErrorKind::InvalidFont,
))
} else {
Ok(FontData {
rt_font,
raw_data,
glyph_id_map: None,
})
}
}
pub fn new_shared(
shared_data: Arc<Vec<u8>>,
builtin: Option<printpdf::BuiltinFont>,
) -> Result<FontData, Error> {
let raw_data = if let Some(builtin) = builtin {
RawFontData::Builtin(builtin)
} else {
RawFontData::Embedded(shared_data.clone())
};
let rt_font = rusttype::Font::from_bytes(shared_data.to_vec())
.context("Failed to read rusttype font")?;
if rt_font.units_per_em() == 0 {
Err(Error::new(
"The font is not scalable",
ErrorKind::InvalidFont,
))
} else {
Ok(FontData {
rt_font,
raw_data,
glyph_id_map: None,
})
}
}
pub fn clone_with_data(
source: &FontData,
embed_data: Arc<Vec<u8>>,
glyph_id_map: Option<GlyphIdMap>,
) -> FontData {
FontData {
rt_font: source.rt_font.clone(),
raw_data: RawFontData::Embedded(embed_data),
glyph_id_map: glyph_id_map.map(Arc::new),
}
}
pub fn new_with_subset(
metrics_data: Arc<Vec<u8>>,
embed_data: Arc<Vec<u8>>,
glyph_id_map: GlyphIdMap,
) -> Result<FontData, Error> {
let rt_font = rusttype::Font::from_bytes(metrics_data.to_vec())
.context("Failed to read font for metrics")?;
if rt_font.units_per_em() == 0 {
return Err(Error::new(
"The font is not scalable",
ErrorKind::InvalidFont,
));
}
Ok(FontData {
rt_font,
raw_data: RawFontData::Embedded(embed_data),
glyph_id_map: Some(Arc::new(glyph_id_map)),
})
}
pub fn load(
path: impl AsRef<path::Path>,
builtin: Option<printpdf::BuiltinFont>,
) -> Result<FontData, Error> {
let data = fs::read(path.as_ref())
.with_context(|| format!("Failed to open font file {}", path.as_ref().display()))?;
FontData::new(data, builtin)
}
pub fn get_data(&self) -> Result<&[u8], Error> {
match &self.raw_data {
RawFontData::Embedded(data) => Ok(data.as_ref()),
RawFontData::Builtin(_) => Err(Error::new(
"Cannot get raw data from built-in font".to_string(),
ErrorKind::InvalidFont,
)),
}
}
pub fn has_glyph(&self, c: char) -> bool {
self.rt_font.glyph(c).id().0 != 0
}
pub fn check_coverage(&self, text: &str) -> GlyphCoverage {
let mut missing_chars = Vec::new();
let unique_chars: std::collections::HashSet<char> = text.chars().collect();
for c in unique_chars.iter() {
if !self.has_glyph(*c) {
missing_chars.push(*c);
}
}
GlyphCoverage {
total_unique: unique_chars.len(),
covered: unique_chars.len() - missing_chars.len(),
missing_chars,
}
}
}
#[derive(Clone, Debug)]
pub struct GlyphCoverage {
total_unique: usize,
covered: usize,
missing_chars: Vec<char>,
}
impl GlyphCoverage {
pub fn coverage_percent(&self) -> f32 {
if self.total_unique == 0 {
100.0
} else {
(self.covered as f32 / self.total_unique as f32) * 100.0
}
}
pub fn is_complete(&self) -> bool {
self.missing_chars.is_empty()
}
pub fn missing_chars(&self) -> &[char] {
&self.missing_chars
}
pub fn covered_count(&self) -> usize {
self.covered
}
pub fn total_count(&self) -> usize {
self.total_unique
}
}
#[derive(Debug, Clone, Default)]
pub struct GlyphIdMap {
mapping: std::collections::HashMap<char, u16>,
}
impl GlyphIdMap {
pub fn new() -> Self {
Self {
mapping: std::collections::HashMap::new(),
}
}
pub fn insert(&mut self, c: char, subset_glyph_id: u16) {
self.mapping.insert(c, subset_glyph_id);
}
pub fn get(&self, c: char) -> Option<u16> {
self.mapping.get(&c).copied()
}
pub fn len(&self) -> usize {
self.mapping.len()
}
pub fn is_empty(&self) -> bool {
self.mapping.is_empty()
}
}
#[derive(Clone, Debug)]
pub struct FontFallbackChain {
primary: FontData,
fallbacks: Vec<FontData>,
}
impl FontFallbackChain {
pub fn new(primary: FontData) -> Self {
Self {
primary,
fallbacks: Vec::new(),
}
}
pub fn with_fallback(mut self, fallback: FontData) -> Self {
self.fallbacks.push(fallback);
self
}
pub fn find_font_for_char(&self, c: char) -> &FontData {
if self.primary.has_glyph(c) {
return &self.primary;
}
for fallback in &self.fallbacks {
if fallback.has_glyph(c) {
return fallback;
}
}
&self.primary
}
pub fn primary(&self) -> &FontData {
&self.primary
}
pub fn fallbacks(&self) -> &[FontData] {
&self.fallbacks
}
pub fn check_coverage(&self, text: &str) -> GlyphCoverage {
let unique_chars: std::collections::HashSet<char> = text.chars().collect();
let mut missing_chars = Vec::new();
for c in unique_chars.iter() {
let has_glyph = self.primary.has_glyph(*c)
|| self.fallbacks.iter().any(|f| f.has_glyph(*c));
if !has_glyph {
missing_chars.push(*c);
}
}
GlyphCoverage {
total_unique: unique_chars.len(),
covered: unique_chars.len() - missing_chars.len(),
missing_chars,
}
}
pub fn segment_text(&self, text: &str) -> Vec<(String, &FontData)> {
let mut segments = Vec::new();
let mut current_segment = String::new();
let mut current_font: Option<&FontData> = None;
for c in text.chars() {
let font_for_char = self.find_font_for_char(c);
if let Some(current) = current_font {
if !std::ptr::eq(current, font_for_char) {
if !current_segment.is_empty() {
segments.push((current_segment.clone(), current));
current_segment.clear();
}
current_font = Some(font_for_char);
}
} else {
current_font = Some(font_for_char);
}
current_segment.push(c);
}
if !current_segment.is_empty() {
if let Some(font) = current_font {
segments.push((current_segment, font));
}
}
segments
}
}
#[derive(Clone, Debug)]
enum RawFontData {
Builtin(printpdf::BuiltinFont),
Embedded(Arc<Vec<u8>>),
}
#[derive(Clone, Copy, Debug)]
enum FontStyle {
Regular,
Bold,
Italic,
BoldItalic,
}
impl FontStyle {
fn name(&self) -> &'static str {
match self {
FontStyle::Regular => "Regular",
FontStyle::Bold => "Bold",
FontStyle::Italic => "Italic",
FontStyle::BoldItalic => "BoldItalic",
}
}
}
impl fmt::Display for FontStyle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Builtin {
Times,
Helvetica,
Courier,
}
impl Builtin {
fn style(&self, style: FontStyle) -> printpdf::BuiltinFont {
match self {
Builtin::Times => match style {
FontStyle::Regular => printpdf::BuiltinFont::TimesRoman,
FontStyle::Bold => printpdf::BuiltinFont::TimesBold,
FontStyle::Italic => printpdf::BuiltinFont::TimesItalic,
FontStyle::BoldItalic => printpdf::BuiltinFont::TimesBoldItalic,
},
Builtin::Helvetica => match style {
FontStyle::Regular => printpdf::BuiltinFont::Helvetica,
FontStyle::Bold => printpdf::BuiltinFont::HelveticaBold,
FontStyle::Italic => printpdf::BuiltinFont::HelveticaOblique,
FontStyle::BoldItalic => printpdf::BuiltinFont::HelveticaBoldOblique,
},
Builtin::Courier => match style {
FontStyle::Regular => printpdf::BuiltinFont::Courier,
FontStyle::Bold => printpdf::BuiltinFont::CourierBold,
FontStyle::Italic => printpdf::BuiltinFont::CourierOblique,
FontStyle::BoldItalic => printpdf::BuiltinFont::CourierBoldOblique,
},
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FontFamily<T: Clone + fmt::Debug> {
pub regular: T,
pub bold: T,
pub italic: T,
pub bold_italic: T,
}
impl<T: Clone + Copy + fmt::Debug + PartialEq> FontFamily<T> {
pub fn get(&self, style: Style) -> T {
if style.is_bold() && style.is_italic() {
self.bold_italic
} else if style.is_bold() {
self.bold
} else if style.is_italic() {
self.italic
} else {
self.regular
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Font {
idx: usize,
is_builtin: bool,
scale: rusttype::Scale,
line_height: Mm,
glyph_height: Mm,
ascent: Mm,
descent: Mm,
}
impl Font {
fn new(idx: usize, is_builtin: bool, rt_font: &rusttype::Font<'static>) -> Font {
let units_per_em = rt_font.units_per_em();
assert!(units_per_em != 0);
let units_per_em = f32::from(units_per_em);
let v_metrics = rt_font.v_metrics_unscaled();
let glyph_height = (v_metrics.ascent - v_metrics.descent) / units_per_em;
let scale = rusttype::Scale::uniform(glyph_height);
let ascent = v_metrics.ascent / units_per_em;
let descent = v_metrics.descent / units_per_em;
let line_height = glyph_height + v_metrics.line_gap / units_per_em;
Font {
idx,
is_builtin,
scale,
line_height: printpdf::Pt(f32::from(line_height)).into(),
glyph_height: printpdf::Pt(f32::from(glyph_height)).into(),
ascent: printpdf::Pt(f32::from(ascent)).into(),
descent: printpdf::Pt(f32::from(descent)).into(),
}
}
pub fn is_builtin(&self) -> bool {
self.is_builtin
}
pub fn get_line_height(&self, font_size: u8) -> Mm {
self.line_height * f32::from(font_size)
}
pub fn glyph_height(&self, font_size: u8) -> Mm {
self.glyph_height * f32::from(font_size)
}
pub fn ascent(&self, font_size: u8) -> Mm {
self.ascent * f32::from(font_size)
}
pub fn descent(&self, font_size: u8) -> Mm {
self.descent * f32::from(font_size)
}
pub fn char_width(&self, font_cache: &FontCache, c: char, font_size: u8) -> Mm {
let advance_width = self.char_h_metrics(font_cache, c).advance_width;
Mm::from(printpdf::Pt(f32::from(
advance_width * f32::from(font_size),
)))
}
pub fn char_left_side_bearing(&self, font_cache: &FontCache, c: char, font_size: u8) -> Mm {
let left_side_bearing = self.char_h_metrics(font_cache, c).left_side_bearing;
Mm::from(printpdf::Pt(f32::from(
left_side_bearing * f32::from(font_size),
)))
}
fn char_h_metrics(&self, font_cache: &FontCache, c: char) -> rusttype::HMetrics {
if self.is_builtin {
self.builtin_char_h_metrics(c)
} else {
font_cache
.get_rt_font(*self)
.glyph(c)
.scaled(self.scale)
.h_metrics()
}
}
fn builtin_char_h_metrics(&self, c: char) -> rusttype::HMetrics {
let advance_width = match c {
' ' => 0.278, '!' => 0.278, '"' => 0.355, '#' => 0.556, '$' => 0.556, '%' => 0.889, '&' => 0.667, '\'' => 0.191, '(' => 0.333, ')' => 0.333, '*' => 0.389, '+' => 0.584, ',' => 0.278, '-' => 0.333, '.' => 0.278, '/' => 0.278, '0'..='9' => 0.556, ':' => 0.278, ';' => 0.278, '<' => 0.584, '=' => 0.584, '>' => 0.584, '?' => 0.556, '@' => 1.015, 'A' => 0.667, 'B' => 0.667, 'C' => 0.722, 'D' => 0.722, 'E' => 0.667, 'F' => 0.611, 'G' => 0.778, 'H' => 0.722, 'I' => 0.278, 'J' => 0.500, 'K' => 0.667, 'L' => 0.556, 'M' => 0.833, 'N' => 0.722, 'O' => 0.778, 'P' => 0.667, 'Q' => 0.778, 'R' => 0.722, 'S' => 0.667, 'T' => 0.611, 'U' => 0.722, 'V' => 0.667, 'W' => 0.944, 'X' => 0.667, 'Y' => 0.667, 'Z' => 0.611, '[' => 0.278, '\\' => 0.278, ']' => 0.278, '^' => 0.469, '_' => 0.556, '`' => 0.333, 'a' => 0.556, 'b' => 0.556, 'c' => 0.500, 'd' => 0.556, 'e' => 0.556, 'f' => 0.278, 'g' => 0.556, 'h' => 0.556, 'i' => 0.222, 'j' => 0.222, 'k' => 0.500, 'l' => 0.222, 'm' => 0.833, 'n' => 0.556, 'o' => 0.556, 'p' => 0.556, 'q' => 0.556, 'r' => 0.333, 's' => 0.500, 't' => 0.278, 'u' => 0.556, 'v' => 0.500, 'w' => 0.722, 'x' => 0.500, 'y' => 0.500, 'z' => 0.500, '{' => 0.334, '|' => 0.260, '}' => 0.334, '~' => 0.584, _ => 0.556, };
rusttype::HMetrics {
advance_width: advance_width,
left_side_bearing: 0.0, }
}
pub fn str_width(&self, font_cache: &FontCache, s: &str, font_size: u8) -> Mm {
let str_width: Mm = if self.is_builtin {
s.chars()
.map(|c| self.builtin_char_h_metrics(c).advance_width)
.map(|w| Mm::from(printpdf::Pt(f32::from(w * f32::from(font_size)))))
.sum()
} else {
font_cache
.get_rt_font(*self)
.glyphs_for(s.chars())
.map(|g| g.scaled(self.scale).h_metrics().advance_width)
.map(|w| Mm::from(printpdf::Pt(f32::from(w * f32::from(font_size)))))
.sum()
};
let kerning_width: Mm = self
.kerning(font_cache, s.chars())
.into_iter()
.map(|val| val * f32::from(font_size))
.map(|val| Mm::from(printpdf::Pt(f32::from(val))))
.sum();
str_width + kerning_width
}
pub fn kerning<I>(&self, font_cache: &FontCache, iter: I) -> Vec<f32>
where
I: IntoIterator<Item = char>,
{
if self.is_builtin {
iter.into_iter().map(|_| 0.0).collect()
} else {
let font = font_cache.get_rt_font(*self);
font.glyphs_for(iter.into_iter())
.scan(None, |last, g| {
let pos = if let Some(last) = last {
Some(font.pair_kerning(self.scale, *last, g.id()))
} else {
Some(0.0)
};
*last = Some(g.id());
pos
})
.collect()
}
}
pub fn glyph_ids<I>(&self, font_cache: &FontCache, iter: I) -> Vec<u16>
where
I: IntoIterator<Item = char>,
{
let font_data = &font_cache.fonts[self.idx];
let font = font_cache.get_rt_font(*self);
if let Some(ref glyph_map) = font_data.glyph_id_map {
iter.into_iter()
.map(|c| {
glyph_map
.get(c)
.unwrap_or_else(|| font.glyph(c).id().0 as u16)
})
.collect()
} else {
font.glyphs_for(iter.into_iter())
.map(|g| g.id().0 as u16)
.collect()
}
}
pub fn metrics(&self, font_size: u8) -> Metrics {
Metrics::new(
self.line_height * f32::from(font_size),
self.glyph_height * f32::from(font_size),
self.ascent * f32::from(font_size),
self.descent * f32::from(font_size),
)
}
}
fn from_file(
dir: impl AsRef<path::Path>,
name: &str,
style: FontStyle,
builtin: Option<Builtin>,
) -> Result<FontData, Error> {
let builtin = builtin.map(|b| b.style(style));
FontData::load(
&dir.as_ref().join(format!("{}-{}.ttf", name, style)),
builtin,
)
}
pub fn from_files(
dir: impl AsRef<path::Path>,
name: &str,
builtin: Option<Builtin>,
) -> Result<FontFamily<FontData>, Error> {
let dir = dir.as_ref();
Ok(FontFamily {
regular: from_file(dir, name, FontStyle::Regular, builtin)?,
bold: from_file(dir, name, FontStyle::Bold, builtin)?,
italic: from_file(dir, name, FontStyle::Italic, builtin)?,
bold_italic: from_file(dir, name, FontStyle::BoldItalic, builtin)?,
})
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Metrics {
pub line_height: Mm,
pub glyph_height: Mm,
pub ascent: Mm,
pub descent: Mm,
}
impl Metrics {
pub fn new(line_height: Mm, glyph_height: Mm, ascent: Mm, descent: Mm) -> Metrics {
Metrics {
line_height,
glyph_height,
ascent,
descent,
}
}
pub fn max(&self, other: &Self) -> Self {
Self {
line_height: self.line_height.max(other.line_height),
glyph_height: self.glyph_height.max(other.glyph_height),
ascent: self.ascent.max(other.ascent),
descent: self.descent.max(other.descent),
}
}
}