#![allow(non_snake_case)]
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
#[cfg(all(feature = "std", feature = "parsing"))]
use alloc::borrow::ToOwned;
use alloc::collections::btree_map::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use alloc::{format, vec};
#[cfg(all(feature = "std", feature = "parsing"))]
use allsorts::binary::read::ReadScope;
#[cfg(all(feature = "std", feature = "parsing"))]
use allsorts::get_name::fontcode_get_name;
#[cfg(all(feature = "std", feature = "parsing"))]
use allsorts::tables::os2::Os2;
#[cfg(all(feature = "std", feature = "parsing"))]
use allsorts::tables::{FontTableProvider, HheaTable, HmtxTable, MaxpTable};
#[cfg(all(feature = "std", feature = "parsing"))]
use allsorts::tag;
#[cfg(all(feature = "std", feature = "parsing"))]
use std::path::PathBuf;
pub mod utils;
#[cfg(feature = "std")]
pub mod config;
#[cfg(feature = "ffi")]
pub mod ffi;
#[cfg(feature = "async-registry")]
pub mod scoring;
#[cfg(feature = "async-registry")]
pub mod registry;
#[cfg(feature = "async-registry")]
pub mod multithread;
#[cfg(feature = "cache")]
pub mod disk_cache;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OperatingSystem {
Windows,
Linux,
MacOS,
Wasm,
}
impl OperatingSystem {
pub fn current() -> Self {
#[cfg(target_os = "windows")]
return OperatingSystem::Windows;
#[cfg(target_os = "linux")]
return OperatingSystem::Linux;
#[cfg(target_os = "macos")]
return OperatingSystem::MacOS;
#[cfg(target_family = "wasm")]
return OperatingSystem::Wasm;
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos", target_family = "wasm")))]
return OperatingSystem::Linux; }
pub fn get_serif_fonts(&self, unicode_ranges: &[UnicodeRange]) -> Vec<String> {
let has_cjk = has_cjk_ranges(unicode_ranges);
let has_arabic = has_arabic_ranges(unicode_ranges);
let _has_cyrillic = has_cyrillic_ranges(unicode_ranges);
match self {
OperatingSystem::Windows => {
let mut fonts = Vec::new();
if has_cjk {
fonts.extend_from_slice(&["MS Mincho", "SimSun", "MingLiU"]);
}
if has_arabic {
fonts.push("Traditional Arabic");
}
fonts.push("Times New Roman");
fonts.iter().map(|s| s.to_string()).collect()
}
OperatingSystem::Linux => {
let mut fonts = Vec::new();
if has_cjk {
fonts.extend_from_slice(&["Noto Serif CJK SC", "Noto Serif CJK JP", "Noto Serif CJK KR"]);
}
if has_arabic {
fonts.push("Noto Serif Arabic");
}
fonts.extend_from_slice(&[
"Times", "Times New Roman", "DejaVu Serif", "Free Serif",
"Noto Serif", "Bitstream Vera Serif", "Roman", "Regular"
]);
fonts.iter().map(|s| s.to_string()).collect()
}
OperatingSystem::MacOS => {
let mut fonts = Vec::new();
if has_cjk {
fonts.extend_from_slice(&["Hiragino Mincho ProN", "STSong", "AppleMyungjo"]);
}
if has_arabic {
fonts.push("Geeza Pro");
}
fonts.extend_from_slice(&["Times", "New York", "Palatino"]);
fonts.iter().map(|s| s.to_string()).collect()
}
OperatingSystem::Wasm => Vec::new(),
}
}
pub fn get_sans_serif_fonts(&self, unicode_ranges: &[UnicodeRange]) -> Vec<String> {
let has_cjk = has_cjk_ranges(unicode_ranges);
let has_arabic = has_arabic_ranges(unicode_ranges);
let _has_cyrillic = has_cyrillic_ranges(unicode_ranges);
let has_hebrew = has_hebrew_ranges(unicode_ranges);
let has_thai = has_thai_ranges(unicode_ranges);
match self {
OperatingSystem::Windows => {
let mut fonts = Vec::new();
if has_cjk {
fonts.extend_from_slice(&["Microsoft YaHei", "MS Gothic", "Malgun Gothic", "SimHei"]);
}
if has_arabic {
fonts.push("Segoe UI Arabic");
}
if has_hebrew {
fonts.push("Segoe UI Hebrew");
}
if has_thai {
fonts.push("Leelawadee UI");
}
fonts.extend_from_slice(&["Segoe UI", "Tahoma", "Microsoft Sans Serif", "MS Sans Serif", "Helv"]);
fonts.iter().map(|s| s.to_string()).collect()
}
OperatingSystem::Linux => {
let mut fonts = Vec::new();
if has_cjk {
fonts.extend_from_slice(&[
"Noto Sans CJK SC", "Noto Sans CJK JP", "Noto Sans CJK KR",
"WenQuanYi Micro Hei", "Droid Sans Fallback"
]);
}
if has_arabic {
fonts.push("Noto Sans Arabic");
}
if has_hebrew {
fonts.push("Noto Sans Hebrew");
}
if has_thai {
fonts.push("Noto Sans Thai");
}
fonts.extend_from_slice(&["Ubuntu", "Arial", "DejaVu Sans", "Noto Sans", "Liberation Sans"]);
fonts.iter().map(|s| s.to_string()).collect()
}
OperatingSystem::MacOS => {
let mut fonts = Vec::new();
if has_cjk {
fonts.extend_from_slice(&[
"Hiragino Sans", "Hiragino Kaku Gothic ProN",
"PingFang SC", "PingFang TC", "Apple SD Gothic Neo"
]);
}
if has_arabic {
fonts.push("Geeza Pro");
}
if has_hebrew {
fonts.push("Arial Hebrew");
}
if has_thai {
fonts.push("Thonburi");
}
fonts.extend_from_slice(&["San Francisco", "Helvetica Neue", "Lucida Grande"]);
fonts.iter().map(|s| s.to_string()).collect()
}
OperatingSystem::Wasm => Vec::new(),
}
}
pub fn get_monospace_fonts(&self, unicode_ranges: &[UnicodeRange]) -> Vec<String> {
let has_cjk = has_cjk_ranges(unicode_ranges);
match self {
OperatingSystem::Windows => {
let mut fonts = Vec::new();
if has_cjk {
fonts.extend_from_slice(&["MS Gothic", "SimHei"]);
}
fonts.extend_from_slice(&["Segoe UI Mono", "Courier New", "Cascadia Code", "Cascadia Mono", "Consolas"]);
fonts.iter().map(|s| s.to_string()).collect()
}
OperatingSystem::Linux => {
let mut fonts = Vec::new();
if has_cjk {
fonts.extend_from_slice(&["Noto Sans Mono CJK SC", "Noto Sans Mono CJK JP", "WenQuanYi Zen Hei Mono"]);
}
fonts.extend_from_slice(&[
"Source Code Pro", "Cantarell", "DejaVu Sans Mono",
"Roboto Mono", "Ubuntu Monospace", "Droid Sans Mono"
]);
fonts.iter().map(|s| s.to_string()).collect()
}
OperatingSystem::MacOS => {
let mut fonts = Vec::new();
if has_cjk {
fonts.extend_from_slice(&["Hiragino Sans", "PingFang SC"]);
}
fonts.extend_from_slice(&["SF Mono", "Menlo", "Monaco", "Courier", "Oxygen Mono", "Source Code Pro", "Fira Mono"]);
fonts.iter().map(|s| s.to_string()).collect()
}
OperatingSystem::Wasm => Vec::new(),
}
}
pub fn expand_generic_family(&self, family: &str, unicode_ranges: &[UnicodeRange]) -> Vec<String> {
match family.to_lowercase().as_str() {
"serif" => self.get_serif_fonts(unicode_ranges),
"sans-serif" => self.get_sans_serif_fonts(unicode_ranges),
"monospace" => self.get_monospace_fonts(unicode_ranges),
"cursive" | "fantasy" | "system-ui" => {
self.get_sans_serif_fonts(unicode_ranges)
}
_ => vec![family.to_string()],
}
}
}
pub fn expand_font_families(families: &[String], os: OperatingSystem, unicode_ranges: &[UnicodeRange]) -> Vec<String> {
let mut expanded = Vec::new();
for family in families {
expanded.extend(os.expand_generic_family(family, unicode_ranges));
}
expanded
}
#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
pub struct FontId(pub u128);
impl core::fmt::Debug for FontId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
core::fmt::Display::fmt(self, f)
}
}
impl core::fmt::Display for FontId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let id = self.0;
write!(
f,
"{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
(id >> 96) & 0xFFFFFFFF,
(id >> 80) & 0xFFFF,
(id >> 64) & 0xFFFF,
(id >> 48) & 0xFFFF,
id & 0xFFFFFFFFFFFF
)
}
}
impl FontId {
pub fn new() -> Self {
use core::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(1);
let id = COUNTER.fetch_add(1, Ordering::Relaxed) as u128;
FontId(id)
}
}
#[derive(Debug, Default, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
#[repr(C)]
pub enum PatternMatch {
#[default]
DontCare,
True,
False,
}
impl PatternMatch {
fn needs_to_match(&self) -> bool {
matches!(self, PatternMatch::True | PatternMatch::False)
}
fn matches(&self, other: &PatternMatch) -> bool {
match (self, other) {
(PatternMatch::DontCare, _) => true,
(_, PatternMatch::DontCare) => true,
(a, b) => a == b,
}
}
}
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
#[repr(C)]
pub enum FcWeight {
Thin = 100,
ExtraLight = 200,
Light = 300,
Normal = 400,
Medium = 500,
SemiBold = 600,
Bold = 700,
ExtraBold = 800,
Black = 900,
}
impl FcWeight {
pub fn from_u16(weight: u16) -> Self {
match weight {
0..=149 => FcWeight::Thin,
150..=249 => FcWeight::ExtraLight,
250..=349 => FcWeight::Light,
350..=449 => FcWeight::Normal,
450..=549 => FcWeight::Medium,
550..=649 => FcWeight::SemiBold,
650..=749 => FcWeight::Bold,
750..=849 => FcWeight::ExtraBold,
_ => FcWeight::Black,
}
}
pub fn find_best_match(&self, available: &[FcWeight]) -> Option<FcWeight> {
if available.is_empty() {
return None;
}
if available.contains(self) {
return Some(*self);
}
let self_value = *self as u16;
match *self {
FcWeight::Normal => {
if available.contains(&FcWeight::Medium) {
return Some(FcWeight::Medium);
}
for weight in &[FcWeight::Light, FcWeight::ExtraLight, FcWeight::Thin] {
if available.contains(weight) {
return Some(*weight);
}
}
for weight in &[
FcWeight::SemiBold,
FcWeight::Bold,
FcWeight::ExtraBold,
FcWeight::Black,
] {
if available.contains(weight) {
return Some(*weight);
}
}
}
FcWeight::Medium => {
if available.contains(&FcWeight::Normal) {
return Some(FcWeight::Normal);
}
for weight in &[FcWeight::Light, FcWeight::ExtraLight, FcWeight::Thin] {
if available.contains(weight) {
return Some(*weight);
}
}
for weight in &[
FcWeight::SemiBold,
FcWeight::Bold,
FcWeight::ExtraBold,
FcWeight::Black,
] {
if available.contains(weight) {
return Some(*weight);
}
}
}
FcWeight::Thin | FcWeight::ExtraLight | FcWeight::Light => {
let mut best_match = None;
let mut smallest_diff = u16::MAX;
for weight in available {
let weight_value = *weight as u16;
if weight_value <= self_value {
let diff = self_value - weight_value;
if diff < smallest_diff {
smallest_diff = diff;
best_match = Some(*weight);
}
}
}
if best_match.is_some() {
return best_match;
}
best_match = None;
smallest_diff = u16::MAX;
for weight in available {
let weight_value = *weight as u16;
if weight_value > self_value {
let diff = weight_value - self_value;
if diff < smallest_diff {
smallest_diff = diff;
best_match = Some(*weight);
}
}
}
return best_match;
}
FcWeight::SemiBold | FcWeight::Bold | FcWeight::ExtraBold | FcWeight::Black => {
let mut best_match = None;
let mut smallest_diff = u16::MAX;
for weight in available {
let weight_value = *weight as u16;
if weight_value >= self_value {
let diff = weight_value - self_value;
if diff < smallest_diff {
smallest_diff = diff;
best_match = Some(*weight);
}
}
}
if best_match.is_some() {
return best_match;
}
best_match = None;
smallest_diff = u16::MAX;
for weight in available {
let weight_value = *weight as u16;
if weight_value < self_value {
let diff = self_value - weight_value;
if diff < smallest_diff {
smallest_diff = diff;
best_match = Some(*weight);
}
}
}
return best_match;
}
}
Some(available[0])
}
}
impl Default for FcWeight {
fn default() -> Self {
FcWeight::Normal
}
}
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
#[repr(C)]
pub enum FcStretch {
UltraCondensed = 1,
ExtraCondensed = 2,
Condensed = 3,
SemiCondensed = 4,
Normal = 5,
SemiExpanded = 6,
Expanded = 7,
ExtraExpanded = 8,
UltraExpanded = 9,
}
impl FcStretch {
pub fn is_condensed(&self) -> bool {
use self::FcStretch::*;
match self {
UltraCondensed => true,
ExtraCondensed => true,
Condensed => true,
SemiCondensed => true,
Normal => false,
SemiExpanded => false,
Expanded => false,
ExtraExpanded => false,
UltraExpanded => false,
}
}
pub fn from_u16(width_class: u16) -> Self {
match width_class {
1 => FcStretch::UltraCondensed,
2 => FcStretch::ExtraCondensed,
3 => FcStretch::Condensed,
4 => FcStretch::SemiCondensed,
5 => FcStretch::Normal,
6 => FcStretch::SemiExpanded,
7 => FcStretch::Expanded,
8 => FcStretch::ExtraExpanded,
9 => FcStretch::UltraExpanded,
_ => FcStretch::Normal,
}
}
pub fn find_best_match(&self, available: &[FcStretch]) -> Option<FcStretch> {
if available.is_empty() {
return None;
}
if available.contains(self) {
return Some(*self);
}
if *self <= FcStretch::Normal {
let mut closest_narrower = None;
for stretch in available.iter() {
if *stretch < *self
&& (closest_narrower.is_none() || *stretch > closest_narrower.unwrap())
{
closest_narrower = Some(*stretch);
}
}
if closest_narrower.is_some() {
return closest_narrower;
}
let mut closest_wider = None;
for stretch in available.iter() {
if *stretch > *self
&& (closest_wider.is_none() || *stretch < closest_wider.unwrap())
{
closest_wider = Some(*stretch);
}
}
return closest_wider;
} else {
let mut closest_wider = None;
for stretch in available.iter() {
if *stretch > *self
&& (closest_wider.is_none() || *stretch < closest_wider.unwrap())
{
closest_wider = Some(*stretch);
}
}
if closest_wider.is_some() {
return closest_wider;
}
let mut closest_narrower = None;
for stretch in available.iter() {
if *stretch < *self
&& (closest_narrower.is_none() || *stretch > closest_narrower.unwrap())
{
closest_narrower = Some(*stretch);
}
}
return closest_narrower;
}
}
}
impl Default for FcStretch {
fn default() -> Self {
FcStretch::Normal
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
pub struct UnicodeRange {
pub start: u32,
pub end: u32,
}
impl UnicodeRange {
pub fn contains(&self, c: char) -> bool {
let c = c as u32;
c >= self.start && c <= self.end
}
pub fn overlaps(&self, other: &UnicodeRange) -> bool {
self.start <= other.end && other.start <= self.end
}
pub fn is_subset_of(&self, other: &UnicodeRange) -> bool {
self.start >= other.start && self.end <= other.end
}
}
pub fn has_cjk_ranges(ranges: &[UnicodeRange]) -> bool {
ranges.iter().any(|r| {
(r.start >= 0x4E00 && r.start <= 0x9FFF) ||
(r.start >= 0x3040 && r.start <= 0x309F) ||
(r.start >= 0x30A0 && r.start <= 0x30FF) ||
(r.start >= 0xAC00 && r.start <= 0xD7AF)
})
}
pub fn has_arabic_ranges(ranges: &[UnicodeRange]) -> bool {
ranges.iter().any(|r| r.start >= 0x0600 && r.start <= 0x06FF)
}
pub fn has_cyrillic_ranges(ranges: &[UnicodeRange]) -> bool {
ranges.iter().any(|r| r.start >= 0x0400 && r.start <= 0x04FF)
}
pub fn has_hebrew_ranges(ranges: &[UnicodeRange]) -> bool {
ranges.iter().any(|r| r.start >= 0x0590 && r.start <= 0x05FF)
}
pub fn has_thai_ranges(ranges: &[UnicodeRange]) -> bool {
ranges.iter().any(|r| r.start >= 0x0E00 && r.start <= 0x0E7F)
}
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub enum TraceLevel {
Debug,
Info,
Warning,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum MatchReason {
NameMismatch {
requested: Option<String>,
found: Option<String>,
},
FamilyMismatch {
requested: Option<String>,
found: Option<String>,
},
StyleMismatch {
property: &'static str,
requested: String,
found: String,
},
WeightMismatch {
requested: FcWeight,
found: FcWeight,
},
StretchMismatch {
requested: FcStretch,
found: FcStretch,
},
UnicodeRangeMismatch {
character: char,
ranges: Vec<UnicodeRange>,
},
Success,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraceMsg {
pub level: TraceLevel,
pub path: String,
pub reason: MatchReason,
}
#[repr(C)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
pub enum FcHintStyle {
#[default]
None = 0,
Slight = 1,
Medium = 2,
Full = 3,
}
#[repr(C)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
pub enum FcRgba {
#[default]
Unknown = 0,
Rgb = 1,
Bgr = 2,
Vrgb = 3,
Vbgr = 4,
None = 5,
}
#[repr(C)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
pub enum FcLcdFilter {
#[default]
None = 0,
Default = 1,
Light = 2,
Legacy = 3,
}
#[derive(Debug, Default, Clone, PartialEq, PartialOrd)]
#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
pub struct FcFontRenderConfig {
pub antialias: Option<bool>,
pub hinting: Option<bool>,
pub hintstyle: Option<FcHintStyle>,
pub autohint: Option<bool>,
pub rgba: Option<FcRgba>,
pub lcdfilter: Option<FcLcdFilter>,
pub embeddedbitmap: Option<bool>,
pub embolden: Option<bool>,
pub dpi: Option<f64>,
pub scale: Option<f64>,
pub minspace: Option<bool>,
}
impl Eq for FcFontRenderConfig {}
impl Ord for FcFontRenderConfig {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
let ord = self.antialias.cmp(&other.antialias)
.then_with(|| self.hinting.cmp(&other.hinting))
.then_with(|| self.hintstyle.cmp(&other.hintstyle))
.then_with(|| self.autohint.cmp(&other.autohint))
.then_with(|| self.rgba.cmp(&other.rgba))
.then_with(|| self.lcdfilter.cmp(&other.lcdfilter))
.then_with(|| self.embeddedbitmap.cmp(&other.embeddedbitmap))
.then_with(|| self.embolden.cmp(&other.embolden))
.then_with(|| self.minspace.cmp(&other.minspace));
let ord = ord.then_with(|| {
let a = self.dpi.map(|v| v.to_bits());
let b = other.dpi.map(|v| v.to_bits());
a.cmp(&b)
});
ord.then_with(|| {
let a = self.scale.map(|v| v.to_bits());
let b = other.scale.map(|v| v.to_bits());
a.cmp(&b)
})
}
}
#[derive(Default, Clone, PartialOrd, Ord, PartialEq, Eq)]
#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
#[repr(C)]
pub struct FcPattern {
pub name: Option<String>,
pub family: Option<String>,
pub italic: PatternMatch,
pub oblique: PatternMatch,
pub bold: PatternMatch,
pub monospace: PatternMatch,
pub condensed: PatternMatch,
pub weight: FcWeight,
pub stretch: FcStretch,
pub unicode_ranges: Vec<UnicodeRange>,
pub metadata: FcFontMetadata,
pub render_config: FcFontRenderConfig,
}
impl core::fmt::Debug for FcPattern {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let mut d = f.debug_struct("FcPattern");
if let Some(name) = &self.name {
d.field("name", name);
}
if let Some(family) = &self.family {
d.field("family", family);
}
if self.italic != PatternMatch::DontCare {
d.field("italic", &self.italic);
}
if self.oblique != PatternMatch::DontCare {
d.field("oblique", &self.oblique);
}
if self.bold != PatternMatch::DontCare {
d.field("bold", &self.bold);
}
if self.monospace != PatternMatch::DontCare {
d.field("monospace", &self.monospace);
}
if self.condensed != PatternMatch::DontCare {
d.field("condensed", &self.condensed);
}
if self.weight != FcWeight::Normal {
d.field("weight", &self.weight);
}
if self.stretch != FcStretch::Normal {
d.field("stretch", &self.stretch);
}
if !self.unicode_ranges.is_empty() {
d.field("unicode_ranges", &self.unicode_ranges);
}
let empty_metadata = FcFontMetadata::default();
if self.metadata != empty_metadata {
d.field("metadata", &self.metadata);
}
let empty_render_config = FcFontRenderConfig::default();
if self.render_config != empty_render_config {
d.field("render_config", &self.render_config);
}
d.finish()
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
pub struct FcFontMetadata {
pub copyright: Option<String>,
pub designer: Option<String>,
pub designer_url: Option<String>,
pub font_family: Option<String>,
pub font_subfamily: Option<String>,
pub full_name: Option<String>,
pub id_description: Option<String>,
pub license: Option<String>,
pub license_url: Option<String>,
pub manufacturer: Option<String>,
pub manufacturer_url: Option<String>,
pub postscript_name: Option<String>,
pub preferred_family: Option<String>,
pub preferred_subfamily: Option<String>,
pub trademark: Option<String>,
pub unique_id: Option<String>,
pub version: Option<String>,
}
impl FcPattern {
pub fn contains_char(&self, c: char) -> bool {
if self.unicode_ranges.is_empty() {
return true; }
for range in &self.unicode_ranges {
if range.contains(c) {
return true;
}
}
false
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FontMatch {
pub id: FontId,
pub unicode_ranges: Vec<UnicodeRange>,
pub fallbacks: Vec<FontMatchNoFallback>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FontMatchNoFallback {
pub id: FontId,
pub unicode_ranges: Vec<UnicodeRange>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedFontRun {
pub text: String,
pub start_byte: usize,
pub end_byte: usize,
pub font_id: Option<FontId>,
pub css_source: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FontFallbackChain {
pub css_fallbacks: Vec<CssFallbackGroup>,
pub unicode_fallbacks: Vec<FontMatch>,
pub original_stack: Vec<String>,
}
impl FontFallbackChain {
pub fn resolve_char(&self, cache: &FcFontCache, ch: char) -> Option<(FontId, String)> {
let codepoint = ch as u32;
for group in &self.css_fallbacks {
for font in &group.fonts {
let Some(meta) = cache.get_metadata_by_id(&font.id) else { continue };
if meta.unicode_ranges.is_empty() {
continue; }
if meta.unicode_ranges.iter().any(|r| codepoint >= r.start && codepoint <= r.end) {
return Some((font.id, group.css_name.clone()));
}
}
}
for font in &self.unicode_fallbacks {
let Some(meta) = cache.get_metadata_by_id(&font.id) else { continue };
if meta.unicode_ranges.iter().any(|r| codepoint >= r.start && codepoint <= r.end) {
return Some((font.id, "(unicode-fallback)".to_string()));
}
}
None
}
pub fn resolve_text(&self, cache: &FcFontCache, text: &str) -> Vec<(char, Option<(FontId, String)>)> {
text.chars()
.map(|ch| (ch, self.resolve_char(cache, ch)))
.collect()
}
pub fn query_for_text(&self, cache: &FcFontCache, text: &str) -> Vec<ResolvedFontRun> {
if text.is_empty() {
return Vec::new();
}
let mut runs: Vec<ResolvedFontRun> = Vec::new();
let mut current_font: Option<FontId> = None;
let mut current_css_source: Option<String> = None;
let mut current_start_byte: usize = 0;
for (byte_idx, ch) in text.char_indices() {
let resolved = self.resolve_char(cache, ch);
let (font_id, css_source) = match &resolved {
Some((id, source)) => (Some(*id), Some(source.clone())),
None => (None, None),
};
let font_changed = font_id != current_font;
if font_changed && byte_idx > 0 {
let run_text = &text[current_start_byte..byte_idx];
runs.push(ResolvedFontRun {
text: run_text.to_string(),
start_byte: current_start_byte,
end_byte: byte_idx,
font_id: current_font,
css_source: current_css_source.clone().unwrap_or_default(),
});
current_start_byte = byte_idx;
}
current_font = font_id;
current_css_source = css_source;
}
if current_start_byte < text.len() {
let run_text = &text[current_start_byte..];
runs.push(ResolvedFontRun {
text: run_text.to_string(),
start_byte: current_start_byte,
end_byte: text.len(),
font_id: current_font,
css_source: current_css_source.unwrap_or_default(),
});
}
runs
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CssFallbackGroup {
pub css_name: String,
pub fonts: Vec<FontMatch>,
}
#[cfg(feature = "std")]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct FontChainCacheKey {
pub(crate) font_families: Vec<String>,
pub(crate) weight: FcWeight,
pub(crate) italic: PatternMatch,
pub(crate) oblique: PatternMatch,
}
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
#[repr(C)]
pub struct FcFontPath {
pub path: String,
pub font_index: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C)]
pub struct FcFont {
pub bytes: Vec<u8>,
pub font_index: usize,
pub id: String, }
#[derive(Debug, Clone)]
pub enum FontSource<'a> {
Memory(&'a FcFont),
Disk(&'a FcFontPath),
}
#[derive(Debug, Clone)]
pub struct NamedFont {
pub name: String,
pub bytes: Vec<u8>,
}
impl NamedFont {
pub fn new(name: impl Into<String>, bytes: Vec<u8>) -> Self {
Self {
name: name.into(),
bytes,
}
}
}
#[derive(Debug)]
pub struct FcFontCache {
pub(crate) patterns: BTreeMap<FcPattern, FontId>,
pub(crate) disk_fonts: BTreeMap<FontId, FcFontPath>,
pub(crate) memory_fonts: BTreeMap<FontId, FcFont>,
pub(crate) metadata: BTreeMap<FontId, FcPattern>,
pub(crate) token_index: BTreeMap<String, alloc::collections::BTreeSet<FontId>>,
pub(crate) font_tokens: BTreeMap<FontId, Vec<String>>,
#[cfg(feature = "std")]
pub(crate) chain_cache: std::sync::Mutex<std::collections::HashMap<FontChainCacheKey, FontFallbackChain>>,
}
impl Clone for FcFontCache {
fn clone(&self) -> Self {
Self {
patterns: self.patterns.clone(),
disk_fonts: self.disk_fonts.clone(),
memory_fonts: self.memory_fonts.clone(),
metadata: self.metadata.clone(),
token_index: self.token_index.clone(),
font_tokens: self.font_tokens.clone(),
#[cfg(feature = "std")]
chain_cache: std::sync::Mutex::new(std::collections::HashMap::new()), }
}
}
impl Default for FcFontCache {
fn default() -> Self {
Self {
patterns: BTreeMap::new(),
disk_fonts: BTreeMap::new(),
memory_fonts: BTreeMap::new(),
metadata: BTreeMap::new(),
token_index: BTreeMap::new(),
font_tokens: BTreeMap::new(),
#[cfg(feature = "std")]
chain_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
}
}
}
impl FcFontCache {
pub(crate) fn index_pattern_tokens(&mut self, pattern: &FcPattern, id: FontId) {
let mut all_tokens = Vec::new();
if let Some(name) = &pattern.name {
all_tokens.extend(Self::extract_font_name_tokens(name));
}
if let Some(family) = &pattern.family {
all_tokens.extend(Self::extract_font_name_tokens(family));
}
let tokens_lower: Vec<String> = all_tokens.iter().map(|t| t.to_lowercase()).collect();
for token_lower in &tokens_lower {
self.token_index
.entry(token_lower.clone())
.or_insert_with(alloc::collections::BTreeSet::new)
.insert(id);
}
self.font_tokens.insert(id, tokens_lower);
}
pub fn with_memory_fonts(&mut self, fonts: Vec<(FcPattern, FcFont)>) -> &mut Self {
for (pattern, font) in fonts {
let id = FontId::new();
self.patterns.insert(pattern.clone(), id);
self.metadata.insert(id, pattern.clone());
self.memory_fonts.insert(id, font);
self.index_pattern_tokens(&pattern, id);
}
self
}
pub fn with_memory_font_with_id(
&mut self,
id: FontId,
pattern: FcPattern,
font: FcFont,
) -> &mut Self {
self.patterns.insert(pattern.clone(), id);
self.metadata.insert(id, pattern.clone());
self.memory_fonts.insert(id, font);
self.index_pattern_tokens(&pattern, id);
self
}
pub fn get_font_by_id<'a>(&'a self, id: &FontId) -> Option<FontSource<'a>> {
if let Some(font) = self.memory_fonts.get(id) {
return Some(FontSource::Memory(font));
}
if let Some(path) = self.disk_fonts.get(id) {
return Some(FontSource::Disk(path));
}
None
}
pub fn get_metadata_by_id(&self, id: &FontId) -> Option<&FcPattern> {
self.metadata.get(id)
}
#[cfg(feature = "std")]
pub fn get_font_bytes(&self, id: &FontId) -> Option<Vec<u8>> {
match self.get_font_by_id(id)? {
FontSource::Memory(font) => {
Some(font.bytes.clone())
}
FontSource::Disk(path) => {
std::fs::read(&path.path).ok()
}
}
}
#[cfg(not(all(feature = "std", feature = "parsing")))]
pub fn build() -> Self {
Self::default()
}
#[cfg(all(feature = "std", feature = "parsing"))]
pub fn build() -> Self {
Self::build_inner(None)
}
#[cfg(all(feature = "std", feature = "parsing"))]
pub fn build_with_families(families: &[impl AsRef<str>]) -> Self {
let os = OperatingSystem::current();
let mut target_families: Vec<String> = Vec::new();
for family in families {
let family_str = family.as_ref();
let expanded = os.expand_generic_family(family_str, &[]);
if expanded.is_empty() || (expanded.len() == 1 && expanded[0] == family_str) {
target_families.push(family_str.to_string());
} else {
target_families.extend(expanded);
}
}
Self::build_inner(Some(&target_families))
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn build_inner(family_filter: Option<&[String]>) -> Self {
let mut cache = FcFontCache::default();
let filter_normalized: Option<Vec<String>> = family_filter.map(|families| {
families
.iter()
.map(|f| crate::utils::normalize_family_name(f))
.collect()
});
let matches_filter = |pattern: &FcPattern| -> bool {
match &filter_normalized {
None => true, Some(targets) => {
pattern.name.as_ref().map_or(false, |name| {
let name_norm = crate::utils::normalize_family_name(name);
targets.iter().any(|target| name_norm.contains(target))
}) || pattern.family.as_ref().map_or(false, |family| {
let family_norm = crate::utils::normalize_family_name(family);
targets.iter().any(|target| family_norm.contains(target))
})
}
}
};
#[cfg(target_os = "linux")]
{
if let Some((font_entries, render_configs)) = FcScanDirectories() {
for (mut pattern, path) in font_entries {
if matches_filter(&pattern) {
if let Some(family) = pattern.name.as_ref().or(pattern.family.as_ref()) {
if let Some(rc) = render_configs.get(family) {
pattern.render_config = rc.clone();
}
}
let id = FontId::new();
cache.patterns.insert(pattern.clone(), id);
cache.metadata.insert(id, pattern.clone());
cache.disk_fonts.insert(id, path);
cache.index_pattern_tokens(&pattern, id);
}
}
}
}
#[cfg(target_os = "windows")]
{
let system_root = std::env::var("SystemRoot")
.or_else(|_| std::env::var("WINDIR"))
.unwrap_or_else(|_| "C:\\Windows".to_string());
let user_profile = std::env::var("USERPROFILE")
.unwrap_or_else(|_| "C:\\Users\\Default".to_string());
let font_dirs = vec![
(None, format!("{}\\Fonts\\", system_root)),
(None, format!("{}\\AppData\\Local\\Microsoft\\Windows\\Fonts\\", user_profile)),
];
let font_entries = FcScanDirectoriesInner(&font_dirs);
for (pattern, path) in font_entries {
if matches_filter(&pattern) {
let id = FontId::new();
cache.patterns.insert(pattern.clone(), id);
cache.metadata.insert(id, pattern.clone());
cache.disk_fonts.insert(id, path);
cache.index_pattern_tokens(&pattern, id);
}
}
}
#[cfg(target_os = "macos")]
{
let font_dirs = vec![
(None, "~/Library/Fonts".to_owned()),
(None, "/System/Library/Fonts".to_owned()),
(None, "/Library/Fonts".to_owned()),
(None, "/System/Library/AssetsV2".to_owned()),
];
let font_entries = FcScanDirectoriesInner(&font_dirs);
for (pattern, path) in font_entries {
if matches_filter(&pattern) {
let id = FontId::new();
cache.patterns.insert(pattern.clone(), id);
cache.metadata.insert(id, pattern.clone());
cache.disk_fonts.insert(id, path);
cache.index_pattern_tokens(&pattern, id);
}
}
}
cache
}
pub fn is_memory_font(&self, id: &FontId) -> bool {
self.memory_fonts.contains_key(id)
}
pub fn list(&self) -> Vec<(&FcPattern, FontId)> {
self.patterns
.iter()
.map(|(pattern, id)| (pattern, *id))
.collect()
}
pub fn is_empty(&self) -> bool {
self.patterns.is_empty()
}
pub fn len(&self) -> usize {
self.patterns.len()
}
pub fn query(&self, pattern: &FcPattern, trace: &mut Vec<TraceMsg>) -> Option<FontMatch> {
let mut matches = Vec::new();
for (stored_pattern, id) in &self.patterns {
if Self::query_matches_internal(stored_pattern, pattern, trace) {
let metadata = self.metadata.get(id).unwrap_or(stored_pattern);
let unicode_compatibility = if pattern.unicode_ranges.is_empty() {
Self::calculate_unicode_coverage(&metadata.unicode_ranges) as i32
} else {
Self::calculate_unicode_compatibility(&pattern.unicode_ranges, &metadata.unicode_ranges)
};
let style_score = Self::calculate_style_score(pattern, metadata);
let is_memory = self.memory_fonts.contains_key(id);
matches.push((*id, unicode_compatibility, style_score, metadata.clone(), is_memory));
}
}
matches.sort_by(|a, b| {
b.4.cmp(&a.4)
.then_with(|| b.1.cmp(&a.1)) .then_with(|| a.2.cmp(&b.2)) });
matches.first().map(|(id, _, _, metadata, _)| {
FontMatch {
id: *id,
unicode_ranges: metadata.unicode_ranges.clone(),
fallbacks: Vec::new(), }
})
}
fn query_internal(&self, pattern: &FcPattern, trace: &mut Vec<TraceMsg>) -> Vec<FontMatch> {
let mut matches = Vec::new();
for (stored_pattern, id) in &self.patterns {
if Self::query_matches_internal(stored_pattern, pattern, trace) {
let metadata = self.metadata.get(id).unwrap_or(stored_pattern);
let unicode_compatibility = if pattern.unicode_ranges.is_empty() {
Self::calculate_unicode_coverage(&metadata.unicode_ranges) as i32
} else {
Self::calculate_unicode_compatibility(&pattern.unicode_ranges, &metadata.unicode_ranges)
};
let style_score = Self::calculate_style_score(pattern, metadata);
matches.push((*id, unicode_compatibility, style_score, metadata.clone()));
}
}
matches.sort_by(|a, b| {
a.2.cmp(&b.2) .then_with(|| b.1.cmp(&a.1)) .then_with(|| a.3.italic.cmp(&b.3.italic)) .then_with(|| a.3.name.cmp(&b.3.name)) });
matches
.into_iter()
.map(|(id, _, _, metadata)| {
FontMatch {
id,
unicode_ranges: metadata.unicode_ranges.clone(),
fallbacks: Vec::new(), }
})
.collect()
}
pub fn compute_fallbacks(
&self,
font_id: &FontId,
trace: &mut Vec<TraceMsg>,
) -> Vec<FontMatchNoFallback> {
let pattern = match self.metadata.get(font_id) {
Some(p) => p,
None => return Vec::new(),
};
self.compute_fallbacks_for_pattern(pattern, Some(font_id), trace)
}
fn compute_fallbacks_for_pattern(
&self,
pattern: &FcPattern,
exclude_id: Option<&FontId>,
_trace: &mut Vec<TraceMsg>,
) -> Vec<FontMatchNoFallback> {
let mut candidates = Vec::new();
for (stored_pattern, id) in &self.patterns {
if exclude_id.is_some() && exclude_id.unwrap() == id {
continue;
}
if !stored_pattern.unicode_ranges.is_empty() && !pattern.unicode_ranges.is_empty() {
let unicode_compatibility = Self::calculate_unicode_compatibility(
&pattern.unicode_ranges,
&stored_pattern.unicode_ranges
);
if unicode_compatibility > 0 {
let style_score = Self::calculate_style_score(pattern, stored_pattern);
candidates.push((
FontMatchNoFallback {
id: *id,
unicode_ranges: stored_pattern.unicode_ranges.clone(),
},
unicode_compatibility,
style_score,
stored_pattern.clone(),
));
}
} else if pattern.unicode_ranges.is_empty() && !stored_pattern.unicode_ranges.is_empty() {
let coverage = Self::calculate_unicode_coverage(&stored_pattern.unicode_ranges) as i32;
let style_score = Self::calculate_style_score(pattern, stored_pattern);
candidates.push((
FontMatchNoFallback {
id: *id,
unicode_ranges: stored_pattern.unicode_ranges.clone(),
},
coverage,
style_score,
stored_pattern.clone(),
));
}
}
candidates.sort_by(|a, b| {
b.1.cmp(&a.1)
.then_with(|| a.2.cmp(&b.2))
});
let mut seen_ranges = Vec::new();
let mut deduplicated = Vec::new();
for (id, _, _, pattern) in candidates {
let mut is_new_range = false;
for range in &pattern.unicode_ranges {
if !seen_ranges.iter().any(|r: &UnicodeRange| r.overlaps(range)) {
seen_ranges.push(*range);
is_new_range = true;
}
}
if is_new_range {
deduplicated.push(id);
}
}
deduplicated
}
pub fn get_memory_font(&self, id: &FontId) -> Option<&FcFont> {
self.memory_fonts.get(id)
}
fn trace_path(k: &FcPattern) -> String {
k.name.as_ref().cloned().unwrap_or_else(|| "<unknown>".to_string())
}
pub fn query_matches_internal(
k: &FcPattern,
pattern: &FcPattern,
trace: &mut Vec<TraceMsg>,
) -> bool {
if let Some(ref name) = pattern.name {
if !k.name.as_ref().map_or(false, |kn| kn.contains(name)) {
trace.push(TraceMsg {
level: TraceLevel::Info,
path: Self::trace_path(k),
reason: MatchReason::NameMismatch {
requested: pattern.name.clone(),
found: k.name.clone(),
},
});
return false;
}
}
if let Some(ref family) = pattern.family {
if !k.family.as_ref().map_or(false, |kf| kf.contains(family)) {
trace.push(TraceMsg {
level: TraceLevel::Info,
path: Self::trace_path(k),
reason: MatchReason::FamilyMismatch {
requested: pattern.family.clone(),
found: k.family.clone(),
},
});
return false;
}
}
let style_properties = [
(
"italic",
pattern.italic.needs_to_match(),
pattern.italic.matches(&k.italic),
),
(
"oblique",
pattern.oblique.needs_to_match(),
pattern.oblique.matches(&k.oblique),
),
(
"bold",
pattern.bold.needs_to_match(),
pattern.bold.matches(&k.bold),
),
(
"monospace",
pattern.monospace.needs_to_match(),
pattern.monospace.matches(&k.monospace),
),
(
"condensed",
pattern.condensed.needs_to_match(),
pattern.condensed.matches(&k.condensed),
),
];
for (property_name, needs_to_match, matches) in style_properties {
if needs_to_match && !matches {
let (requested, found) = match property_name {
"italic" => (format!("{:?}", pattern.italic), format!("{:?}", k.italic)),
"oblique" => (format!("{:?}", pattern.oblique), format!("{:?}", k.oblique)),
"bold" => (format!("{:?}", pattern.bold), format!("{:?}", k.bold)),
"monospace" => (
format!("{:?}", pattern.monospace),
format!("{:?}", k.monospace),
),
"condensed" => (
format!("{:?}", pattern.condensed),
format!("{:?}", k.condensed),
),
_ => (String::new(), String::new()),
};
trace.push(TraceMsg {
level: TraceLevel::Info,
path: Self::trace_path(k),
reason: MatchReason::StyleMismatch {
property: property_name,
requested,
found,
},
});
return false;
}
}
if pattern.weight != FcWeight::Normal && pattern.weight != k.weight {
trace.push(TraceMsg {
level: TraceLevel::Info,
path: Self::trace_path(k),
reason: MatchReason::WeightMismatch {
requested: pattern.weight,
found: k.weight,
},
});
return false;
}
if pattern.stretch != FcStretch::Normal && pattern.stretch != k.stretch {
trace.push(TraceMsg {
level: TraceLevel::Info,
path: Self::trace_path(k),
reason: MatchReason::StretchMismatch {
requested: pattern.stretch,
found: k.stretch,
},
});
return false;
}
if !pattern.unicode_ranges.is_empty() {
let mut has_overlap = false;
for p_range in &pattern.unicode_ranges {
for k_range in &k.unicode_ranges {
if p_range.overlaps(k_range) {
has_overlap = true;
break;
}
}
if has_overlap {
break;
}
}
if !has_overlap {
trace.push(TraceMsg {
level: TraceLevel::Info,
path: Self::trace_path(k),
reason: MatchReason::UnicodeRangeMismatch {
character: '\0', ranges: k.unicode_ranges.clone(),
},
});
return false;
}
}
true
}
#[cfg(feature = "std")]
pub fn resolve_font_chain(
&self,
font_families: &[String],
weight: FcWeight,
italic: PatternMatch,
oblique: PatternMatch,
trace: &mut Vec<TraceMsg>,
) -> FontFallbackChain {
self.resolve_font_chain_with_os(font_families, weight, italic, oblique, trace, OperatingSystem::current())
}
#[cfg(feature = "std")]
pub fn resolve_font_chain_with_os(
&self,
font_families: &[String],
weight: FcWeight,
italic: PatternMatch,
oblique: PatternMatch,
trace: &mut Vec<TraceMsg>,
os: OperatingSystem,
) -> FontFallbackChain {
let cache_key = FontChainCacheKey {
font_families: font_families.to_vec(), weight,
italic,
oblique,
};
if let Some(cached) = self.chain_cache.lock().ok().and_then(|c| c.get(&cache_key).cloned()) {
return cached;
}
let expanded_families = expand_font_families(font_families, os, &[]);
let chain = self.resolve_font_chain_uncached(
&expanded_families,
weight,
italic,
oblique,
trace,
);
if let Ok(mut cache) = self.chain_cache.lock() {
cache.insert(cache_key, chain.clone());
}
chain
}
#[cfg(feature = "std")]
fn resolve_font_chain_uncached(
&self,
font_families: &[String],
weight: FcWeight,
italic: PatternMatch,
oblique: PatternMatch,
trace: &mut Vec<TraceMsg>,
) -> FontFallbackChain {
let mut css_fallbacks = Vec::new();
for (_i, family) in font_families.iter().enumerate() {
let (pattern, is_generic) = if config::is_generic_family(family) {
let monospace = if family.eq_ignore_ascii_case("monospace") {
PatternMatch::True
} else {
PatternMatch::False
};
let pattern = FcPattern {
name: None,
weight,
italic,
oblique,
monospace,
unicode_ranges: Vec::new(),
..Default::default()
};
(pattern, true)
} else {
let pattern = FcPattern {
name: Some(family.clone()),
weight,
italic,
oblique,
unicode_ranges: Vec::new(),
..Default::default()
};
(pattern, false)
};
let mut matches = if is_generic {
self.query_internal(&pattern, trace)
} else {
self.fuzzy_query_by_name(family, weight, italic, oblique, &[], trace)
};
if is_generic && matches.len() > 5 {
matches.truncate(5);
}
css_fallbacks.push(CssFallbackGroup {
css_name: family.clone(),
fonts: matches,
});
}
let important_ranges = [
UnicodeRange { start: 0x0400, end: 0x04FF }, UnicodeRange { start: 0x0600, end: 0x06FF }, UnicodeRange { start: 0x0900, end: 0x097F }, UnicodeRange { start: 0x3040, end: 0x309F }, UnicodeRange { start: 0x30A0, end: 0x30FF }, UnicodeRange { start: 0x4E00, end: 0x9FFF }, UnicodeRange { start: 0xAC00, end: 0xD7A3 }, ];
let all_uncovered = vec![false; important_ranges.len()];
let unicode_fallbacks = self.find_unicode_fallbacks(
&important_ranges,
&all_uncovered,
&css_fallbacks,
weight,
italic,
oblique,
trace,
);
FontFallbackChain {
css_fallbacks,
unicode_fallbacks,
original_stack: font_families.to_vec(),
}
}
#[allow(dead_code)]
fn extract_unicode_ranges(text: &str) -> Vec<UnicodeRange> {
let mut chars: Vec<char> = text.chars().collect();
chars.sort_unstable();
chars.dedup();
if chars.is_empty() {
return Vec::new();
}
let mut ranges = Vec::new();
let mut range_start = chars[0] as u32;
let mut range_end = range_start;
for &c in &chars[1..] {
let codepoint = c as u32;
if codepoint == range_end + 1 {
range_end = codepoint;
} else {
ranges.push(UnicodeRange { start: range_start, end: range_end });
range_start = codepoint;
range_end = codepoint;
}
}
ranges.push(UnicodeRange { start: range_start, end: range_end });
ranges
}
#[cfg(feature = "std")]
fn fuzzy_query_by_name(
&self,
requested_name: &str,
weight: FcWeight,
italic: PatternMatch,
oblique: PatternMatch,
unicode_ranges: &[UnicodeRange],
_trace: &mut Vec<TraceMsg>,
) -> Vec<FontMatch> {
let tokens = Self::extract_font_name_tokens(requested_name);
if tokens.is_empty() {
return Vec::new();
}
let tokens_lower: Vec<String> = tokens.iter().map(|t| t.to_lowercase()).collect();
let first_token = &tokens_lower[0];
let mut candidate_ids = match self.token_index.get(first_token) {
Some(ids) if !ids.is_empty() => ids.clone(),
_ => {
return Vec::new();
}
};
for token in &tokens_lower[1..] {
if let Some(token_ids) = self.token_index.get(token) {
let intersection: alloc::collections::BTreeSet<FontId> =
candidate_ids.intersection(token_ids).copied().collect();
if intersection.is_empty() {
break;
} else {
candidate_ids = intersection;
}
} else {
break;
}
}
let mut candidates = Vec::new();
for id in candidate_ids {
let pattern = match self.metadata.get(&id) {
Some(p) => p,
None => continue,
};
let font_tokens_lower = match self.font_tokens.get(&id) {
Some(tokens) => tokens,
None => continue,
};
if font_tokens_lower.is_empty() {
continue;
}
let token_matches = tokens_lower.iter()
.filter(|req_token| {
font_tokens_lower.iter().any(|font_token| {
font_token == *req_token
})
})
.count();
if token_matches == 0 {
continue;
}
let token_similarity = (token_matches * 100 / tokens.len()) as i32;
let unicode_similarity = if !unicode_ranges.is_empty() && !pattern.unicode_ranges.is_empty() {
Self::calculate_unicode_compatibility(unicode_ranges, &pattern.unicode_ranges)
} else {
0
};
if !unicode_ranges.is_empty() && unicode_similarity == 0 {
continue;
}
let style_score = Self::calculate_style_score(&FcPattern {
weight,
italic,
oblique,
..Default::default()
}, pattern);
candidates.push((
id,
token_similarity,
unicode_similarity,
style_score,
pattern.clone(),
));
}
candidates.sort_by(|a, b| {
if !unicode_ranges.is_empty() {
b.1.cmp(&a.1) .then_with(|| b.2.cmp(&a.2)) .then_with(|| a.3.cmp(&b.3)) .then_with(|| a.4.italic.cmp(&b.4.italic)) .then_with(|| a.4.name.cmp(&b.4.name)) } else {
b.1.cmp(&a.1) .then_with(|| a.3.cmp(&b.3)) .then_with(|| a.4.italic.cmp(&b.4.italic)) .then_with(|| a.4.name.cmp(&b.4.name)) }
});
candidates.truncate(5);
candidates
.into_iter()
.map(|(id, _token_sim, _unicode_sim, _style, pattern)| {
FontMatch {
id,
unicode_ranges: pattern.unicode_ranges.clone(),
fallbacks: Vec::new(), }
})
.collect()
}
pub fn extract_font_name_tokens(name: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut current_token = String::new();
let mut last_was_lower = false;
for c in name.chars() {
if c.is_whitespace() || c == '-' || c == '_' {
if !current_token.is_empty() {
tokens.push(current_token.clone());
current_token.clear();
}
last_was_lower = false;
} else if c.is_uppercase() && last_was_lower && !current_token.is_empty() {
tokens.push(current_token.clone());
current_token.clear();
current_token.push(c);
last_was_lower = false;
} else {
current_token.push(c);
last_was_lower = c.is_lowercase();
}
}
if !current_token.is_empty() {
tokens.push(current_token);
}
tokens
}
fn find_unicode_fallbacks(
&self,
unicode_ranges: &[UnicodeRange],
covered_chars: &[bool],
existing_groups: &[CssFallbackGroup],
_weight: FcWeight,
_italic: PatternMatch,
_oblique: PatternMatch,
trace: &mut Vec<TraceMsg>,
) -> Vec<FontMatch> {
let mut uncovered_ranges = Vec::new();
for (i, &covered) in covered_chars.iter().enumerate() {
if !covered && i < unicode_ranges.len() {
uncovered_ranges.push(unicode_ranges[i].clone());
}
}
if uncovered_ranges.is_empty() {
return Vec::new();
}
let pattern = FcPattern {
name: None,
weight: FcWeight::Normal, italic: PatternMatch::DontCare,
oblique: PatternMatch::DontCare,
unicode_ranges: uncovered_ranges.clone(),
..Default::default()
};
let mut candidates = self.query_internal(&pattern, trace);
let existing_prefixes: Vec<String> = existing_groups
.iter()
.flat_map(|group| {
group.fonts.iter().filter_map(|font| {
self.get_metadata_by_id(&font.id)
.and_then(|meta| meta.family.clone())
.and_then(|family| {
family.split_whitespace()
.take(2)
.collect::<Vec<_>>()
.join(" ")
.into()
})
})
})
.collect();
candidates.sort_by(|a, b| {
let a_meta = self.get_metadata_by_id(&a.id);
let b_meta = self.get_metadata_by_id(&b.id);
let a_score = Self::calculate_font_similarity_score(a_meta, &existing_prefixes);
let b_score = Self::calculate_font_similarity_score(b_meta, &existing_prefixes);
b_score.cmp(&a_score) .then_with(|| {
let a_coverage = Self::calculate_unicode_compatibility(&uncovered_ranges, &a.unicode_ranges);
let b_coverage = Self::calculate_unicode_compatibility(&uncovered_ranges, &b.unicode_ranges);
b_coverage.cmp(&a_coverage)
})
});
let mut result = Vec::new();
let mut remaining_uncovered: Vec<bool> = vec![true; uncovered_ranges.len()];
for candidate in candidates {
let mut covers_new_range = false;
for (i, range) in uncovered_ranges.iter().enumerate() {
if remaining_uncovered[i] {
for font_range in &candidate.unicode_ranges {
if font_range.overlaps(range) {
remaining_uncovered[i] = false;
covers_new_range = true;
break;
}
}
}
}
if covers_new_range {
result.push(candidate);
if remaining_uncovered.iter().all(|&uncovered| !uncovered) {
break;
}
}
}
result
}
fn calculate_font_similarity_score(
font_meta: Option<&FcPattern>,
existing_prefixes: &[String],
) -> i32 {
let Some(meta) = font_meta else { return 0; };
let Some(family) = &meta.family else { return 0; };
for prefix in existing_prefixes {
if family.starts_with(prefix) {
return 100; }
if family.contains(prefix) {
return 50; }
}
0 }
pub fn calculate_unicode_coverage(ranges: &[UnicodeRange]) -> u64 {
ranges
.iter()
.map(|range| (range.end - range.start + 1) as u64)
.sum()
}
pub fn calculate_unicode_compatibility(
requested: &[UnicodeRange],
available: &[UnicodeRange],
) -> i32 {
if requested.is_empty() {
return Self::calculate_unicode_coverage(available) as i32;
}
let mut total_coverage = 0u32;
for req_range in requested {
for avail_range in available {
let overlap_start = req_range.start.max(avail_range.start);
let overlap_end = req_range.end.min(avail_range.end);
if overlap_start <= overlap_end {
let overlap_size = overlap_end - overlap_start + 1;
total_coverage += overlap_size;
}
}
}
total_coverage as i32
}
pub fn calculate_style_score(original: &FcPattern, candidate: &FcPattern) -> i32 {
let mut score = 0_i32;
if (original.bold == PatternMatch::True && candidate.weight == FcWeight::Bold)
|| (original.bold == PatternMatch::False && candidate.weight != FcWeight::Bold)
{
} else {
let weight_diff = (original.weight as i32 - candidate.weight as i32).abs();
score += weight_diff as i32;
}
if original.weight == candidate.weight {
score -= 15;
if original.weight == FcWeight::Normal {
score -= 10; }
}
if (original.condensed == PatternMatch::True && candidate.stretch.is_condensed())
|| (original.condensed == PatternMatch::False && !candidate.stretch.is_condensed())
{
} else {
let stretch_diff = (original.stretch as i32 - candidate.stretch as i32).abs();
score += (stretch_diff * 100) as i32;
}
let style_props = [
(original.italic, candidate.italic, 300, 150),
(original.oblique, candidate.oblique, 200, 100),
(original.bold, candidate.bold, 300, 150),
(original.monospace, candidate.monospace, 100, 50),
(original.condensed, candidate.condensed, 100, 50),
];
for (orig, cand, mismatch_penalty, dontcare_penalty) in style_props {
if orig.needs_to_match() {
if orig == PatternMatch::False && cand == PatternMatch::DontCare {
score += dontcare_penalty / 2;
} else if !orig.matches(&cand) {
if cand == PatternMatch::DontCare {
score += dontcare_penalty;
} else {
score += mismatch_penalty;
}
} else if orig == PatternMatch::True && cand == PatternMatch::True {
score -= 20;
} else if orig == PatternMatch::False && cand == PatternMatch::False {
score -= 20;
}
} else {
if cand == PatternMatch::True {
score += dontcare_penalty / 3;
}
}
}
if let (Some(name), Some(family)) = (&candidate.name, &candidate.family) {
let name_lower = name.to_lowercase();
let family_lower = family.to_lowercase();
let extra = if name_lower.starts_with(&family_lower) {
name_lower[family_lower.len()..].to_string()
} else {
String::new()
};
let stripped = extra
.replace("regular", "")
.replace("normal", "")
.replace("book", "")
.replace("roman", "");
let stripped = stripped.trim();
if stripped.is_empty() {
score -= 50;
} else {
let extra_words = stripped.split_whitespace().count();
score += (extra_words as i32) * 25;
}
}
if let Some(ref subfamily) = candidate.metadata.font_subfamily {
let sf_lower = subfamily.to_lowercase();
if sf_lower == "regular" {
score -= 30;
}
}
score
}
}
#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
fn FcScanDirectories() -> Option<(Vec<(FcPattern, FcFontPath)>, BTreeMap<String, FcFontRenderConfig>)> {
use std::fs;
use std::path::Path;
const BASE_FONTCONFIG_PATH: &str = "/etc/fonts/fonts.conf";
if !Path::new(BASE_FONTCONFIG_PATH).exists() {
return None;
}
let mut font_paths = Vec::with_capacity(32);
let mut paths_to_visit = vec![(None, PathBuf::from(BASE_FONTCONFIG_PATH))];
let mut render_configs: BTreeMap<String, FcFontRenderConfig> = BTreeMap::new();
while let Some((prefix, path_to_visit)) = paths_to_visit.pop() {
let path = match process_path(&prefix, path_to_visit, true) {
Some(path) => path,
None => continue,
};
let metadata = match fs::metadata(&path) {
Ok(metadata) => metadata,
Err(_) => continue,
};
if metadata.is_file() {
let xml_utf8 = match fs::read_to_string(&path) {
Ok(xml_utf8) => xml_utf8,
Err(_) => continue,
};
if ParseFontsConf(&xml_utf8, &mut paths_to_visit, &mut font_paths).is_none() {
continue;
}
ParseFontsConfRenderConfig(&xml_utf8, &mut render_configs);
} else if metadata.is_dir() {
let dir_entries = match fs::read_dir(&path) {
Ok(dir_entries) => dir_entries,
Err(_) => continue,
};
for entry_result in dir_entries {
let entry = match entry_result {
Ok(entry) => entry,
Err(_) => continue,
};
let entry_path = entry.path();
let entry_metadata = match fs::metadata(&entry_path) {
Ok(metadata) => metadata,
Err(_) => continue,
};
if !entry_metadata.is_file() {
continue;
}
let file_name = match entry_path.file_name() {
Some(name) => name,
None => continue,
};
let file_name_str = file_name.to_string_lossy();
if file_name_str.starts_with(|c: char| c.is_ascii_digit())
&& file_name_str.ends_with(".conf")
{
paths_to_visit.push((None, entry_path));
}
}
}
}
if font_paths.is_empty() {
return None;
}
Some((FcScanDirectoriesInner(&font_paths), render_configs))
}
#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
fn ParseFontsConf(
input: &str,
paths_to_visit: &mut Vec<(Option<String>, PathBuf)>,
font_paths: &mut Vec<(Option<String>, String)>,
) -> Option<()> {
use xmlparser::Token::*;
use xmlparser::Tokenizer;
const TAG_INCLUDE: &str = "include";
const TAG_DIR: &str = "dir";
const ATTRIBUTE_PREFIX: &str = "prefix";
let mut current_prefix: Option<&str> = None;
let mut current_path: Option<&str> = None;
let mut is_in_include = false;
let mut is_in_dir = false;
for token_result in Tokenizer::from(input) {
let token = match token_result {
Ok(token) => token,
Err(_) => return None,
};
match token {
ElementStart { local, .. } => {
if is_in_include || is_in_dir {
return None;
}
match local.as_str() {
TAG_INCLUDE => {
is_in_include = true;
}
TAG_DIR => {
is_in_dir = true;
}
_ => continue,
}
current_path = None;
}
Text { text, .. } => {
let text = text.as_str().trim();
if text.is_empty() {
continue;
}
if is_in_include || is_in_dir {
current_path = Some(text);
}
}
Attribute { local, value, .. } => {
if !is_in_include && !is_in_dir {
continue;
}
if local.as_str() == ATTRIBUTE_PREFIX {
current_prefix = Some(value.as_str());
}
}
ElementEnd { end, .. } => {
let end_tag = match end {
xmlparser::ElementEnd::Close(_, a) => a,
_ => continue,
};
match end_tag.as_str() {
TAG_INCLUDE => {
if !is_in_include {
continue;
}
if let Some(current_path) = current_path.as_ref() {
paths_to_visit.push((
current_prefix.map(ToOwned::to_owned),
PathBuf::from(*current_path),
));
}
}
TAG_DIR => {
if !is_in_dir {
continue;
}
if let Some(current_path) = current_path.as_ref() {
font_paths.push((
current_prefix.map(ToOwned::to_owned),
(*current_path).to_owned(),
));
}
}
_ => continue,
}
is_in_include = false;
is_in_dir = false;
current_path = None;
current_prefix = None;
}
_ => {}
}
}
Some(())
}
#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
fn ParseFontsConfRenderConfig(
input: &str,
configs: &mut BTreeMap<String, FcFontRenderConfig>,
) {
use xmlparser::Token::*;
use xmlparser::Tokenizer;
#[derive(Clone, Copy, PartialEq)]
enum State {
Idle,
InMatchFont,
InTestFamily,
InEdit,
InValue,
}
let mut state = State::Idle;
let mut match_is_font_target = false;
let mut current_family: Option<String> = None;
let mut current_edit_name: Option<String> = None;
let mut current_value: Option<String> = None;
let mut value_tag: Option<String> = None;
let mut config = FcFontRenderConfig::default();
let mut in_test = false;
let mut test_name: Option<String> = None;
for token_result in Tokenizer::from(input) {
let token = match token_result {
Ok(token) => token,
Err(_) => continue,
};
match token {
ElementStart { local, .. } => {
let tag = local.as_str();
match tag {
"match" => {
match_is_font_target = false;
current_family = None;
config = FcFontRenderConfig::default();
}
"test" if state == State::InMatchFont => {
in_test = true;
test_name = None;
}
"edit" if state == State::InMatchFont => {
current_edit_name = None;
}
"bool" | "double" | "const" | "string" | "int" => {
if state == State::InTestFamily || state == State::InEdit {
value_tag = Some(tag.to_owned());
current_value = None;
}
}
_ => {}
}
}
Attribute { local, value, .. } => {
let attr_name = local.as_str();
let attr_value = value.as_str();
match attr_name {
"target" => {
if attr_value == "font" {
match_is_font_target = true;
}
}
"name" => {
if in_test && state == State::InMatchFont {
test_name = Some(attr_value.to_owned());
} else if state == State::InMatchFont {
current_edit_name = Some(attr_value.to_owned());
}
}
_ => {}
}
}
Text { text, .. } => {
let text = text.as_str().trim();
if !text.is_empty() && (state == State::InTestFamily || state == State::InEdit) {
current_value = Some(text.to_owned());
}
}
ElementEnd { end, .. } => {
match end {
xmlparser::ElementEnd::Open => {
if match_is_font_target && state == State::Idle {
state = State::InMatchFont;
match_is_font_target = false;
} else if in_test {
if test_name.as_deref() == Some("family") {
state = State::InTestFamily;
}
in_test = false;
} else if current_edit_name.is_some() && state == State::InMatchFont {
state = State::InEdit;
}
}
xmlparser::ElementEnd::Close(_, local) => {
let tag = local.as_str();
match tag {
"match" => {
if let Some(family) = current_family.take() {
let empty = FcFontRenderConfig::default();
if config != empty {
configs.insert(family, config.clone());
}
}
state = State::Idle;
config = FcFontRenderConfig::default();
}
"test" => {
if state == State::InTestFamily {
if let Some(ref val) = current_value {
current_family = Some(val.clone());
}
state = State::InMatchFont;
}
current_value = None;
value_tag = None;
}
"edit" => {
if state == State::InEdit {
if let (Some(ref name), Some(ref val)) = (¤t_edit_name, ¤t_value) {
apply_edit_value(&mut config, name, val, value_tag.as_deref());
}
state = State::InMatchFont;
}
current_edit_name = None;
current_value = None;
value_tag = None;
}
"bool" | "double" | "const" | "string" | "int" => {
}
_ => {}
}
}
xmlparser::ElementEnd::Empty => {
}
}
}
_ => {}
}
}
}
#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
fn apply_edit_value(
config: &mut FcFontRenderConfig,
edit_name: &str,
value: &str,
value_tag: Option<&str>,
) {
match edit_name {
"antialias" => {
config.antialias = parse_bool_value(value);
}
"hinting" => {
config.hinting = parse_bool_value(value);
}
"autohint" => {
config.autohint = parse_bool_value(value);
}
"embeddedbitmap" => {
config.embeddedbitmap = parse_bool_value(value);
}
"embolden" => {
config.embolden = parse_bool_value(value);
}
"minspace" => {
config.minspace = parse_bool_value(value);
}
"hintstyle" => {
config.hintstyle = parse_hintstyle_const(value);
}
"rgba" => {
config.rgba = parse_rgba_const(value);
}
"lcdfilter" => {
config.lcdfilter = parse_lcdfilter_const(value);
}
"dpi" => {
if let Ok(v) = value.parse::<f64>() {
config.dpi = Some(v);
}
}
"scale" => {
if let Ok(v) = value.parse::<f64>() {
config.scale = Some(v);
}
}
_ => {
}
}
}
#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
fn parse_bool_value(value: &str) -> Option<bool> {
match value {
"true" => Some(true),
"false" => Some(false),
_ => None,
}
}
#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
fn parse_hintstyle_const(value: &str) -> Option<FcHintStyle> {
match value {
"hintnone" => Some(FcHintStyle::None),
"hintslight" => Some(FcHintStyle::Slight),
"hintmedium" => Some(FcHintStyle::Medium),
"hintfull" => Some(FcHintStyle::Full),
_ => None,
}
}
#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
fn parse_rgba_const(value: &str) -> Option<FcRgba> {
match value {
"unknown" => Some(FcRgba::Unknown),
"rgb" => Some(FcRgba::Rgb),
"bgr" => Some(FcRgba::Bgr),
"vrgb" => Some(FcRgba::Vrgb),
"vbgr" => Some(FcRgba::Vbgr),
"none" => Some(FcRgba::None),
_ => None,
}
}
#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
fn parse_lcdfilter_const(value: &str) -> Option<FcLcdFilter> {
match value {
"lcdnone" => Some(FcLcdFilter::None),
"lcddefault" => Some(FcLcdFilter::Default),
"lcdlight" => Some(FcLcdFilter::Light),
"lcdlegacy" => Some(FcLcdFilter::Legacy),
_ => None,
}
}
#[cfg(all(feature = "std", feature = "parsing"))]
const UNICODE_RANGE_MAPPINGS: &[(usize, u32, u32)] = &[
(0, 0x0000, 0x007F), (1, 0x0080, 0x00FF), (2, 0x0100, 0x017F), (3, 0x0180, 0x024F), (4, 0x0250, 0x02AF), (5, 0x02B0, 0x02FF), (6, 0x0300, 0x036F), (7, 0x0370, 0x03FF), (8, 0x2C80, 0x2CFF), (9, 0x0400, 0x04FF), (10, 0x0530, 0x058F), (11, 0x0590, 0x05FF), (12, 0x0600, 0x06FF), (13, 0x0700, 0x074F), (14, 0x0780, 0x07BF), (15, 0x0900, 0x097F), (16, 0x0980, 0x09FF), (17, 0x0A00, 0x0A7F), (18, 0x0A80, 0x0AFF), (19, 0x0B00, 0x0B7F), (20, 0x0B80, 0x0BFF), (21, 0x0C00, 0x0C7F), (22, 0x0C80, 0x0CFF), (23, 0x0D00, 0x0D7F), (24, 0x0E00, 0x0E7F), (25, 0x0E80, 0x0EFF), (26, 0x10A0, 0x10FF), (27, 0x1B00, 0x1B7F), (28, 0x1100, 0x11FF), (29, 0x1E00, 0x1EFF), (30, 0x1F00, 0x1FFF), (31, 0x2000, 0x206F), (32, 0x2070, 0x209F), (33, 0x20A0, 0x20CF), (34, 0x20D0, 0x20FF), (35, 0x2100, 0x214F), (36, 0x2150, 0x218F), (37, 0x2190, 0x21FF), (38, 0x2200, 0x22FF), (39, 0x2300, 0x23FF), (40, 0x2400, 0x243F), (41, 0x2440, 0x245F), (42, 0x2460, 0x24FF), (43, 0x2500, 0x257F), (44, 0x2580, 0x259F), (45, 0x25A0, 0x25FF), (46, 0x2600, 0x26FF), (47, 0x2700, 0x27BF), (48, 0x3000, 0x303F), (49, 0x3040, 0x309F), (50, 0x30A0, 0x30FF), (51, 0x3100, 0x312F), (52, 0x3130, 0x318F), (53, 0x3190, 0x319F), (54, 0x31A0, 0x31BF), (55, 0x31C0, 0x31EF), (56, 0x31F0, 0x31FF), (57, 0x3200, 0x32FF), (58, 0x3300, 0x33FF), (59, 0x4E00, 0x9FFF), (60, 0xA000, 0xA48F), (61, 0xA490, 0xA4CF), (62, 0xAC00, 0xD7AF), (63, 0xD800, 0xDFFF), (64, 0x10000, 0x10FFFF), (65, 0xF900, 0xFAFF), (66, 0xFB00, 0xFB4F), (67, 0xFB50, 0xFDFF), (68, 0xFE00, 0xFE0F), (69, 0xFE10, 0xFE1F), (70, 0xFE20, 0xFE2F), (71, 0xFE30, 0xFE4F), (72, 0xFE50, 0xFE6F), (73, 0xFE70, 0xFEFF), (74, 0xFF00, 0xFFEF), (75, 0xFFF0, 0xFFFF), (76, 0x0F00, 0x0FFF), (77, 0x0700, 0x074F), (78, 0x0780, 0x07BF), (79, 0x0D80, 0x0DFF), (80, 0x1000, 0x109F), (81, 0x1200, 0x137F), (82, 0x13A0, 0x13FF), (83, 0x1400, 0x167F), (84, 0x1680, 0x169F), (85, 0x16A0, 0x16FF), (86, 0x1780, 0x17FF), (87, 0x1800, 0x18AF), (88, 0x2800, 0x28FF), (89, 0xA000, 0xA48F), (90, 0x1680, 0x169F), (91, 0x16A0, 0x16FF), (92, 0x1700, 0x171F), (93, 0x1720, 0x173F), (94, 0x1740, 0x175F), (95, 0x1760, 0x177F), (96, 0x1900, 0x194F), (97, 0x1950, 0x197F), (98, 0x1980, 0x19DF), (99, 0x1A00, 0x1A1F), (100, 0x2C00, 0x2C5F), (101, 0x2D30, 0x2D7F), (102, 0x4DC0, 0x4DFF), (103, 0xA800, 0xA82F), (104, 0x10000, 0x1007F), (105, 0x10080, 0x100FF), (106, 0x10100, 0x1013F), (107, 0x10140, 0x1018F), (108, 0x10300, 0x1032F), (109, 0x10330, 0x1034F), (110, 0x10380, 0x1039F), (111, 0x103A0, 0x103DF), (112, 0x10400, 0x1044F), (113, 0x10450, 0x1047F), (114, 0x10480, 0x104AF), (115, 0x10800, 0x1083F), (116, 0x10A00, 0x10A5F), (117, 0x1D000, 0x1D0FF), (118, 0x1D100, 0x1D1FF), (119, 0x1D200, 0x1D24F), (120, 0x1D300, 0x1D35F), (121, 0x1D400, 0x1D7FF), (122, 0x1F000, 0x1F02F), (123, 0x1F030, 0x1F09F), (124, 0x1F300, 0x1F9FF), (125, 0x1F680, 0x1F6FF), (126, 0x1F700, 0x1F77F), (127, 0x1F900, 0x1F9FF), ];
#[cfg(all(feature = "std", feature = "parsing"))]
struct ParsedFontFace {
pattern: FcPattern,
font_index: usize,
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn parse_font_faces(font_bytes: &[u8]) -> Option<Vec<ParsedFontFace>> {
use allsorts::{
binary::read::ReadScope,
font_data::FontData,
get_name::fontcode_get_name,
post::PostTable,
tables::{
os2::Os2, HeadTable, NameTable,
},
tag,
};
use std::collections::BTreeSet;
const FONT_SPECIFIER_NAME_ID: u16 = 4;
const FONT_SPECIFIER_FAMILY_ID: u16 = 1;
let max_fonts = if font_bytes.len() >= 12 && &font_bytes[0..4] == b"ttcf" {
let num_fonts =
u32::from_be_bytes([font_bytes[8], font_bytes[9], font_bytes[10], font_bytes[11]]);
std::cmp::min(num_fonts as usize, 100)
} else {
1
};
let scope = ReadScope::new(font_bytes);
let font_file = scope.read::<FontData<'_>>().ok()?;
let mut results = Vec::new();
for font_index in 0..max_fonts {
let provider = font_file.table_provider(font_index).ok()?;
let head_data = provider.table_data(tag::HEAD).ok()??.into_owned();
let head_table = ReadScope::new(&head_data).read::<HeadTable>().ok()?;
let is_bold = head_table.is_bold();
let is_italic = head_table.is_italic();
let mut detected_monospace = None;
let post_data = provider.table_data(tag::POST).ok()??;
if let Ok(post_table) = ReadScope::new(&post_data).read::<PostTable>() {
detected_monospace = Some(post_table.header.is_fixed_pitch != 0);
}
let os2_data = provider.table_data(tag::OS_2).ok()??;
let os2_table = ReadScope::new(&os2_data)
.read_dep::<Os2>(os2_data.len())
.ok()?;
let is_oblique = os2_table
.fs_selection
.contains(allsorts::tables::os2::FsSelection::OBLIQUE);
let weight = FcWeight::from_u16(os2_table.us_weight_class);
let stretch = FcStretch::from_u16(os2_table.us_width_class);
let mut unicode_ranges = Vec::new();
let os2_ranges = [
os2_table.ul_unicode_range1,
os2_table.ul_unicode_range2,
os2_table.ul_unicode_range3,
os2_table.ul_unicode_range4,
];
for &(bit, start, end) in UNICODE_RANGE_MAPPINGS {
let range_idx = bit / 32;
let bit_pos = bit % 32;
if range_idx < 4 && (os2_ranges[range_idx] & (1 << bit_pos)) != 0 {
unicode_ranges.push(UnicodeRange { start, end });
}
}
unicode_ranges = verify_unicode_ranges_with_cmap(&provider, unicode_ranges);
if unicode_ranges.is_empty() {
if let Some(cmap_ranges) = analyze_cmap_coverage(&provider) {
unicode_ranges = cmap_ranges;
}
}
let is_monospace = detect_monospace(&provider, &os2_table, detected_monospace)
.unwrap_or(false);
let name_data = provider.table_data(tag::NAME).ok()??.into_owned();
let name_table = ReadScope::new(&name_data).read::<NameTable>().ok()?;
let mut metadata = FcFontMetadata::default();
const NAME_ID_COPYRIGHT: u16 = 0;
const NAME_ID_FAMILY: u16 = 1;
const NAME_ID_SUBFAMILY: u16 = 2;
const NAME_ID_UNIQUE_ID: u16 = 3;
const NAME_ID_FULL_NAME: u16 = 4;
const NAME_ID_VERSION: u16 = 5;
const NAME_ID_POSTSCRIPT_NAME: u16 = 6;
const NAME_ID_TRADEMARK: u16 = 7;
const NAME_ID_MANUFACTURER: u16 = 8;
const NAME_ID_DESIGNER: u16 = 9;
const NAME_ID_DESCRIPTION: u16 = 10;
const NAME_ID_VENDOR_URL: u16 = 11;
const NAME_ID_DESIGNER_URL: u16 = 12;
const NAME_ID_LICENSE: u16 = 13;
const NAME_ID_LICENSE_URL: u16 = 14;
const NAME_ID_PREFERRED_FAMILY: u16 = 16;
const NAME_ID_PREFERRED_SUBFAMILY: u16 = 17;
metadata.copyright = get_name_string(&name_data, NAME_ID_COPYRIGHT);
metadata.font_family = get_name_string(&name_data, NAME_ID_FAMILY);
metadata.font_subfamily = get_name_string(&name_data, NAME_ID_SUBFAMILY);
metadata.full_name = get_name_string(&name_data, NAME_ID_FULL_NAME);
metadata.unique_id = get_name_string(&name_data, NAME_ID_UNIQUE_ID);
metadata.version = get_name_string(&name_data, NAME_ID_VERSION);
metadata.postscript_name = get_name_string(&name_data, NAME_ID_POSTSCRIPT_NAME);
metadata.trademark = get_name_string(&name_data, NAME_ID_TRADEMARK);
metadata.manufacturer = get_name_string(&name_data, NAME_ID_MANUFACTURER);
metadata.designer = get_name_string(&name_data, NAME_ID_DESIGNER);
metadata.id_description = get_name_string(&name_data, NAME_ID_DESCRIPTION);
metadata.designer_url = get_name_string(&name_data, NAME_ID_DESIGNER_URL);
metadata.manufacturer_url = get_name_string(&name_data, NAME_ID_VENDOR_URL);
metadata.license = get_name_string(&name_data, NAME_ID_LICENSE);
metadata.license_url = get_name_string(&name_data, NAME_ID_LICENSE_URL);
metadata.preferred_family = get_name_string(&name_data, NAME_ID_PREFERRED_FAMILY);
metadata.preferred_subfamily = get_name_string(&name_data, NAME_ID_PREFERRED_SUBFAMILY);
let mut f_family = None;
let patterns = name_table
.name_records
.iter()
.filter_map(|name_record| {
let name_id = name_record.name_id;
if name_id == FONT_SPECIFIER_FAMILY_ID {
if let Ok(Some(family)) =
fontcode_get_name(&name_data, FONT_SPECIFIER_FAMILY_ID)
{
f_family = Some(family);
}
None
} else if name_id == FONT_SPECIFIER_NAME_ID {
let family = f_family.as_ref()?;
let name = fontcode_get_name(&name_data, FONT_SPECIFIER_NAME_ID).ok()??;
if name.to_bytes().is_empty() {
None
} else {
let mut name_str =
String::from_utf8_lossy(name.to_bytes()).to_string();
let mut family_str =
String::from_utf8_lossy(family.as_bytes()).to_string();
if name_str.starts_with('.') {
name_str = name_str[1..].to_string();
}
if family_str.starts_with('.') {
family_str = family_str[1..].to_string();
}
Some((
FcPattern {
name: Some(name_str),
family: Some(family_str),
bold: if is_bold {
PatternMatch::True
} else {
PatternMatch::False
},
italic: if is_italic {
PatternMatch::True
} else {
PatternMatch::False
},
oblique: if is_oblique {
PatternMatch::True
} else {
PatternMatch::False
},
monospace: if is_monospace {
PatternMatch::True
} else {
PatternMatch::False
},
condensed: if stretch <= FcStretch::Condensed {
PatternMatch::True
} else {
PatternMatch::False
},
weight,
stretch,
unicode_ranges: unicode_ranges.clone(),
metadata: metadata.clone(),
render_config: FcFontRenderConfig::default(),
},
font_index,
))
}
} else {
None
}
})
.collect::<BTreeSet<_>>();
results.extend(patterns.into_iter().map(|(pat, idx)| ParsedFontFace {
pattern: pat,
font_index: idx,
}));
}
if results.is_empty() {
None
} else {
Some(results)
}
}
#[cfg(all(feature = "std", feature = "parsing"))]
pub(crate) fn FcParseFont(filepath: &PathBuf) -> Option<Vec<(FcPattern, FcFontPath)>> {
#[cfg(all(not(target_family = "wasm"), feature = "std"))]
use mmapio::MmapOptions;
use std::fs::File;
let file = File::open(filepath).ok()?;
#[cfg(all(not(target_family = "wasm"), feature = "std"))]
let font_bytes = unsafe { MmapOptions::new().map(&file).ok()? };
#[cfg(not(all(not(target_family = "wasm"), feature = "std")))]
let font_bytes = std::fs::read(filepath).ok()?;
let faces = parse_font_faces(&font_bytes[..])?;
let path_str = filepath.to_string_lossy().to_string();
Some(
faces
.into_iter()
.map(|face| {
(
face.pattern,
FcFontPath {
path: path_str.clone(),
font_index: face.font_index,
},
)
})
.collect(),
)
}
#[cfg(all(feature = "std", feature = "parsing"))]
#[allow(non_snake_case)]
pub fn FcParseFontBytes(font_bytes: &[u8], font_id: &str) -> Option<Vec<(FcPattern, FcFont)>> {
FcParseFontBytesInner(font_bytes, font_id)
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn FcParseFontBytesInner(font_bytes: &[u8], font_id: &str) -> Option<Vec<(FcPattern, FcFont)>> {
let faces = parse_font_faces(font_bytes)?;
let id = font_id.to_string();
let bytes = font_bytes.to_vec();
Some(
faces
.into_iter()
.map(|face| {
(
face.pattern,
FcFont {
bytes: bytes.clone(),
font_index: face.font_index,
id: id.clone(),
},
)
})
.collect(),
)
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn FcScanDirectoriesInner(paths: &[(Option<String>, String)]) -> Vec<(FcPattern, FcFontPath)> {
#[cfg(feature = "multithreading")]
{
use rayon::prelude::*;
paths
.par_iter()
.filter_map(|(prefix, p)| {
process_path(prefix, PathBuf::from(p), false).map(FcScanSingleDirectoryRecursive)
})
.flatten()
.collect()
}
#[cfg(not(feature = "multithreading"))]
{
paths
.iter()
.filter_map(|(prefix, p)| {
process_path(prefix, PathBuf::from(p), false).map(FcScanSingleDirectoryRecursive)
})
.flatten()
.collect()
}
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn FcScanSingleDirectoryRecursive(dir: PathBuf) -> Vec<(FcPattern, FcFontPath)> {
let mut files_to_parse = Vec::new();
let mut dirs_to_parse = vec![dir];
'outer: loop {
let mut new_dirs_to_parse = Vec::new();
'inner: for dir in dirs_to_parse.clone() {
let dir = match std::fs::read_dir(dir) {
Ok(o) => o,
Err(_) => continue 'inner,
};
for (path, pathbuf) in dir.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
let pathbuf = path.to_path_buf();
Some((path, pathbuf))
}) {
if path.is_dir() {
new_dirs_to_parse.push(pathbuf);
} else {
files_to_parse.push(pathbuf);
}
}
}
if new_dirs_to_parse.is_empty() {
break 'outer;
} else {
dirs_to_parse = new_dirs_to_parse;
}
}
FcParseFontFiles(&files_to_parse)
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn FcParseFontFiles(files_to_parse: &[PathBuf]) -> Vec<(FcPattern, FcFontPath)> {
let result = {
#[cfg(feature = "multithreading")]
{
use rayon::prelude::*;
files_to_parse
.par_iter()
.filter_map(|file| FcParseFont(file))
.collect::<Vec<Vec<_>>>()
}
#[cfg(not(feature = "multithreading"))]
{
files_to_parse
.iter()
.filter_map(|file| FcParseFont(file))
.collect::<Vec<Vec<_>>>()
}
};
result.into_iter().flat_map(|f| f.into_iter()).collect()
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn process_path(
prefix: &Option<String>,
mut path: PathBuf,
is_include_path: bool,
) -> Option<PathBuf> {
use std::env::var;
const HOME_SHORTCUT: &str = "~";
const CWD_PATH: &str = ".";
const HOME_ENV_VAR: &str = "HOME";
const XDG_CONFIG_HOME_ENV_VAR: &str = "XDG_CONFIG_HOME";
const XDG_CONFIG_HOME_DEFAULT_PATH_SUFFIX: &str = ".config";
const XDG_DATA_HOME_ENV_VAR: &str = "XDG_DATA_HOME";
const XDG_DATA_HOME_DEFAULT_PATH_SUFFIX: &str = ".local/share";
const PREFIX_CWD: &str = "cwd";
const PREFIX_DEFAULT: &str = "default";
const PREFIX_XDG: &str = "xdg";
fn get_home_value() -> Option<PathBuf> {
var(HOME_ENV_VAR).ok().map(PathBuf::from)
}
fn get_xdg_config_home_value() -> Option<PathBuf> {
var(XDG_CONFIG_HOME_ENV_VAR)
.ok()
.map(PathBuf::from)
.or_else(|| {
get_home_value()
.map(|home_path| home_path.join(XDG_CONFIG_HOME_DEFAULT_PATH_SUFFIX))
})
}
fn get_xdg_data_home_value() -> Option<PathBuf> {
var(XDG_DATA_HOME_ENV_VAR)
.ok()
.map(PathBuf::from)
.or_else(|| {
get_home_value().map(|home_path| home_path.join(XDG_DATA_HOME_DEFAULT_PATH_SUFFIX))
})
}
if path.starts_with(HOME_SHORTCUT) {
if let Some(home_path) = get_home_value() {
path = home_path.join(
path.strip_prefix(HOME_SHORTCUT)
.expect("already checked that it starts with the prefix"),
);
} else {
return None;
}
}
match prefix {
Some(prefix) => match prefix.as_str() {
PREFIX_CWD | PREFIX_DEFAULT => {
let mut new_path = PathBuf::from(CWD_PATH);
new_path.push(path);
Some(new_path)
}
PREFIX_XDG => {
if is_include_path {
get_xdg_config_home_value()
.map(|xdg_config_home_path| xdg_config_home_path.join(path))
} else {
get_xdg_data_home_value()
.map(|xdg_data_home_path| xdg_data_home_path.join(path))
}
}
_ => None, },
None => Some(path),
}
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn get_name_string(name_data: &[u8], name_id: u16) -> Option<String> {
fontcode_get_name(name_data, name_id)
.ok()
.flatten()
.map(|name| String::from_utf8_lossy(name.to_bytes()).to_string())
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn get_verification_codepoints(start: u32, end: u32) -> Vec<u32> {
match start {
0x0000 => vec!['A' as u32, 'M' as u32, 'Z' as u32, 'a' as u32, 'm' as u32, 'z' as u32],
0x0080 => vec![0x00C0, 0x00C9, 0x00D1, 0x00E0, 0x00E9, 0x00F1], 0x0100 => vec![0x0100, 0x0110, 0x0141, 0x0152, 0x0160], 0x0180 => vec![0x0180, 0x01A0, 0x01B0, 0x01CD], 0x0250 => vec![0x0250, 0x0259, 0x026A, 0x0279], 0x0370 => vec![0x0391, 0x0392, 0x0393, 0x03B1, 0x03B2, 0x03C9], 0x0400 => vec![0x0410, 0x0411, 0x0412, 0x0430, 0x0431, 0x042F], 0x0530 => vec![0x0531, 0x0532, 0x0533, 0x0561, 0x0562], 0x0590 => vec![0x05D0, 0x05D1, 0x05D2, 0x05E9, 0x05EA], 0x0600 => vec![0x0627, 0x0628, 0x062A, 0x062C, 0x0645], 0x0700 => vec![0x0710, 0x0712, 0x0713, 0x0715], 0x0900 => vec![0x0905, 0x0906, 0x0915, 0x0916, 0x0939], 0x0980 => vec![0x0985, 0x0986, 0x0995, 0x0996], 0x0A00 => vec![0x0A05, 0x0A06, 0x0A15, 0x0A16], 0x0A80 => vec![0x0A85, 0x0A86, 0x0A95, 0x0A96], 0x0B00 => vec![0x0B05, 0x0B06, 0x0B15, 0x0B16], 0x0B80 => vec![0x0B85, 0x0B86, 0x0B95, 0x0BA4], 0x0C00 => vec![0x0C05, 0x0C06, 0x0C15, 0x0C16], 0x0C80 => vec![0x0C85, 0x0C86, 0x0C95, 0x0C96], 0x0D00 => vec![0x0D05, 0x0D06, 0x0D15, 0x0D16], 0x0E00 => vec![0x0E01, 0x0E02, 0x0E04, 0x0E07, 0x0E40], 0x0E80 => vec![0x0E81, 0x0E82, 0x0E84, 0x0E87], 0x1000 => vec![0x1000, 0x1001, 0x1002, 0x1010, 0x1019], 0x10A0 => vec![0x10D0, 0x10D1, 0x10D2, 0x10D3], 0x1100 => vec![0x1100, 0x1102, 0x1103, 0x1161, 0x1162], 0x1200 => vec![0x1200, 0x1208, 0x1210, 0x1218], 0x13A0 => vec![0x13A0, 0x13A1, 0x13A2, 0x13A3], 0x1780 => vec![0x1780, 0x1781, 0x1782, 0x1783], 0x1800 => vec![0x1820, 0x1821, 0x1822, 0x1823], 0x3040 => vec![0x3042, 0x3044, 0x3046, 0x304B, 0x304D, 0x3093], 0x30A0 => vec![0x30A2, 0x30A4, 0x30A6, 0x30AB, 0x30AD, 0x30F3], 0x3100 => vec![0x3105, 0x3106, 0x3107, 0x3108], 0x4E00 => vec![0x4E00, 0x4E2D, 0x4EBA, 0x5927, 0x65E5, 0x6708], 0xAC00 => vec![0xAC00, 0xAC01, 0xAC04, 0xB098, 0xB2E4], 0xF900 => vec![0xF900, 0xF901, 0xF902], 0xFB50 => vec![0xFB50, 0xFB51, 0xFB52, 0xFB56], 0xFE70 => vec![0xFE70, 0xFE72, 0xFE74, 0xFE76], 0xFF00 => vec![0xFF01, 0xFF21, 0xFF41, 0xFF61], _ => {
let range_size = end - start;
if range_size > 20 {
vec![
start + range_size / 5,
start + 2 * range_size / 5,
start + 3 * range_size / 5,
start + 4 * range_size / 5,
]
} else {
vec![start, start + range_size / 2]
}
}
}
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn find_best_cmap_subtable<'a>(
cmap: &allsorts::tables::cmap::Cmap<'a>,
) -> Option<allsorts::tables::cmap::EncodingRecord> {
use allsorts::tables::cmap::{PlatformId, EncodingId};
cmap.find_subtable(PlatformId::UNICODE, EncodingId(3))
.or_else(|| cmap.find_subtable(PlatformId::UNICODE, EncodingId(4)))
.or_else(|| cmap.find_subtable(PlatformId::WINDOWS, EncodingId(1)))
.or_else(|| cmap.find_subtable(PlatformId::WINDOWS, EncodingId(10)))
.or_else(|| cmap.find_subtable(PlatformId::UNICODE, EncodingId(0)))
.or_else(|| cmap.find_subtable(PlatformId::UNICODE, EncodingId(1)))
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn verify_unicode_ranges_with_cmap(
provider: &impl FontTableProvider,
os2_ranges: Vec<UnicodeRange>
) -> Vec<UnicodeRange> {
use allsorts::tables::cmap::{Cmap, CmapSubtable};
if os2_ranges.is_empty() {
return Vec::new();
}
let cmap_data = match provider.table_data(tag::CMAP) {
Ok(Some(data)) => data,
_ => return os2_ranges, };
let cmap = match ReadScope::new(&cmap_data).read::<Cmap<'_>>() {
Ok(c) => c,
Err(_) => return os2_ranges,
};
let encoding_record = match find_best_cmap_subtable(&cmap) {
Some(r) => r,
None => return os2_ranges, };
let cmap_subtable = match ReadScope::new(&cmap_data)
.offset(encoding_record.offset as usize)
.read::<CmapSubtable<'_>>()
{
Ok(st) => st,
Err(_) => return os2_ranges,
};
let mut verified_ranges = Vec::new();
for range in os2_ranges {
let test_codepoints = get_verification_codepoints(range.start, range.end);
let required_hits = (test_codepoints.len() + 1) / 2; let mut hits = 0;
for cp in test_codepoints {
if cp >= range.start && cp <= range.end {
if let Ok(Some(gid)) = cmap_subtable.map_glyph(cp) {
if gid != 0 {
hits += 1;
if hits >= required_hits {
break;
}
}
}
}
}
if hits >= required_hits {
verified_ranges.push(range);
}
}
verified_ranges
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn analyze_cmap_coverage(provider: &impl FontTableProvider) -> Option<Vec<UnicodeRange>> {
use allsorts::tables::cmap::{Cmap, CmapSubtable};
let cmap_data = provider.table_data(tag::CMAP).ok()??;
let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().ok()?;
let encoding_record = find_best_cmap_subtable(&cmap)?;
let cmap_subtable = ReadScope::new(&cmap_data)
.offset(encoding_record.offset as usize)
.read::<CmapSubtable<'_>>()
.ok()?;
let blocks_to_check: &[(u32, u32)] = &[
(0x0000, 0x007F), (0x0080, 0x00FF), (0x0100, 0x017F), (0x0180, 0x024F), (0x0250, 0x02AF), (0x0300, 0x036F), (0x0370, 0x03FF), (0x0400, 0x04FF), (0x0500, 0x052F), (0x0530, 0x058F), (0x0590, 0x05FF), (0x0600, 0x06FF), (0x0700, 0x074F), (0x0900, 0x097F), (0x0980, 0x09FF), (0x0A00, 0x0A7F), (0x0A80, 0x0AFF), (0x0B00, 0x0B7F), (0x0B80, 0x0BFF), (0x0C00, 0x0C7F), (0x0C80, 0x0CFF), (0x0D00, 0x0D7F), (0x0E00, 0x0E7F), (0x0E80, 0x0EFF), (0x1000, 0x109F), (0x10A0, 0x10FF), (0x1100, 0x11FF), (0x1200, 0x137F), (0x13A0, 0x13FF), (0x1780, 0x17FF), (0x1800, 0x18AF), (0x2000, 0x206F), (0x20A0, 0x20CF), (0x2100, 0x214F), (0x2190, 0x21FF), (0x2200, 0x22FF), (0x2500, 0x257F), (0x25A0, 0x25FF), (0x2600, 0x26FF), (0x3000, 0x303F), (0x3040, 0x309F), (0x30A0, 0x30FF), (0x3100, 0x312F), (0x3130, 0x318F), (0x4E00, 0x9FFF), (0xAC00, 0xD7AF), (0xF900, 0xFAFF), (0xFB50, 0xFDFF), (0xFE70, 0xFEFF), (0xFF00, 0xFFEF), ];
let mut ranges = Vec::new();
for &(start, end) in blocks_to_check {
let test_codepoints = get_verification_codepoints(start, end);
let required_hits = (test_codepoints.len() + 1) / 2;
let mut hits = 0;
for cp in test_codepoints {
if let Ok(Some(gid)) = cmap_subtable.map_glyph(cp) {
if gid != 0 {
hits += 1;
if hits >= required_hits {
break;
}
}
}
}
if hits >= required_hits {
ranges.push(UnicodeRange { start, end });
}
}
if ranges.is_empty() {
None
} else {
Some(ranges)
}
}
#[cfg(all(feature = "std", feature = "parsing"))]
#[allow(dead_code)]
fn extract_unicode_ranges(os2_table: &Os2) -> Vec<UnicodeRange> {
let mut unicode_ranges = Vec::new();
let ranges = [
os2_table.ul_unicode_range1,
os2_table.ul_unicode_range2,
os2_table.ul_unicode_range3,
os2_table.ul_unicode_range4,
];
for &(bit, start, end) in UNICODE_RANGE_MAPPINGS {
let range_idx = bit / 32;
let bit_pos = bit % 32;
if range_idx < 4 && (ranges[range_idx] & (1 << bit_pos)) != 0 {
unicode_ranges.push(UnicodeRange { start, end });
}
}
unicode_ranges
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn detect_monospace(
provider: &impl FontTableProvider,
os2_table: &Os2,
detected_monospace: Option<bool>,
) -> Option<bool> {
if let Some(is_monospace) = detected_monospace {
return Some(is_monospace);
}
if os2_table.panose[0] == 2 {
return Some(os2_table.panose[3] == 9); }
let hhea_data = provider.table_data(tag::HHEA).ok()??;
let hhea_table = ReadScope::new(&hhea_data).read::<HheaTable>().ok()?;
let maxp_data = provider.table_data(tag::MAXP).ok()??;
let maxp_table = ReadScope::new(&maxp_data).read::<MaxpTable>().ok()?;
let hmtx_data = provider.table_data(tag::HMTX).ok()??;
let hmtx_table = ReadScope::new(&hmtx_data)
.read_dep::<HmtxTable<'_>>((
usize::from(maxp_table.num_glyphs),
usize::from(hhea_table.num_h_metrics),
))
.ok()?;
let mut monospace = true;
let mut last_advance = 0;
for i in 0..hhea_table.num_h_metrics as usize {
let advance = hmtx_table.h_metrics.read_item(i).ok()?.advance_width;
if i > 0 && advance != last_advance {
monospace = false;
break;
}
last_advance = advance;
}
Some(monospace)
}