use crate::error::TextError;
use crate::types::FontDescriptor;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
pub struct CharacterSet {
bmp: [u64; 1024],
smp: HashSet<u32>,
}
impl CharacterSet {
pub fn new() -> Self {
Self {
bmp: [0; 1024],
smp: HashSet::new(),
}
}
pub fn insert(&mut self, cp: u32) {
if cp < 0x10000 {
let idx = (cp / 64) as usize;
let bit = cp % 64;
self.bmp[idx] |= 1u64 << bit;
} else {
self.smp.insert(cp);
}
}
#[inline]
pub fn contains(&self, cp: u32) -> bool {
if cp < 0x10000 {
let idx = (cp / 64) as usize;
let bit = cp % 64;
(self.bmp[idx] >> bit) & 1 != 0
} else {
self.smp.contains(&cp)
}
}
pub fn len(&self) -> usize {
let bmp_count: usize = self.bmp.iter().map(|w| w.count_ones() as usize).sum();
bmp_count + self.smp.len()
}
pub fn is_empty(&self) -> bool {
self.bmp.iter().all(|&w| w == 0) && self.smp.is_empty()
}
}
impl Default for CharacterSet {
fn default() -> Self {
Self::new()
}
}
pub struct DeferredFont {
pub descriptor: FontDescriptor,
charset: CharacterSet,
font_path: PathBuf,
}
impl DeferredFont {
pub fn new(descriptor: FontDescriptor) -> Result<Self, TextError> {
let font_path = resolve_font_path(&descriptor)?;
let charset = extract_charset(&font_path)?;
Ok(Self {
descriptor,
charset,
font_path,
})
}
pub fn from_descriptor_with_path(descriptor: FontDescriptor) -> Result<Self, TextError> {
let font_path = descriptor
.path
.clone()
.ok_or_else(|| TextError::FontNotFound {
family: descriptor.family.clone(),
})?;
let charset = extract_charset(&font_path)?;
Ok(Self {
descriptor,
charset,
font_path,
})
}
#[inline]
pub fn has_codepoint(&self, cp: u32) -> bool {
self.charset.contains(cp)
}
#[inline]
pub fn has_char(&self, c: char) -> bool {
self.charset.contains(c as u32)
}
pub fn path(&self) -> &Path {
&self.font_path
}
pub fn read_bytes(&self) -> Result<Vec<u8>, TextError> {
std::fs::read(&self.font_path).map_err(|e| TextError::FontFileLoad(e.to_string()))
}
pub fn charset_size(&self) -> usize {
self.charset.len()
}
}
fn extract_charset(path: &Path) -> Result<CharacterSet, TextError> {
use std::fs::File;
let file = File::open(path)
.map_err(|e| TextError::FontFileLoad(format!("{}: {}", path.display(), e)))?;
let mmap = unsafe { memmap2::Mmap::map(&file) }
.map_err(|e| TextError::FontFileLoad(format!("mmap {}: {}", path.display(), e)))?;
let face = ttf_parser::Face::parse(&mmap, 0)
.map_err(|e| TextError::FontFileLoad(format!("parse {}: {}", path.display(), e)))?;
let mut charset = CharacterSet::new();
if let Some(cmap) = face.tables().cmap {
for subtable in cmap.subtables {
if !subtable.is_unicode() {
continue;
}
subtable.codepoints(|cp| {
charset.insert(cp);
});
}
}
Ok(charset)
}
#[cfg(target_os = "windows")]
fn resolve_font_path(desc: &FontDescriptor) -> Result<PathBuf, TextError> {
if let Some(ref path) = desc.path
&& path.exists()
{
return Ok(path.clone());
}
let font_dirs = [
std::env::var("WINDIR")
.map(|w| PathBuf::from(w).join("Fonts"))
.ok(),
std::env::var("LOCALAPPDATA")
.map(|l| PathBuf::from(l).join("Microsoft\\Windows\\Fonts"))
.ok(),
];
let family_clean = desc.family.replace(' ', "");
let patterns = [
format!("{}.ttf", family_clean),
format!("{}.otf", family_clean),
format!("{}.ttc", family_clean),
format!("{}Regular.ttf", family_clean),
format!("{}-Regular.ttf", family_clean),
];
for dir in font_dirs.iter().flatten() {
for pattern in &patterns {
let candidate = dir.join(pattern);
if candidate.exists() {
return Ok(candidate);
}
}
}
Err(TextError::FontNotFound {
family: desc.family.clone(),
})
}
#[cfg(target_os = "macos")]
fn resolve_font_path(desc: &FontDescriptor) -> Result<PathBuf, TextError> {
if let Some(ref path) = desc.path
&& path.exists()
{
return Ok(path.clone());
}
let font_dirs = [
PathBuf::from("/System/Library/Fonts"),
PathBuf::from("/Library/Fonts"),
dirs::home_dir()
.map(|h| h.join("Library/Fonts"))
.unwrap_or_default(),
];
let family_clean = desc.family.replace(' ', "");
let patterns = [
format!("{}.ttf", family_clean),
format!("{}.otf", family_clean),
format!("{}.ttc", family_clean),
format!("{}.dfont", family_clean),
];
for dir in &font_dirs {
for pattern in &patterns {
let candidate = dir.join(pattern);
if candidate.exists() {
return Ok(candidate);
}
}
}
Err(TextError::FontNotFound {
family: desc.family.clone(),
})
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
fn resolve_font_path(desc: &FontDescriptor) -> Result<PathBuf, TextError> {
if let Some(ref path) = desc.path
&& path.exists()
{
return Ok(path.clone());
}
Err(TextError::FontNotFound {
family: desc.family.clone(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn charset_bmp_insert_and_check() {
let mut cs = CharacterSet::new();
cs.insert('A' as u32);
cs.insert('z' as u32);
cs.insert(0x4E00);
assert!(cs.contains('A' as u32));
assert!(cs.contains('z' as u32));
assert!(cs.contains(0x4E00));
assert!(!cs.contains('B' as u32));
}
#[test]
fn charset_smp_insert_and_check() {
let mut cs = CharacterSet::new();
cs.insert(0x1F600); cs.insert(0x1F4A9);
assert!(cs.contains(0x1F600));
assert!(cs.contains(0x1F4A9));
assert!(!cs.contains(0x1F601));
}
#[test]
fn charset_len_counts_correctly() {
let mut cs = CharacterSet::new();
assert_eq!(cs.len(), 0);
assert!(cs.is_empty());
cs.insert('A' as u32);
cs.insert('B' as u32);
cs.insert(0x1F600);
assert_eq!(cs.len(), 3);
assert!(!cs.is_empty());
}
}