fontique 0.8.0

Font enumeration and fallback.
Documentation
// Copyright 2024 the Parley Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Scanning files and memory for fonts.

#![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")]
/// Font collection generated by scanning the file system.
#[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 {
    /// Creates a new collection by scanning the given paths for
    /// font files.
    pub fn from_paths(paths: impl IntoIterator<Item = impl AsRef<Path>>, max_depth: u32) -> Self {
        scan_collection(paths, max_depth)
    }
}

/// Font generated by scanning the file system or a memory buffer.
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> {
    /// Returns the name string for the given identifier.
    pub fn english_or_first_name(&self, id: NameId) -> Option<name::NameString<'a>> {
        english_or_first(&self.name_table, id)
    }
}

#[cfg(feature = "std")]
/// Scans paths and invokes the given function for each font discovered.
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);
    }
}

/// Scans a memory buffer and invokes the given function for each font
/// discovered.
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 {
    // Find the "English or first" name first. We'll use that as the default
    // name.
    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);
        }
    }
    // Add the "best" name first
    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);
        }
    }
    // And then the rest
    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())
}