blitz-pdf 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -11
- package/THIRD_PARTY_NOTICES.md +19 -1
- package/dist/api.d.ts +2 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +31 -6
- package/dist/api.js.map +1 -1
- package/dist/browser.d.ts +2 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/node.d.ts +2 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +6 -2
- package/dist/node.js.map +1 -1
- package/dist/types.d.ts +16 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/worker-host.d.ts +10 -5
- package/dist/worker-host.d.ts.map +1 -1
- package/dist/worker-host.js +149 -28
- package/dist/worker-host.js.map +1 -1
- package/dist/worker-protocol.d.ts +5 -0
- package/dist/worker-protocol.d.ts.map +1 -1
- package/dist/worker-runtime.d.ts +1 -1
- package/dist/worker-runtime.d.ts.map +1 -1
- package/dist/worker-runtime.js +22 -6
- package/dist/worker-runtime.js.map +1 -1
- package/licenses/Noto-OFL.txt +94 -0
- package/licenses/NotoCJK-OFL.txt +92 -0
- package/licenses/NotoEmoji-OFL.txt +93 -0
- package/licenses/RUST_DEPENDENCIES.txt +34 -40
- package/licenses/Unifont-OFL.txt +90 -0
- package/licenses/font-sources.json +106 -0
- package/package.json +1 -1
- package/src/api.ts +32 -7
- package/src/browser.ts +2 -1
- package/src/node.ts +7 -2
- package/src/types.ts +17 -1
- package/src/worker-host.ts +148 -29
- package/src/worker-protocol.ts +5 -5
- package/src/worker-runtime.ts +20 -6
- package/vendor/README.md +38 -5
- package/vendor/blitz-dom/Cargo.toml +6 -6
- package/vendor/blitz-dom/src/accessibility.rs +175 -2
- package/vendor/blitz-dom/src/config.rs +8 -10
- package/vendor/blitz-dom/src/document.rs +66 -3
- package/vendor/blitz-dom/src/events/keyboard.rs +2 -6
- package/vendor/blitz-dom/src/layout/construct.rs +5 -300
- package/vendor/blitz-dom/src/layout/inline/build.rs +309 -0
- package/vendor/blitz-dom/src/layout/{inline.rs → inline/layout.rs} +1 -1
- package/vendor/blitz-dom/src/layout/inline/mod.rs +4 -0
- package/vendor/blitz-dom/src/layout/mod.rs +3 -1
- package/vendor/blitz-dom/src/layout/replaced.rs +13 -4
- package/vendor/blitz-dom/src/mutator.rs +47 -0
- package/vendor/blitz-dom/src/node/element.rs +17 -0
- package/vendor/blitz-dom/src/node/mod.rs +1 -0
- package/vendor/blitz-dom/src/node/node.rs +46 -5
- package/vendor/blitz-dom/src/node/text.rs +11 -11
- package/vendor/blitz-dom/src/resolve.rs +2 -2
- package/vendor/blitz-dom/src/stylo.rs +30 -22
- package/vendor/blitz-dom/src/util.rs +1 -1
- package/vendor/blitz-paint/Cargo.toml +4 -4
- package/vendor/fontique/Cargo.toml +194 -0
- package/vendor/fontique/LICENSE-APACHE +201 -0
- package/vendor/fontique/LICENSE-MIT +25 -0
- package/vendor/fontique/README.md +54 -0
- package/vendor/fontique/examples/generic_families.rs +37 -0
- package/vendor/fontique/src/attributes.rs +42 -0
- package/vendor/fontique/src/backend/android.rs +218 -0
- package/vendor/fontique/src/backend/coretext.rs +206 -0
- package/vendor/fontique/src/backend/dwrite.rs +395 -0
- package/vendor/fontique/src/backend/fontconfig.rs +844 -0
- package/vendor/fontique/src/backend/mod.rs +75 -0
- package/vendor/fontique/src/charmap.rs +127 -0
- package/vendor/fontique/src/collection/mod.rs +1016 -0
- package/vendor/fontique/src/collection/query.rs +392 -0
- package/vendor/fontique/src/fallback.rs +293 -0
- package/vendor/fontique/src/family.rs +111 -0
- package/vendor/fontique/src/family_name.rs +153 -0
- package/vendor/fontique/src/font.rs +562 -0
- package/vendor/fontique/src/generic.rs +36 -0
- package/vendor/fontique/src/impl_fontconfig.rs +159 -0
- package/vendor/fontique/src/lib.rs +87 -0
- package/vendor/fontique/src/matching.rs +483 -0
- package/vendor/fontique/src/scan.rs +312 -0
- package/vendor/fontique/src/script.rs +271 -0
- package/vendor/fontique/src/source.rs +108 -0
- package/vendor/fontique/src/source_cache.rs +254 -0
- package/wasm/blitz_pdf_bg.wasm +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// Copyright 2024 the Parley Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
|
+
|
|
4
|
+
use alloc::{
|
|
5
|
+
boxed::Box,
|
|
6
|
+
str::FromStr,
|
|
7
|
+
string::{String, ToString},
|
|
8
|
+
sync::Arc,
|
|
9
|
+
vec,
|
|
10
|
+
vec::Vec,
|
|
11
|
+
};
|
|
12
|
+
use std::path::Path;
|
|
13
|
+
|
|
14
|
+
use hashbrown::HashMap;
|
|
15
|
+
use roxmltree::{Document, Node};
|
|
16
|
+
|
|
17
|
+
use super::{
|
|
18
|
+
FallbackKey, FamilyId, FamilyInfo, FamilyNameMap, GenericFamily, GenericFamilyMap, Language,
|
|
19
|
+
Script, scan,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// TODO: Use actual generic families here, where available, when fonts.xml is properly parsed.
|
|
23
|
+
// system-ui should map to `variant="compact"` in some scripts during fallback resolution.
|
|
24
|
+
const DEFAULT_GENERIC_FAMILIES: &[(GenericFamily, &[&str])] = &[
|
|
25
|
+
(
|
|
26
|
+
GenericFamily::SansSerif,
|
|
27
|
+
&["Roboto Flex", "Roboto", "Noto Sans"],
|
|
28
|
+
),
|
|
29
|
+
(GenericFamily::Serif, &["Noto Serif"]),
|
|
30
|
+
(GenericFamily::Monospace, &["monospace"]),
|
|
31
|
+
(GenericFamily::Cursive, &["Dancing Script"]),
|
|
32
|
+
(GenericFamily::Fantasy, &["Noto Serif"]),
|
|
33
|
+
(
|
|
34
|
+
GenericFamily::SystemUi,
|
|
35
|
+
&["Roboto Flex", "Roboto", "Noto Sans"],
|
|
36
|
+
),
|
|
37
|
+
(GenericFamily::Emoji, &["Noto Color Emoji"]),
|
|
38
|
+
(GenericFamily::Math, &["Noto Sans Math", "Noto Sans"]),
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
pub(crate) struct SystemFonts {
|
|
42
|
+
pub(crate) name_map: Arc<FamilyNameMap>,
|
|
43
|
+
pub(crate) generic_families: Arc<GenericFamilyMap>,
|
|
44
|
+
family_map: HashMap<FamilyId, FamilyInfo>,
|
|
45
|
+
locale_fallback: Box<[(Language, FamilyId)]>,
|
|
46
|
+
script_fallback: Box<[(Script, FamilyId)]>,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
impl SystemFonts {
|
|
50
|
+
pub(crate) fn new() -> Self {
|
|
51
|
+
let android_root: String = std::env::var("ANDROID_ROOT").unwrap_or("/system".to_string());
|
|
52
|
+
|
|
53
|
+
let scan::ScannedCollection {
|
|
54
|
+
family_names: mut name_map,
|
|
55
|
+
families: family_map,
|
|
56
|
+
postscript_names,
|
|
57
|
+
..
|
|
58
|
+
} = scan::ScannedCollection::from_paths(Path::new(&android_root).join("fonts").to_str(), 8);
|
|
59
|
+
let mut generic_families = GenericFamilyMap::default();
|
|
60
|
+
for (family, names) in DEFAULT_GENERIC_FAMILIES {
|
|
61
|
+
generic_families.set(
|
|
62
|
+
*family,
|
|
63
|
+
names
|
|
64
|
+
.iter()
|
|
65
|
+
.filter_map(|name| name_map.get(name))
|
|
66
|
+
.map(|name| name.id()),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let mut locale_fallback = vec![];
|
|
71
|
+
let mut script_fallback = vec![];
|
|
72
|
+
|
|
73
|
+
// Try to get generic info from fonts.xml
|
|
74
|
+
if let Ok(s) = std::fs::read_to_string(Path::new(&android_root).join("etc/fonts.xml"))
|
|
75
|
+
&& let Ok(doc) = Document::parse(s.clone().as_str())
|
|
76
|
+
{
|
|
77
|
+
let root = doc.root_element();
|
|
78
|
+
if root.tag_name().name() == "familyset"
|
|
79
|
+
|| root
|
|
80
|
+
.attribute("version")
|
|
81
|
+
.is_some_and(|v| usize::from_str(v).is_ok_and(|x| x >= 21))
|
|
82
|
+
{
|
|
83
|
+
for child in root.children() {
|
|
84
|
+
match child.tag_name().name() {
|
|
85
|
+
"alias" => {
|
|
86
|
+
if let Some((name, to)) =
|
|
87
|
+
child.attribute("name").zip(child.attribute("to"))
|
|
88
|
+
{
|
|
89
|
+
if child.attribute("weight").is_some() {
|
|
90
|
+
// weight aliases are an Android quirk and are not in
|
|
91
|
+
// teresting for use cases other than Android legacy.
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
let to_n = name_map.get_or_insert(to);
|
|
95
|
+
name_map.add_alias(to_n.id(), name);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
"family" => {
|
|
99
|
+
if let Some(name) = child.attribute("name") {
|
|
100
|
+
let f = name_map.get_or_insert(name);
|
|
101
|
+
let _id = f.id();
|
|
102
|
+
for _child in child.children() {
|
|
103
|
+
// TODO: map using postScriptName when available other
|
|
104
|
+
// wise use the file name, and perhaps if necess
|
|
105
|
+
// ary (e.g. if it's a collection), do something
|
|
106
|
+
// smarter, or something dumb that meets expecta
|
|
107
|
+
// tions on Android.
|
|
108
|
+
}
|
|
109
|
+
} else if let Some(langs) = child
|
|
110
|
+
.attribute("lang")
|
|
111
|
+
.map(|s| s.split(',').collect::<Vec<&str>>())
|
|
112
|
+
{
|
|
113
|
+
let (_has_for, hasnt_for): (Vec<Node<'_, '_>>, Vec<Node<'_, '_>>) =
|
|
114
|
+
child
|
|
115
|
+
.children()
|
|
116
|
+
.partition(|c| c.attribute("fallbackFor").is_some());
|
|
117
|
+
{
|
|
118
|
+
// general fallback families
|
|
119
|
+
let (ps_named, _ps_unnamed): (
|
|
120
|
+
Vec<Node<'_, '_>>,
|
|
121
|
+
Vec<Node<'_, '_>>,
|
|
122
|
+
) = hasnt_for
|
|
123
|
+
.iter()
|
|
124
|
+
.partition(|c| c.attribute("postScriptName").is_some());
|
|
125
|
+
|
|
126
|
+
if let Some(family) = ps_named.iter().find_map(|x| {
|
|
127
|
+
postscript_names.get(x.attribute("postScriptName").unwrap())
|
|
128
|
+
}) {
|
|
129
|
+
for lang in langs {
|
|
130
|
+
if let Some(scr) = lang.strip_prefix("und-") {
|
|
131
|
+
// Undefined lang for script-only fallbacks
|
|
132
|
+
script_fallback.push((
|
|
133
|
+
scr.parse().unwrap_or(Script::UNKNOWN),
|
|
134
|
+
*family,
|
|
135
|
+
));
|
|
136
|
+
} else if let Ok(locale) = Language::parse(lang) {
|
|
137
|
+
if let Some(scr) = locale
|
|
138
|
+
.script()
|
|
139
|
+
.and_then(|s| s.parse::<Script>().ok())
|
|
140
|
+
{
|
|
141
|
+
// Also fallback for the script on its own
|
|
142
|
+
script_fallback.push((scr, *family));
|
|
143
|
+
if Script::from_bytes(*b"Hant") == scr {
|
|
144
|
+
// This works around ambiguous han char
|
|
145
|
+
// acters going unmapped with current
|
|
146
|
+
// fallback code. This should be done in
|
|
147
|
+
// a locale-dependent manner, since that
|
|
148
|
+
// is the norm.
|
|
149
|
+
script_fallback.push((
|
|
150
|
+
Script::from_bytes(*b"Hani"),
|
|
151
|
+
*family,
|
|
152
|
+
));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
locale_fallback.push((locale, *family));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// TODO: handle mapping to family names from file names
|
|
161
|
+
// when postScriptName is unavailable.
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// family-specific fallback families, currently unimplemented
|
|
165
|
+
// because it requires a GenericFamily to be plumbed through
|
|
166
|
+
// the `RangedStyle` `font_stack` from `resolve` where it is
|
|
167
|
+
// currently thrown away.
|
|
168
|
+
{}
|
|
169
|
+
}
|
|
170
|
+
// TODO: interpret variant="compact" without fallbackFor as a
|
|
171
|
+
// fallback for system-ui, as falling back to a
|
|
172
|
+
// variant="elegant" for system-ui can mess up a layout
|
|
173
|
+
// in some scripts.
|
|
174
|
+
}
|
|
175
|
+
_ => {}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
Self {
|
|
182
|
+
name_map: Arc::new(name_map),
|
|
183
|
+
generic_families: Arc::new(generic_families),
|
|
184
|
+
family_map,
|
|
185
|
+
locale_fallback: locale_fallback.into(),
|
|
186
|
+
script_fallback: script_fallback.into(),
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
pub(crate) fn family(&self, id: FamilyId) -> Option<FamilyInfo> {
|
|
191
|
+
self.family_map.get(&id).cloned()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
pub(crate) fn fallback(&self, key: impl Into<FallbackKey>) -> Option<FamilyId> {
|
|
195
|
+
let key: FallbackKey = key.into();
|
|
196
|
+
let script = key.script();
|
|
197
|
+
|
|
198
|
+
key.locale()
|
|
199
|
+
.and_then(|locale| {
|
|
200
|
+
self.locale_fallback
|
|
201
|
+
.iter()
|
|
202
|
+
.find(|(other, _)| locale == *other)
|
|
203
|
+
.map(|(_, fid)| *fid)
|
|
204
|
+
})
|
|
205
|
+
.or_else(|| {
|
|
206
|
+
self.script_fallback
|
|
207
|
+
.iter()
|
|
208
|
+
.find(|(s, _)| script == *s)
|
|
209
|
+
.map(|(_, fid)| *fid)
|
|
210
|
+
})
|
|
211
|
+
.or_else(|| {
|
|
212
|
+
self.generic_families
|
|
213
|
+
.get(GenericFamily::SansSerif)
|
|
214
|
+
.first()
|
|
215
|
+
.copied()
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Copyright 2024 the Parley Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
|
+
|
|
4
|
+
use super::{
|
|
5
|
+
FallbackKey, FamilyId, FamilyInfo, FamilyNameMap, GenericFamily, GenericFamilyMap, ScriptExt,
|
|
6
|
+
scan,
|
|
7
|
+
};
|
|
8
|
+
use alloc::format;
|
|
9
|
+
use alloc::string::ToString;
|
|
10
|
+
use alloc::sync::Arc;
|
|
11
|
+
use alloc::vec::Vec;
|
|
12
|
+
use core::ptr::{null, null_mut};
|
|
13
|
+
use hashbrown::{HashMap, HashSet};
|
|
14
|
+
use objc2_core_foundation::{
|
|
15
|
+
CFArray, CFDictionary, CFRange, CFRetained, CFString, CFType, CFURL, CFURLPathStyle,
|
|
16
|
+
};
|
|
17
|
+
use objc2_core_text::{
|
|
18
|
+
CTFont, CTFontCollection, CTFontDescriptor, CTFontUIFontType, kCTFontURLAttribute,
|
|
19
|
+
};
|
|
20
|
+
use objc2_foundation::{
|
|
21
|
+
NSSearchPathDirectory, NSSearchPathDomainMask, NSSearchPathForDirectoriesInDomains,
|
|
22
|
+
};
|
|
23
|
+
use parlance::Script;
|
|
24
|
+
use std::path::{Path, PathBuf};
|
|
25
|
+
|
|
26
|
+
const DEFAULT_GENERIC_FAMILIES: &[(GenericFamily, &[&str])] = &[
|
|
27
|
+
(GenericFamily::Serif, &["Times", "Times New Roman"]),
|
|
28
|
+
(GenericFamily::SansSerif, &["Helvetica"]),
|
|
29
|
+
(GenericFamily::Monospace, &["Courier", "Courier New"]),
|
|
30
|
+
(GenericFamily::Cursive, &["Apple Chancery"]),
|
|
31
|
+
(GenericFamily::Fantasy, &["Papyrus"]),
|
|
32
|
+
(GenericFamily::SystemUi, &["System Font", ".SF NS"]),
|
|
33
|
+
(GenericFamily::Emoji, &["Apple Color Emoji"]),
|
|
34
|
+
(GenericFamily::Math, &["STIX Two Math"]),
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
pub(crate) struct SystemFonts {
|
|
38
|
+
pub(crate) name_map: Arc<FamilyNameMap>,
|
|
39
|
+
pub(crate) generic_families: Arc<GenericFamilyMap>,
|
|
40
|
+
family_map: HashMap<FamilyId, FamilyInfo>,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
impl SystemFonts {
|
|
44
|
+
pub(crate) fn new() -> Self {
|
|
45
|
+
let scanned = scan_system_fonts().unwrap_or_default();
|
|
46
|
+
let name_map = scanned.family_names;
|
|
47
|
+
let mut generic_families = GenericFamilyMap::default();
|
|
48
|
+
for (family, names) in DEFAULT_GENERIC_FAMILIES {
|
|
49
|
+
generic_families.set(
|
|
50
|
+
*family,
|
|
51
|
+
names
|
|
52
|
+
.iter()
|
|
53
|
+
.filter_map(|name| name_map.get(name))
|
|
54
|
+
.map(|name| name.id()),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
Self {
|
|
58
|
+
name_map: Arc::new(name_map),
|
|
59
|
+
generic_families: Arc::new(generic_families),
|
|
60
|
+
family_map: scanned.families,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pub(crate) fn family(&mut self, id: FamilyId) -> Option<FamilyInfo> {
|
|
65
|
+
self.family_map.get(&id).cloned()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
pub(crate) fn fallback(&mut self, key: impl Into<FallbackKey>) -> Option<FamilyId> {
|
|
69
|
+
const HANI: Script = Script::from_bytes(*b"Hani");
|
|
70
|
+
const HANT: Script = Script::from_bytes(*b"Hant");
|
|
71
|
+
const HANS: Script = Script::from_bytes(*b"Hans");
|
|
72
|
+
let key = key.into();
|
|
73
|
+
let script = key.script();
|
|
74
|
+
let font = create_fallback_font_for_text(script.sample()?, key.locale_str(), false)?;
|
|
75
|
+
let family_name = unsafe { font.family_name() };
|
|
76
|
+
if let Some(family) = self.name_map.get(&family_name.to_string()) {
|
|
77
|
+
return Some(family.id());
|
|
78
|
+
}
|
|
79
|
+
// HACK: if we don't have a usable PingFangUI due to our inability
|
|
80
|
+
// to render hvgl outlines then try another font
|
|
81
|
+
let name = match script {
|
|
82
|
+
HANI | HANS => "Heiti SC",
|
|
83
|
+
HANT => "Heiti TC",
|
|
84
|
+
_ => return None,
|
|
85
|
+
};
|
|
86
|
+
self.name_map.get(name).map(|family| family.id())
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Discover system fonts by combining CoreText enumeration with a directory scan of all
|
|
91
|
+
/// Library/Fonts paths, then index them through the shared scan pipeline.
|
|
92
|
+
fn scan_system_fonts() -> Option<scan::ScannedCollection> {
|
|
93
|
+
// SAFETY: Calls into CoreText. If anything fails we return None and use the fallback scan.
|
|
94
|
+
let collection = unsafe { CTFontCollection::from_available_fonts(None) };
|
|
95
|
+
let descriptors = unsafe { collection.matching_font_descriptors()? };
|
|
96
|
+
let descriptors: CFRetained<CFArray<CTFontDescriptor>> =
|
|
97
|
+
unsafe { CFRetained::cast_unchecked(descriptors) };
|
|
98
|
+
|
|
99
|
+
// Collect unique font file paths to avoid redundant scanning.
|
|
100
|
+
let mut paths: HashSet<PathBuf> = HashSet::new();
|
|
101
|
+
for index in 0..descriptors.len() {
|
|
102
|
+
let Some(descriptor) = descriptors.get(index) else {
|
|
103
|
+
continue;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
let Some(url_cf): Option<CFRetained<CFType>> =
|
|
107
|
+
(unsafe { descriptor.attribute(kCTFontURLAttribute) })
|
|
108
|
+
else {
|
|
109
|
+
continue;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// The attribute is typed as CFType; attempt to downcast to CFURL.
|
|
113
|
+
let Ok(url_cf): Result<CFRetained<CFURL>, _> = url_cf.downcast::<CFURL>() else {
|
|
114
|
+
continue;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Convert the file URL into a POSIX path.
|
|
118
|
+
let Some(path_cf): Option<CFRetained<CFString>> =
|
|
119
|
+
url_cf.file_system_path(CFURLPathStyle::CFURLPOSIXPathStyle)
|
|
120
|
+
else {
|
|
121
|
+
continue;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
let path = PathBuf::from(path_cf.to_string());
|
|
125
|
+
if path.exists() {
|
|
126
|
+
paths.insert(path);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Apple hides certain fonts from CTFontCollection (notably SFNS.ttf, the San Francisco
|
|
131
|
+
// system UI font). Scanning Library/Fonts directories catches what CoreText omits.
|
|
132
|
+
paths.extend(library_font_files());
|
|
133
|
+
|
|
134
|
+
if paths.is_empty() {
|
|
135
|
+
return None;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
Some(scan::ScannedCollection::from_paths(paths.iter(), 0))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fn library_font_files() -> Vec<PathBuf> {
|
|
142
|
+
let mut files = Vec::new();
|
|
143
|
+
for dir in NSSearchPathForDirectoriesInDomains(
|
|
144
|
+
NSSearchPathDirectory::LibraryDirectory,
|
|
145
|
+
NSSearchPathDomainMask::AllDomainsMask,
|
|
146
|
+
true,
|
|
147
|
+
) {
|
|
148
|
+
let font_dir = PathBuf::from(format!("{dir}/Fonts"));
|
|
149
|
+
if font_dir.is_dir() {
|
|
150
|
+
collect_files(&font_dir, 8, 0, &mut files);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
files
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn collect_files(dir: &Path, max_depth: u32, depth: u32, out: &mut Vec<PathBuf>) {
|
|
157
|
+
let Ok(entries) = std::fs::read_dir(dir) else {
|
|
158
|
+
return;
|
|
159
|
+
};
|
|
160
|
+
for entry in entries.filter_map(|e| e.ok()) {
|
|
161
|
+
let path = entry.path();
|
|
162
|
+
if path.is_dir() {
|
|
163
|
+
if depth < max_depth {
|
|
164
|
+
collect_files(&path, max_depth, depth + 1, out);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
out.push(path);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fn create_base_font(prefer_ui_font: bool) -> CFRetained<CTFont> {
|
|
173
|
+
if prefer_ui_font
|
|
174
|
+
&& let Some(font) =
|
|
175
|
+
unsafe { CTFont::new_ui_font_for_language(CTFontUIFontType::System, 0.0, None) }
|
|
176
|
+
{
|
|
177
|
+
return font;
|
|
178
|
+
}
|
|
179
|
+
unsafe {
|
|
180
|
+
let attrs = CFDictionary::new(None, null_mut(), null_mut(), 0, null(), null());
|
|
181
|
+
let desc = CTFontDescriptor::with_attributes(&attrs.unwrap());
|
|
182
|
+
CTFont::with_font_descriptor(&desc, 0.0, null())
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fn create_fallback_font_for_text(
|
|
187
|
+
text: &str,
|
|
188
|
+
locale: Option<&str>,
|
|
189
|
+
prefer_ui_font: bool,
|
|
190
|
+
) -> Option<CFRetained<CTFont>> {
|
|
191
|
+
let text = CFString::from_str(text);
|
|
192
|
+
let text_range = CFRange {
|
|
193
|
+
location: 0,
|
|
194
|
+
length: text.length(),
|
|
195
|
+
};
|
|
196
|
+
let locale = locale.map(CFString::from_str);
|
|
197
|
+
let base_font = create_base_font(prefer_ui_font);
|
|
198
|
+
let font = unsafe {
|
|
199
|
+
if let Some(locale) = locale {
|
|
200
|
+
CTFont::for_string_with_language(&base_font, &text, text_range, Some(&locale))
|
|
201
|
+
} else {
|
|
202
|
+
CTFont::for_string(&base_font, &text, text_range)
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
Some(font)
|
|
206
|
+
}
|