use std::path::{Path, PathBuf};
use std::fs;
pub use ttf_parser::{GlyphId, Weight, Width as Stretch};
use crate::tree;
use crate::utils;
#[cfg(all(unix, not(target_os = "macos")))]
const GENERIC_FAMILIES: &[&str] = &["serif", "sans-serif", "monospace", "cursive", "fantasy"];
#[derive(Clone, Debug)]
pub struct FontItem {
pub id: ID,
pub path: PathBuf,
pub face_index: u32,
pub family: String,
pub properties: Properties,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct ID(u16);
pub struct Database {
fonts: Vec<FontItem>,
#[allow(dead_code)]
has_generic_fonts: bool,
}
impl Database {
pub fn new() -> Self {
Database {
fonts: Vec::new(),
has_generic_fonts: false,
}
}
pub fn populate(&mut self) {
if self.fonts.is_empty() {
load_all_fonts(&mut self.fonts);
}
}
#[inline(never)]
#[cfg(all(unix, not(target_os = "macos")))]
fn collect_generic_fonts(&mut self) {
if self.fonts.is_empty() {
return;
}
for family in GENERIC_FAMILIES {
self.collect_generic_font(family);
}
self.has_generic_fonts = true;
}
#[cfg(all(unix, not(target_os = "macos")))]
fn collect_generic_font(&mut self, generic_family: &str) -> Option<()> {
let output = std::process::Command::new("fc-match")
.arg(generic_family)
.arg("--format=%{family}")
.output().ok();
let output = try_opt_warn_or!(output, None, "Failed to run 'fc-match'.");
let family = std::str::from_utf8(&output.stdout).ok()?.trim();
duplicate_family(family, generic_family, &mut self.fonts);
Some(())
}
pub fn font(&self, id: ID) -> &FontItem {
&self.fonts[id.0 as usize]
}
pub fn fonts(&self) -> &[FontItem] {
&self.fonts
}
#[inline(never)]
pub fn select_best_match(
&mut self,
family_names: &[&str],
properties: Properties,
) -> Option<ID> {
for family_name in family_names {
#[cfg(all(unix, not(target_os = "macos")))]
{
if !self.has_generic_fonts && GENERIC_FAMILIES.contains(family_name) {
self.collect_generic_fonts();
}
}
let mut ids = Vec::new();
let mut candidates = Vec::new();
for item in self.fonts.iter().filter(|font| &font.family == family_name) {
ids.push(item.id);
candidates.push(item.properties);
}
if let Some(index) = find_best_match(&candidates, properties) {
return Some(ids[index]);
}
}
None
}
#[inline(never)]
pub fn outline(&self, id: ID, glyph_id: GlyphId) -> Option<tree::PathData> {
let item = self.font(id);
let file = fs::File::open(&item.path).ok()?;
let mmap = unsafe { memmap2::MmapOptions::new().map(&file).ok()? };
let font = ttf_parser::Font::from_data(&mmap, item.face_index)?;
let mut builder = PathBuilder { path: tree::PathData::with_capacity(16) };
font.outline_glyph(glyph_id, &mut builder)?;
Some(builder.path)
}
#[inline(never)]
pub fn has_char(&self, id: ID, c: char) -> bool {
self._has_char(id, c).unwrap_or(false)
}
fn _has_char(&self, id: ID, c: char) -> Option<bool> {
let item = self.font(id);
let file = fs::File::open(&item.path).ok()?;
let mmap = unsafe { memmap2::MmapOptions::new().map(&file).ok()? };
let font = ttf_parser::Font::from_data(&mmap, item.face_index)?;
font.glyph_index(c)?;
Some(true)
}
#[inline(never)]
pub fn load_font(&self, id: ID) -> Option<Font> {
let item = self.font(id);
let file = fs::File::open(&item.path).ok()?;
let mmap = unsafe { memmap2::MmapOptions::new().map(&file).ok()? };
let font = ttf_parser::Font::from_data(&mmap, item.face_index)?;
let units_per_em = font.units_per_em()?;
let ascent = font.ascender();
let descent = font.descender();
let x_height = match font.x_height() {
Some(height) => height,
None => {
(f32::from(ascent - descent) * 0.45) as i16
}
};
let underline = match font.underline_metrics() {
Some(metrics) => metrics,
None => {
ttf_parser::LineMetrics {
position: -(units_per_em as i16) / 9,
thickness: units_per_em as i16 / 12,
}
}
};
let line_through_position = match font.strikeout_metrics() {
Some(metrics) => metrics.position,
None => x_height / 2,
};
let mut subscript_offset = (units_per_em as f32 / 0.2).round() as i16;
let mut superscript_offset = (units_per_em as f32 / 0.4).round() as i16;
if let Some(metrics) = font.subscript_metrics() {
subscript_offset = metrics.y_offset;
}
if let Some(metrics) = font.superscript_metrics() {
superscript_offset = metrics.y_offset;
}
Some(Font {
id,
units_per_em,
ascent,
descent,
x_height,
underline_position: underline.position,
underline_thickness: underline.thickness,
line_through_position,
subscript_offset,
superscript_offset,
})
}
}
#[derive(Clone, Copy)]
pub struct Font {
pub id: ID,
units_per_em: u16,
ascent: i16,
descent: i16,
x_height: i16,
underline_position: i16,
underline_thickness: i16,
line_through_position: i16,
subscript_offset: i16,
superscript_offset: i16,
}
impl Font {
#[inline]
pub fn scale(&self, font_size: f64) -> f64 {
font_size / self.units_per_em as f64
}
#[inline]
pub fn ascent(&self, font_size: f64) -> f64 {
self.ascent as f64 * self.scale(font_size)
}
#[inline]
pub fn descent(&self, font_size: f64) -> f64 {
self.descent as f64 * self.scale(font_size)
}
#[inline]
pub fn height(&self, font_size: f64) -> f64 {
self.ascent(font_size) - self.descent(font_size)
}
#[inline]
pub fn x_height(&self, font_size: f64) -> f64 {
self.x_height as f64 * self.scale(font_size)
}
#[inline]
pub fn underline_position(&self, font_size: f64) -> f64 {
self.underline_position as f64 * self.scale(font_size)
}
#[inline]
pub fn underline_thickness(&self, font_size: f64) -> f64 {
self.underline_thickness as f64 * self.scale(font_size)
}
#[inline]
pub fn line_through_position(&self, font_size: f64) -> f64 {
self.line_through_position as f64 * self.scale(font_size)
}
#[inline]
pub fn subscript_offset(&self, font_size: f64) -> f64 {
self.subscript_offset as f64 * self.scale(font_size)
}
#[inline]
pub fn superscript_offset(&self, font_size: f64) -> f64 {
self.superscript_offset as f64 * self.scale(font_size)
}
}
#[derive(Clone, Copy, PartialEq, Default, Debug)]
pub struct Properties {
pub style: Style,
pub weight: Weight,
pub stretch: Stretch,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Style {
Normal,
Italic,
Oblique,
}
impl Default for Style {
fn default() -> Style {
Style::Normal
}
}
impl_enum_from_str!(Style,
"normal" => Style::Normal,
"italic" => Style::Italic,
"oblique" => Style::Oblique
);
#[inline(never)]
fn find_best_match(
candidates: &[Properties],
query: Properties,
) -> Option<usize> {
let weight = query.weight.to_number();
let mut matching_set: Vec<usize> = (0..candidates.len()).collect();
if matching_set.is_empty() {
return None;
}
let matching_stretch = if matching_set
.iter()
.any(|&index| candidates[index].stretch == query.stretch)
{
query.stretch
} else if query.stretch <= Stretch::Normal {
match matching_set
.iter()
.filter(|&&index| candidates[index].stretch < query.stretch)
.min_by_key(|&&index| {
query.stretch.to_number() - candidates[index].stretch.to_number()
}) {
Some(&matching_index) => candidates[matching_index].stretch,
None => {
let matching_index = *matching_set
.iter()
.min_by_key(|&&index| {
candidates[index].stretch.to_number() - query.stretch.to_number()
})
.unwrap();
candidates[matching_index].stretch
}
}
} else {
match matching_set
.iter()
.filter(|&&index| candidates[index].stretch > query.stretch)
.min_by_key(|&&index| {
candidates[index].stretch.to_number() - query.stretch.to_number()
}) {
Some(&matching_index) => candidates[matching_index].stretch,
None => {
let matching_index = *matching_set
.iter()
.min_by_key(|&&index| {
query.stretch.to_number() - candidates[index].stretch.to_number()
})
.unwrap();
candidates[matching_index].stretch
}
}
};
matching_set.retain(|&index| candidates[index].stretch == matching_stretch);
let style_preference = match query.style {
Style::Italic => [Style::Italic, Style::Oblique, Style::Normal],
Style::Oblique => [Style::Oblique, Style::Italic, Style::Normal],
Style::Normal => [Style::Normal, Style::Oblique, Style::Italic],
};
let matching_style = *style_preference
.iter()
.filter(|&query_style| {
matching_set
.iter()
.any(|&index| candidates[index].style == *query_style)
})
.next()
.unwrap();
matching_set.retain(|&index| candidates[index].style == matching_style);
let matching_weight = if weight >= 400
&& weight < 450
&& matching_set
.iter()
.any(|&index| candidates[index].weight.to_number() == 500)
{
Weight::Medium
} else if weight >= 450 && weight <= 500 && matching_set
.iter()
.any(|&index| candidates[index].weight.to_number() == 400)
{
Weight::Normal
} else if weight <= 500 {
match matching_set
.iter()
.filter(|&&index| candidates[index].weight.to_number() <= weight)
.min_by_key(|&&index| weight - candidates[index].weight.to_number())
{
Some(&matching_index) => candidates[matching_index].weight,
None => {
let matching_index = *matching_set
.iter()
.min_by_key(|&&index| {
candidates[index].weight.to_number() - weight
})
.unwrap();
candidates[matching_index].weight
}
}
} else {
match matching_set
.iter()
.filter(|&&index| candidates[index].weight.to_number() >= weight)
.min_by_key(|&&index| candidates[index].weight.to_number() - weight)
{
Some(&matching_index) => candidates[matching_index].weight,
None => {
let matching_index = *matching_set
.iter()
.min_by_key(|&&index| {
weight - candidates[index].weight.to_number()
})
.unwrap();
candidates[matching_index].weight
}
}
};
matching_set.retain(|&index| candidates[index].weight == matching_weight);
matching_set.into_iter().next()
}
struct PathBuilder {
path: tree::PathData,
}
impl ttf_parser::OutlineBuilder for PathBuilder {
fn move_to(&mut self, x: f32, y: f32) {
self.path.push_move_to(x as f64, y as f64);
}
fn line_to(&mut self, x: f32, y: f32) {
self.path.push_line_to(x as f64, y as f64);
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.path.push_quad_to(
x1 as f64, y1 as f64,
x as f64, y as f64,
);
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.path.push_curve_to(
x1 as f64, y1 as f64,
x2 as f64, y2 as f64,
x as f64, y as f64
);
}
fn close(&mut self) {
self.path.push_close_path();
}
}
#[cfg(all(unix, not(target_os = "macos")))]
fn load_all_fonts(fonts: &mut Vec<FontItem>) {
load_fonts_from("/usr/share/fonts/", fonts);
load_fonts_from("/usr/local/share/fonts/", fonts);
if let Ok(ref home) = std::env::var("HOME") {
let path = Path::new(home).join(".local/share/fonts");
load_fonts_from(path.to_str().unwrap(), fonts);
}
}
#[cfg(target_os = "windows")]
fn load_all_fonts(fonts: &mut Vec<FontItem>) {
load_fonts_from("C:\\Windows\\Fonts\\", fonts);
duplicate_family("Times New Roman", "serif", fonts);
duplicate_family("Arial", "sans-serif", fonts);
duplicate_family("Courier New", "monospace", fonts);
duplicate_family("Comic Sans MS", "cursive", fonts);
duplicate_family("Impact", "fantasy", fonts);
}
#[cfg(target_os = "macos")]
fn load_all_fonts(fonts: &mut Vec<FontItem>) {
load_fonts_from("/Library/Fonts", fonts);
load_fonts_from("/System/Library/Fonts", fonts);
if let Ok(ref home) = std::env::var("HOME") {
let path = Path::new(home).join("Library/Fonts");
load_fonts_from(path.to_str().unwrap(), fonts);
}
duplicate_family("Times New Roman", "serif", fonts);
duplicate_family("Arial", "sans-serif", fonts);
duplicate_family("Courier New", "monospace", fonts);
duplicate_family("Comic Sans MS", "cursive", fonts);
duplicate_family("Papyrus", "fantasy", fonts);
}
fn load_fonts_from(dir: &str, fonts: &mut Vec<FontItem>) {
let fonts_dir = try_opt!(std::fs::read_dir(dir).ok());
for entry in fonts_dir {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_file() {
match utils::file_extension(&path) {
Some("ttf") | Some("ttc") | Some("TTF") | Some("TTC") |
Some("otf") | Some("otc") | Some("OTF") | Some("OTC") => {
let _ = load_font(&path, fonts);
}
_ => {}
}
} else if path.is_dir() {
load_fonts_from(path.to_str().unwrap(), fonts);
}
}
}
}
fn load_font(
path: &Path,
fonts: &mut Vec<FontItem>,
) -> Result<(), Box<dyn std::error::Error>> {
let file = fs::File::open(path)?;
let mmap = unsafe { memmap2::MmapOptions::new().map(&file)? };
let n = ttf_parser::fonts_in_collection(&mmap).unwrap_or(1);
for index in 0..n {
if let Some(item) = resolve_font(&mmap, path, index, fonts.len()) {
fonts.push(item);
}
}
Ok(())
}
fn resolve_font(
data: &[u8],
path: &Path,
index: u32,
id: usize,
) -> Option<FontItem> {
let font = ttf_parser::Font::from_data(data, index)?;
let family = font.family_name()?;
let style = if font.is_italic() {
Style::Italic
} else if font.is_oblique() {
Style::Oblique
} else {
Style::Normal
};
let weight = font.weight();
let stretch = font.width();
let properties = Properties { style, weight, stretch };
Some(FontItem {
id: ID(id as u16),
path: path.to_path_buf(),
face_index: index,
family,
properties,
})
}
fn duplicate_family(
old_family: &str,
new_family: &str,
fonts: &mut Vec<FontItem>,
) {
let mut i = 0;
while i < fonts.len() {
if fonts[i].family == old_family {
let mut new_font = fonts[i].clone();
new_font.family = new_family.to_string();
fonts.push(new_font);
}
i += 1;
}
}