#![allow(dead_code, unused_imports)]
use super::{
family::{FamilyId, FamilyInfo},
family_name::{FamilyName, FamilyNameMap},
font::FontInfo,
};
use alloc::string::String;
use alloc::vec;
use hashbrown::HashMap;
use read_fonts::{FileRef, FontRef, TableProvider as _, tables::name, types::NameId};
use smallvec::SmallVec;
#[cfg(feature = "std")]
use {super::source::SourcePathMap, std::path::Path};
use alloc::vec::Vec;
#[cfg(feature = "std")]
#[derive(Default)]
pub struct ScannedCollection {
pub family_names: FamilyNameMap,
pub postscript_names: HashMap<String, FamilyId>,
pub data_paths: SourcePathMap,
pub families: HashMap<FamilyId, FamilyInfo>,
}
#[cfg(feature = "std")]
impl ScannedCollection {
pub fn from_paths(paths: impl IntoIterator<Item = impl AsRef<Path>>, max_depth: u32) -> Self {
scan_collection(paths, max_depth)
}
}
pub struct ScannedFont<'a> {
pub font: FontRef<'a>,
#[cfg(feature = "std")]
pub path: Option<&'a Path>,
pub index: u32,
pub name_table: name::Name<'a>,
}
impl<'a> ScannedFont<'a> {
pub fn english_or_first_name(&self, id: NameId) -> Option<name::NameString<'a>> {
english_or_first(&self.name_table, id)
}
}
#[cfg(feature = "std")]
pub fn scan_paths(
paths: impl IntoIterator<Item = impl AsRef<Path>>,
max_depth: u32,
mut f: impl FnMut(&ScannedFont<'_>),
) {
for path in paths {
scan_path_impl(path.as_ref(), max_depth, &mut f, 0);
}
}
pub fn scan_memory<'a>(buf: &'a [u8], mut f: impl FnMut(&ScannedFont<'a>)) {
#[allow(clippy::unit_arg)]
scan_memory_impl(buf, ScanMemoryPathType::default(), &mut f);
}
#[cfg(feature = "std")]
fn scan_collection(
paths: impl IntoIterator<Item = impl AsRef<Path>>,
max_depth: u32,
) -> ScannedCollection {
let mut collection = ScannedCollection::default();
let mut families: HashMap<FamilyId, (FamilyName, SmallVec<[FontInfo; 4]>)> = HashMap::default();
let mut postscript_name = String::default();
let mut name_pool = vec![];
let mut names = vec![];
scan_paths(paths, max_depth, |scanned_font| {
let Some(path) = &scanned_font.path else {
return;
};
name_pool.append(&mut names);
postscript_name.clear();
if !all_names(
&scanned_font.name_table,
NameId::TYPOGRAPHIC_FAMILY_NAME,
&mut name_pool,
&mut names,
) && !all_names(
&scanned_font.name_table,
NameId::FAMILY_NAME,
&mut name_pool,
&mut names,
) {
return;
}
let postscript_chars = scanned_font
.english_or_first_name(NameId::POSTSCRIPT_NAME)
.map(|name| name.chars());
if let Some(chars) = postscript_chars {
postscript_name.extend(chars);
} else {
return;
}
let data = collection.data_paths.get_or_insert(path);
let Some(font) = FontInfo::from_font_ref(&scanned_font.font, data, scanned_font.index)
else {
return;
};
let [first_name, other_names @ ..] = names.as_slice() else {
return;
};
let name = collection.family_names.get_or_insert(first_name);
for other_name in other_names {
collection.family_names.add_alias(name.id(), other_name);
}
collection
.postscript_names
.insert(postscript_name.clone(), name.id());
families
.entry(name.id())
.or_insert_with(|| (name.clone(), SmallVec::default()))
.1
.push(font);
});
collection.families.extend(
families
.drain()
.map(|(id, (name, fonts))| (id, FamilyInfo::new(name, fonts))),
);
collection
}
#[cfg(feature = "std")]
fn scan_path_impl(
path: &Path,
max_depth: u32,
f: &mut impl FnMut(&ScannedFont<'_>),
depth: u32,
) -> Option<()> {
let metadata = path.metadata().ok()?;
if metadata.is_dir() {
if depth > max_depth {
return None;
}
for entry in std::fs::read_dir(path).ok()?.filter_map(|entry| entry.ok()) {
scan_path_impl(entry.path().as_path(), max_depth, f, depth + 1);
}
} else {
let file = std::fs::File::open(path).ok()?;
let mapped = unsafe { memmap2::Mmap::map(&file) }.ok()?;
scan_memory_impl(&mapped, Some(path), f);
}
Some(())
}
#[cfg(feature = "std")]
type ScanMemoryPathType<'a> = Option<&'a Path>;
#[cfg(not(feature = "std"))]
type ScanMemoryPathType<'a> = ();
fn scan_memory_impl<'a>(
data: &'a [u8],
path: ScanMemoryPathType<'a>,
f: &mut impl FnMut(&ScannedFont<'a>),
) -> Option<()> {
let font_file = FileRef::new(data).ok()?;
match font_file {
FileRef::Font(font) => {
scan_font(font, path, 0, f);
}
FileRef::Collection(collection) => {
for i in 0..collection.len() {
let Ok(font) = collection.get(i) else {
continue;
};
scan_font(font, path, i, f);
}
}
}
Some(())
}
fn scan_font<'a>(
font: FontRef<'a>,
#[allow(unused)] path: ScanMemoryPathType<'a>,
index: u32,
f: &mut impl FnMut(&ScannedFont<'a>),
) -> Option<()> {
let name_table = font.name().ok()?;
f(&ScannedFont {
font,
#[cfg(feature = "std")]
path,
index,
name_table,
});
Some(())
}
fn all_names(
name_table: &name::Name<'_>,
id: NameId,
pool: &mut Vec<String>,
result: &mut Vec<String>,
) -> bool {
let mut best_index = 0;
let mut best_rank = -1;
let mut best_record = None;
for (i, record) in name_table
.name_record()
.iter()
.enumerate()
.filter(|x| x.1.name_id() == id)
{
let rank = match (i, record.language_id()) {
(_, 0x0409) => {
best_index = i;
best_record = Some(record);
break;
}
(_, 0) => 2,
(0, _) => 1,
_ => continue,
};
if rank > best_rank {
best_rank = rank;
best_index = i;
best_record = Some(record);
}
}
if let Some(best) = best_record.and_then(|rec| rec.string(name_table.string_data()).ok()) {
let mut str = pool.pop().unwrap_or_default();
str.clear();
str.extend(best.chars());
if !str.is_empty() {
result.push(str);
} else {
pool.push(str);
}
}
for (i, record) in name_table
.name_record()
.iter()
.enumerate()
.filter(|x| x.1.name_id() == id)
{
if best_record.is_some() && i == best_index {
continue;
}
let Ok(name_str) = record.string(name_table.string_data()) else {
continue;
};
let mut str = pool.pop().unwrap_or_default();
str.clear();
str.extend(name_str.chars());
if !str.is_empty() {
result.push(str);
} else {
pool.push(str);
}
}
!result.is_empty()
}
fn english_or_first<'a>(names: &name::Name<'a>, id: NameId) -> Option<name::NameString<'a>> {
let mut best_rank = -1;
let mut best_record = None;
for (i, record) in names
.name_record()
.iter()
.enumerate()
.filter(|x| x.1.name_id() == id)
{
let rank = match (i, record.language_id()) {
(_, 0x0409) => {
best_record = Some(record);
break;
}
(_, 0) => 2,
(0, _) => 1,
_ => continue,
};
if rank > best_rank {
best_rank = rank;
best_record = Some(record);
}
}
best_record.and_then(|record| record.string(names.string_data()).ok())
}