// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Minimal font system wrapper for the MoonBit cosmic-text port.
///
/// Upstream `cosmic-text` uses `fontdb` for font discovery. In this MoonBit port
/// we start with an in-memory font list backed by `moon_swash`.
pub struct FontEntry {
id : Int
family_name : String
postscript_name : String
source_data : Bytes
source_index : Int
font : @moon_swash.FontRef
charmap_proxy : @moon_swash.CharmapProxy
attributes : @moon_swash.Attributes
is_monospace : Bool
not_emoji : Bool
}
///|
pub fn FontEntry::id(self : FontEntry) -> Int {
self.id
}
///|
pub fn FontEntry::family_name(self : FontEntry) -> String {
self.family_name
}
///|
pub fn FontEntry::postscript_name(self : FontEntry) -> String {
self.postscript_name
}
///|
pub fn FontEntry::source_data(self : FontEntry) -> Bytes {
self.source_data
}
///|
pub fn FontEntry::source_index(self : FontEntry) -> Int {
self.source_index
}
///|
pub fn FontEntry::font(self : FontEntry) -> @moon_swash.FontRef {
self.font
}
///|
pub fn FontEntry::charmap_proxy(self : FontEntry) -> @moon_swash.CharmapProxy {
self.charmap_proxy
}
///|
pub fn FontEntry::attributes(self : FontEntry) -> @moon_swash.Attributes {
self.attributes
}
///|
pub fn FontEntry::is_monospace(self : FontEntry) -> Bool {
self.is_monospace
}
///|
pub fn FontEntry::not_emoji(self : FontEntry) -> Bool {
self.not_emoji
}
///|
pub struct FontSystem {
locale : String
fonts : Array[FontEntry]
priv mut fallback_profile_opt : FallbackProfile?
font_matches_cache : @hashmap.HashMap[FontMatchAttrs, Array[Int]]
codepoint_support_cache : @hashmap.HashMap[
Int,
FontCachedCodepointSupportInfo,
]
shape_run_cache : ShapeRunCache
fn new() -> FontSystem
}
///|
pub(all) enum FallbackProfile {
Unix
MacOS
Windows
Other
}
///|
pub impl Eq for FallbackProfile with equal(self, other) {
match (self, other) {
(Unix, Unix) => true
(MacOS, MacOS) => true
(Windows, Windows) => true
(Other, Other) => true
_ => false
}
}
///|
const FONT_MATCHES_CACHE_SIZE_LIMIT : Int = 256
///|
const CODEPOINT_SUPPORTED_PER_FONT_LIMIT : Int = 512
///|
const CODEPOINT_NOT_SUPPORTED_PER_FONT_LIMIT : Int = 1024
///|
const WEIGHT_AXIS_TAG : UInt = 0x77676874U // "wght"
///|
pub fn FontSystem::new() -> FontSystem {
FontSystem::new_with_locale("en-US")
}
///|
pub fn FontSystem::new_with_locale(locale : String) -> FontSystem {
FontSystem::{
locale,
fonts: [],
fallback_profile_opt: None,
font_matches_cache: @hashmap.HashMap::default(),
codepoint_support_cache: @hashmap.HashMap::default(),
shape_run_cache: ShapeRunCache::new(),
}
}
///|
pub fn FontSystem::locale(self : FontSystem) -> String {
self.locale
}
///|
pub fn FontSystem::fonts(self : FontSystem) -> Array[FontEntry] {
self.fonts
}
///|
fn font_family_name(font : @moon_swash.FontRef, locale : String) -> String {
let strings = font.localized_strings()
match strings.find_by_id(@moon_swash.StringId::Family, Some(locale)) {
Some(s) => s.to_string()
None =>
match strings.find_by_id(@moon_swash.StringId::Family, None) {
Some(s) => s.to_string()
None => ""
}
}
}
///|
fn font_postscript_name(font : @moon_swash.FontRef) -> String {
let strings = font.localized_strings()
match strings.find_by_id(@moon_swash.StringId::PostScript, None) {
Some(s) => s.to_string()
None => ""
}
}
///|
fn string_contains_ascii(haystack : String, needle : String) -> Bool {
let hlen = haystack.length()
let nlen = needle.length()
if nlen == 0 {
return true
}
if nlen > hlen {
return false
}
for i in 0..<(hlen - nlen + 1) {
let mut ok = true
for j in 0.. Bool {
match id {
@moon_swash.StringId::Family
| @moon_swash.StringId::TypographicFamily
| @moon_swash.StringId::WwsFamily => true
_ => false
}
}
///|
fn entry_has_family_name(entry : FontEntry, family_name : String) -> Bool {
if entry.family_name == family_name {
return true
}
let strings = entry.font.localized_strings()
for s in strings.iter() {
if is_family_name_id(s.id()) && s.to_string() == family_name {
return true
}
}
false
}
///|
fn entry_in_forbidden_fallback(
entry : FontEntry,
forbidden_families : Array[String],
) -> Bool {
for family_name in forbidden_families {
if entry_has_family_name(entry, family_name) {
return true
}
}
false
}
///|
/// Load font data (TTF/OTF/TTC bytes) into the system.
///
/// If the bytes do not parse as a font/collection, this is a no-op.
pub fn FontSystem::load_font_data(
self : FontSystem,
data : Bytes,
) -> FontSystem {
match @moon_swash.FontDataRef::new(data) {
None => self
Some(data_ref) => {
let locale = self.locale
let fonts = self.fonts
let mut source_index = 0
for f in data_ref.fonts() {
let id = fonts.length()
let family_name = font_family_name(f, locale)
let postscript_name = font_postscript_name(f)
let not_emoji = !string_contains_ascii(postscript_name, "Emoji")
let metrics = f.metrics([])
fonts.push(FontEntry::{
id,
family_name,
postscript_name,
source_data: data,
source_index,
font: f,
charmap_proxy: @moon_swash.CharmapProxy::from_font(f),
attributes: f.attributes(),
is_monospace: metrics.is_monospace && not_emoji,
not_emoji,
})
source_index = source_index + 1
}
// New fonts change candidate ordering; clear match cache.
self.font_matches_cache.clear()
self.shape_run_cache.clear()
self.fallback_profile_opt = None
FontSystem::{ ..self, fonts, }
}
}
}
///|
/// Find a font ID that matches the family selector (best-effort).
pub fn FontSystem::select_family(self : FontSystem, family : Family) -> Int? {
select_family_with_weight(self, family, Weight::normal())
}
///|
fn abs_int(v : Int) -> Int {
if v < 0 {
0 - v
} else {
v
}
}
///|
struct FontCachedCodepointSupportInfo {
supported : Array[UInt]
not_supported : Array[UInt]
mut charmap_opt : @moon_swash.Charmap?
fn new() -> FontCachedCodepointSupportInfo
}
///|
fn FontCachedCodepointSupportInfo::new() -> FontCachedCodepointSupportInfo {
FontCachedCodepointSupportInfo::{
supported: [],
not_supported: [],
charmap_opt: None,
}
}
///|
pub struct FontMatchAttrs {
family : Family
weight : Weight
stretch : @moon_swash.Stretch
style : @moon_swash.Style
}
///|
pub impl Eq for FontMatchAttrs with equal(self, other) {
self.family == other.family &&
self.weight == other.weight &&
self.stretch == other.stretch &&
self.style == other.style
}
///|
pub impl Hash for FontMatchAttrs with hash_combine(self, hasher) {
self.family.hash_combine(hasher)
self.weight.hash_combine(hasher)
hasher.combine_int(self.stretch.raw().to_int())
hasher.combine_string(self.style.to_string())
}
///|
fn family_matches(entry : FontEntry, family : Family) -> Bool {
match family {
Name(name) => entry_has_family_name(entry, name)
Monospace | UiMonospace => entry.is_monospace
Emoji => !entry.not_emoji
_ => true
}
}
///|
fn explicit_family_matches(entry : FontEntry, family : Family) -> Bool {
match family {
Name(name) => entry_has_family_name(entry, name)
Monospace | UiMonospace => entry.is_monospace
Emoji => !entry.not_emoji
_ => false
}
}
///|
fn fallback_profile_to_sub(
profile : FallbackProfile,
) -> @font_fallback.FallbackProfile {
match profile {
Unix => @font_fallback.Unix
MacOS => @font_fallback.MacOS
Windows => @font_fallback.Windows
Other => @font_fallback.Other
}
}
///|
fn fallback_profile_from_sub(
profile : @font_fallback.FallbackProfile,
) -> FallbackProfile {
match profile {
@font_fallback.Unix => Unix
@font_fallback.MacOS => MacOS
@font_fallback.Windows => Windows
@font_fallback.Other => Other
}
}
///|
fn default_fallback_profile(_fs : FontSystem) -> FallbackProfile {
// Use explicit platform default profile. Runtime font list heuristics are
// intentionally avoided to keep behavior deterministic.
fallback_profile_from_sub(@font_fallback.platform_default_fallback_profile())
}
///|
fn fallback_profile(fs : FontSystem) -> FallbackProfile {
match fs.fallback_profile_opt {
Some(profile) => profile
None => {
let profile = default_fallback_profile(fs)
fs.fallback_profile_opt = Some(profile)
profile
}
}
}
///|
pub fn FontSystem::set_fallback_profile(
self : FontSystem,
profile : FallbackProfile,
) -> FontSystem {
if self.fallback_profile_opt is Some(current) && current == profile {
self
} else {
FontSystem::{ ..self, fallback_profile_opt: Some(profile) }
}
}
///|
pub fn FontSystem::clear_fallback_profile(self : FontSystem) -> FontSystem {
FontSystem::{ ..self, fallback_profile_opt: None }
}
///|
fn forbidden_fallback_family_names(profile : FallbackProfile) -> Array[String] {
@font_fallback.forbidden_fallback_family_names(
fallback_profile_to_sub(profile),
)
}
///|
fn filter_forbidden_from_other_stage(
fs : FontSystem,
forbidden_families : Array[String],
ids : Array[Int],
) -> Array[Int] {
if forbidden_families.length() == 0 {
return ids
}
let head : Array[Int] = []
for id in ids {
if id < 0 || id >= fs.fonts.length() {
head.push(id)
continue
}
let entry = fs.fonts[id]
if !entry_in_forbidden_fallback(entry, forbidden_families) {
head.push(id)
}
}
head
}
///|
fn split_default_stage_candidates(
fs : FontSystem,
family : Family,
req_weight : Weight,
ids : Array[Int],
) -> (Array[Int], Array[Int]) {
let defaults : Array[Int] = []
let rest : Array[Int] = []
let mut default_set = false
for id in ids {
if id < 0 || id >= fs.fonts.length() {
continue
}
let entry = fs.fonts[id]
if !default_set &&
explicit_family_matches(entry, family) &&
(
entry_weight_diff(entry, req_weight) == 0 ||
entry_variable_weight_match(entry, req_weight)
) {
default_set = true
defaults.push(id)
} else {
rest.push(id)
}
}
(defaults, rest)
}
///|
fn first_named_fallback_candidate(
fs : FontSystem,
req_weight : Weight,
ids : Array[Int],
family_name : String,
) -> Int? {
for id in ids {
if id < 0 || id >= fs.fonts.length() {
continue
}
let entry = fs.fonts[id]
if (
entry_weight_diff(entry, req_weight) == 0 ||
entry_variable_weight_match(entry, req_weight)
) &&
entry_has_family_name(entry, family_name) {
return Some(id)
}
}
None
}
///|
fn common_fallback_family_names(profile : FallbackProfile) -> Array[String] {
@font_fallback.common_fallback_family_names(fallback_profile_to_sub(profile))
}
///|
fn script_uses_fallback_stage(script : @moon_swash.Script) -> Bool {
@font_fallback.script_uses_fallback_stage(script)
}
///|
fn script_fallback_family_names(
script : @moon_swash.Script,
locale : String,
profile : FallbackProfile,
) -> Array[String] {
@font_fallback.script_fallback_family_names(
script,
locale,
fallback_profile_to_sub(profile),
)
}
///|
fn script_in_array(
scripts : Array[@moon_swash.Script],
script : @moon_swash.Script,
) -> Bool {
for s in scripts {
if s.to_opentype() == script.to_opentype() {
return true
}
}
false
}
///|
fn font_maps_codepoint(
fs : FontSystem,
entry : FontEntry,
codepoint : UInt,
) -> Bool {
match fs.codepoint_support_cache.get(entry.id) {
Some(info) => {
let ok = cached_codepoint_has_support(info, entry, codepoint)
fs.codepoint_support_cache.set(entry.id, info)
ok
}
None => {
let info = FontCachedCodepointSupportInfo()
let ok = cached_codepoint_has_support(info, entry, codepoint)
fs.codepoint_support_cache.set(entry.id, info)
ok
}
}
}
///|
fn sorted_search_uint(arr : Array[UInt], value : UInt) -> (Bool, Int) {
let mut low = 0
let mut high = arr.length()
while low < high {
let mid = low + (high - low) / 2
let cur = arr[mid]
if cur == value {
return (true, mid)
}
if cur < value {
low = mid + 1
} else {
high = mid
}
}
(false, low)
}
///|
fn cached_codepoint_has_support(
info : FontCachedCodepointSupportInfo,
entry : FontEntry,
codepoint : UInt,
) -> Bool {
let (supported_hit, supported_insert_pos) = sorted_search_uint(
info.supported,
codepoint,
)
if supported_hit {
return true
}
let (not_supported_hit, not_supported_insert_pos) = sorted_search_uint(
info.not_supported,
codepoint,
)
if not_supported_hit {
return false
}
let cm = match info.charmap_opt {
Some(cached) => cached
None => {
let built = entry.charmap_proxy.materialize(entry.font)
info.charmap_opt = Some(built)
built
}
}
let ok = cm.map(codepoint) != 0
if ok {
if supported_insert_pos != CODEPOINT_SUPPORTED_PER_FONT_LIMIT {
info.supported.insert(supported_insert_pos, codepoint)
info.supported.truncate(CODEPOINT_SUPPORTED_PER_FONT_LIMIT)
}
} else if not_supported_insert_pos != CODEPOINT_NOT_SUPPORTED_PER_FONT_LIMIT {
info.not_supported.insert(not_supported_insert_pos, codepoint)
info.not_supported.truncate(CODEPOINT_NOT_SUPPORTED_PER_FONT_LIMIT)
}
ok
}
///|
fn entry_weight_diff(entry : FontEntry, req_weight : Weight) -> Int {
let a = entry.attributes.weight().value.to_int()
abs_int(a - req_weight.value)
}
///|
fn entry_weight_raw(entry : FontEntry) -> Int {
entry.attributes.weight().value.to_int()
}
///|
fn entry_stretch_raw(entry : FontEntry) -> Int {
entry.attributes.stretch().raw().to_int()
}
///|
fn entry_stretch_diff(
entry : FontEntry,
req_stretch : @moon_swash.Stretch,
) -> Int {
abs_int(entry_stretch_raw(entry) - req_stretch.raw().to_int())
}
///|
fn style_bucket(style : @moon_swash.Style) -> Int {
match style {
@moon_swash.Style::Normal => 0
@moon_swash.Style::Italic => 1
@moon_swash.Style::Oblique(_) => 2
}
}
///|
fn style_diff(lhs : @moon_swash.Style, rhs : @moon_swash.Style) -> Int {
let a = style_bucket(lhs)
let b = style_bucket(rhs)
if a == b {
0
} else if (a == 1 && b == 2) || (a == 2 && b == 1) {
1
} else {
2
}
}
///|
fn entry_style_diff(entry : FontEntry, req_style : @moon_swash.Style) -> Int {
style_diff(entry.attributes.style(), req_style)
}
///|
fn entry_variable_weight_match(entry : FontEntry, req_weight : Weight) -> Bool {
if entry_weight_diff(entry, req_weight) == 0 ||
!entry.attributes.has_weight_variation() {
return false
}
match entry.font.variations().find_by_tag(WEIGHT_AXIS_TAG) {
None => false
Some(axis) => {
let w = req_weight.value.to_double()
let min_w = axis.min_value()
let max_w = axis.max_value()
w >= min_w && w <= max_w
}
}
}
///|
fn better_font_match(
cand : (Int, Bool, Int, Int, Int, Int, Int, Bool),
best : (Int, Bool, Int, Int, Int, Int, Int, Bool),
) -> Bool {
let cand_id = cand.0
let cand_not_emoji = cand.1
let cand_wdiff = cand.2
let cand_stretch_diff = cand.3
let cand_style_diff = cand.4
let cand_weight = cand.5
let cand_stretch = cand.6
let best_id = best.0
let best_not_emoji = best.1
let best_wdiff = best.2
let best_stretch_diff = best.3
let best_style_diff = best.4
let best_weight = best.5
let best_stretch = best.6
if cand_not_emoji != best_not_emoji {
// Keep the same ordering semantics as reference `FontMatchKey` derived Ord.
return !cand_not_emoji
}
if cand_wdiff != best_wdiff {
return cand_wdiff < best_wdiff
}
if cand_stretch_diff != best_stretch_diff {
return cand_stretch_diff < best_stretch_diff
}
if cand_style_diff != best_style_diff {
return cand_style_diff < best_style_diff
}
if cand_weight != best_weight {
return cand_weight < best_weight
}
if cand_stretch != best_stretch {
return cand_stretch < best_stretch
}
cand_id < best_id
}
///|
fn build_font_matches(fs : FontSystem, attrs : Attrs) -> Array[Int] {
let family = attrs.family_value()
let req_weight = attrs.weight_value()
let req_stretch = attrs.stretch_value()
let req_style = attrs.style_value()
let candidates : Array[(Int, Bool, Int, Int, Int, Int, Int, Bool)] = []
for e in fs.fonts {
candidates.push(
(
e.id,
e.not_emoji,
entry_weight_diff(e, req_weight),
entry_stretch_diff(e, req_stretch),
entry_style_diff(e, req_style),
entry_weight_raw(e),
entry_stretch_raw(e),
entry_variable_weight_match(e, req_weight),
),
)
}
let n = candidates.length()
let used : Array[Bool] = Array::makei(n, _ => false)
let ids : Array[Int] = []
for _ in 0..= 0 {
used.set(best_i, true)
ids.push(candidates[best_i].0)
}
}
// Keep candidate ordering close to upstream:
// sort globally by match score, then move query best family match to front.
let mut preferred : (Int, Bool, Int, Int, Int, Int, Int, Bool)? = None
for e in fs.fonts {
if !explicit_family_matches(e, family) {
continue
}
let cand = (
e.id,
e.not_emoji,
entry_weight_diff(e, req_weight),
entry_stretch_diff(e, req_stretch),
entry_style_diff(e, req_style),
entry_weight_raw(e),
entry_stretch_raw(e),
entry_variable_weight_match(e, req_weight),
)
match preferred {
None => preferred = Some(cand)
Some(best) => if better_font_match(cand, best) { preferred = Some(cand) }
}
}
match preferred {
None => ids
Some(best) => {
let pid = best.0
if ids.length() == 0 || ids[0] == pid {
return ids
}
let reordered : Array[Int] = [pid]
for id in ids {
if id != pid {
reordered.push(id)
}
}
reordered
}
}
}
///|
fn get_font_matches(fs : FontSystem, attrs : Attrs) -> Array[Int] {
if fs.font_matches_cache.length() >= FONT_MATCHES_CACHE_SIZE_LIMIT {
fs.font_matches_cache.clear()
}
let key = FontMatchAttrs::{
family: attrs.family_value(),
weight: attrs.weight_value(),
stretch: attrs.stretch_value(),
style: attrs.style_value(),
}
match fs.font_matches_cache.get(key) {
Some(ids) => ids.copy()
None => {
let ids = build_font_matches(fs, attrs)
fs.font_matches_cache.set(key, ids)
ids.copy()
}
}
}
///|
fn codepoint_to_char(codepoint : UInt) -> Char? {
if codepoint > 0x10FFFF {
None
} else {
let v = codepoint.reinterpret_as_int()
if v >= 0xD800 && v <= 0xDFFF {
return None
}
Some(v.unsafe_to_char())
}
}
///|
/// Get ordered font match candidates for attrs (cached).
pub fn FontSystem::font_matches(self : FontSystem, attrs : Attrs) -> Array[Int] {
get_font_matches(self, attrs)
}
///|
pub fn FontSystem::font_matches_for_script(
self : FontSystem,
attrs : Attrs,
script : @moon_swash.Script,
) -> Array[Int] {
self.font_matches_for_scripts(attrs, [script])
}
///|
pub fn FontSystem::font_matches_for_scripts(
self : FontSystem,
attrs : Attrs,
scripts : Array[@moon_swash.Script],
) -> Array[Int] {
let req_weight = attrs.weight_value()
let profile = fallback_profile(self)
let ids = get_font_matches(self, attrs)
let forbidden_families = forbidden_fallback_family_names(profile)
let defaults_and_rest = split_default_stage_candidates(
self,
attrs.family_value(),
req_weight,
ids,
)
let defaults = defaults_and_rest.0
let seen_scripts : Array[@moon_swash.Script] = []
let script_stage : Array[Int] = []
for script in scripts {
if !script_uses_fallback_stage(script) ||
script_in_array(seen_scripts, script) {
continue
}
seen_scripts.push(script)
let families = script_fallback_family_names(script, self.locale, profile)
for family_name in families {
match first_named_fallback_candidate(self, req_weight, ids, family_name) {
None => ()
Some(id) => script_stage.push(id)
}
}
}
let common_stage : Array[Int] = []
for family_name in common_fallback_family_names(profile) {
match first_named_fallback_candidate(self, req_weight, ids, family_name) {
None => ()
Some(id) => common_stage.push(id)
}
}
let other_stage = filter_forbidden_from_other_stage(
self, forbidden_families, ids,
)
let staged : Array[Int] = []
for id in defaults {
staged.push(id)
}
for id in script_stage {
staged.push(id)
}
for id in common_stage {
staged.push(id)
}
for id in other_stage {
staged.push(id)
}
staged
}
///|
pub fn FontSystem::count_supported_codepoints(
self : FontSystem,
font_id : Int,
codepoints : Array[UInt],
) -> Int {
if font_id < 0 || font_id >= self.fonts.length() {
return 0
}
let entry = self.fonts[font_id]
let mut count = 0
for cp in codepoints {
if font_maps_codepoint(self, entry, cp) {
count = count + 1
}
}
count
}
///|
fn select_family_with_weight(
fs : FontSystem,
family : Family,
req_weight : Weight,
) -> Int? {
let attrs = Attrs::new().family(family).weight(req_weight)
let ids = get_font_matches(fs, attrs)
for id in ids {
if id >= 0 && id < fs.fonts.length() && family_matches(fs.fonts[id], family) {
return Some(id)
}
}
None
}
///|
/// Resolve a font ID for a specific codepoint (best-effort).
///
/// This prefers the requested family (when present) and then minimizes weight
/// difference, falling back to any loaded font that supports the codepoint.
pub fn FontSystem::resolve_for_codepoint(
self : FontSystem,
attrs : Attrs,
codepoint : UInt,
) -> Int? {
let scripts : Array[@moon_swash.Script] = []
match codepoint_to_char(codepoint) {
None => ()
Some(ch) =>
scripts.push(@moon_swash.CharInfo::from_char(ch).properties().script())
}
let matches = if scripts.length() > 0 {
self.font_matches_for_scripts(attrs, scripts)
} else {
get_font_matches(self, attrs)
}
for id in matches {
if id < 0 || id >= self.fonts.length() {
continue
}
let e = self.fonts[id]
if font_maps_codepoint(self, e, codepoint) {
return Some(id)
}
}
self.resolve(attrs)
}
///|
/// Resolve a font ID for the given attrs (best-effort).
pub fn FontSystem::resolve(self : FontSystem, attrs : Attrs) -> Int? {
let matches = get_font_matches(self, attrs)
if matches.length() > 0 {
Some(matches[0])
} else if self.fonts.length() > 0 {
Some(0)
} else {
None
}
}
///|
pub fn FontSystem::get_font(
self : FontSystem,
font_id : Int,
) -> @moon_swash.FontRef? {
if font_id < 0 || font_id >= self.fonts.length() {
return None
}
Some(self.fonts[font_id].font)
}
///|
/// Get the full FontEntry for a font_id (useful for downstream rasterization with TTC collections).
pub fn FontSystem::get_font_entry(
self : FontSystem,
font_id : Int,
) -> FontEntry? {
if font_id < 0 || font_id >= self.fonts.length() {
return None
}
Some(self.fonts[font_id])
}