Add system font enumeration via font-kit
Rust: list_system_fonts and load_system_font Tauri commands using font-kit crate for cross-platform system font access. TS: font shim detects Tauri runtime and uses invoke() for font listing/loading instead of queryLocalFonts (unavailable in WKWebView).
This commit is contained in:
parent
f7d1d59dc1
commit
5ccae709a0
159
desktop/Cargo.lock
generated
159
desktop/Cargo.lock
generated
|
|
@ -487,6 +487,16 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
|
|
@ -503,6 +513,19 @@ version = "0.8.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics"
|
||||
version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics-types 0.1.3",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics"
|
||||
version = "0.24.0"
|
||||
|
|
@ -510,12 +533,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics-types 0.2.0",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics-types"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics-types"
|
||||
version = "0.2.0"
|
||||
|
|
@ -523,7 +557,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-text"
|
||||
version = "20.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5"
|
||||
dependencies = [
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics 0.23.2",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
]
|
||||
|
||||
|
|
@ -736,6 +782,15 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlib"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlopen2"
|
||||
version = "0.8.2"
|
||||
|
|
@ -789,6 +844,18 @@ version = "1.0.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "dwrote"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"winapi",
|
||||
"wio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.20"
|
||||
|
|
@ -931,6 +998,12 @@ dependencies = [
|
|||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "float-ord"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
|
|
@ -943,6 +1016,31 @@ version = "0.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "font-kit"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c7e611d49285d4c4b2e1727b72cf05353558885cc5252f93707b845dfcaf3d3"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"byteorder",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics 0.23.2",
|
||||
"core-text",
|
||||
"dirs",
|
||||
"dwrote",
|
||||
"float-ord",
|
||||
"freetype-sys",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"pathfinder_geometry",
|
||||
"pathfinder_simd",
|
||||
"walkdir",
|
||||
"winapi",
|
||||
"yeslogic-fontconfig-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
|
|
@ -979,6 +1077,17 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "freetype-sys"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futf"
|
||||
version = "0.1.5"
|
||||
|
|
@ -2359,6 +2468,7 @@ dependencies = [
|
|||
name = "open_pencil"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"font-kit",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
|
|
@ -2446,6 +2556,25 @@ version = "0.2.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pathfinder_geometry"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pathfinder_simd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathfinder_simd"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf9027960355bf3afff9841918474a81a5f972ac6d226d518060bba758b5ad57"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
|
|
@ -3507,8 +3636,8 @@ checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
|
|||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"block2",
|
||||
"core-foundation",
|
||||
"core-graphics",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics 0.24.0",
|
||||
"crossbeam-channel",
|
||||
"dispatch",
|
||||
"dlopen2",
|
||||
|
|
@ -5043,6 +5172,15 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wio"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
|
|
@ -5203,6 +5341,17 @@ dependencies = [
|
|||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yeslogic-fontconfig-sys"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd"
|
||||
dependencies = [
|
||||
"dlib",
|
||||
"once_cell",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
|
|
|
|||
|
|
@ -27,4 +27,5 @@ tauri-plugin-dialog = "2.6.0"
|
|||
tauri-plugin-fs = "2.4.5"
|
||||
zstd = "0.13"
|
||||
zip = { version = "2", default-features = false }
|
||||
font-kit = "0.14.3"
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,114 @@ use tauri::{
|
|||
Emitter, Manager,
|
||||
};
|
||||
|
||||
use font_kit::source::SystemSource;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
struct FontFamily {
|
||||
family: String,
|
||||
styles: Vec<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn list_system_fonts() -> Vec<FontFamily> {
|
||||
let source = SystemSource::new();
|
||||
let mut families: Vec<FontFamily> = Vec::new();
|
||||
|
||||
if let Ok(family_names) = source.all_families() {
|
||||
for name in &family_names {
|
||||
if let Ok(handle) = source.select_family_by_name(name) {
|
||||
let styles: Vec<String> = handle
|
||||
.fonts()
|
||||
.iter()
|
||||
.filter_map(|f| {
|
||||
f.load().ok().map(|font| {
|
||||
let props = font.properties();
|
||||
let mut style = match props.weight.0 as i32 {
|
||||
0..=150 => "Thin",
|
||||
151..=250 => "ExtraLight",
|
||||
251..=350 => "Light",
|
||||
351..=450 => "Regular",
|
||||
451..=550 => "Medium",
|
||||
551..=650 => "SemiBold",
|
||||
651..=750 => "Bold",
|
||||
751..=850 => "ExtraBold",
|
||||
_ => "Black",
|
||||
}
|
||||
.to_string();
|
||||
if props.style == font_kit::properties::Style::Italic {
|
||||
style.push_str(" Italic");
|
||||
}
|
||||
style
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !styles.is_empty() {
|
||||
families.push(FontFamily {
|
||||
family: name.clone(),
|
||||
styles,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
families.sort_by(|a, b| a.family.cmp(&b.family));
|
||||
families
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn load_system_font(family: String, style: String) -> Result<Vec<u8>, String> {
|
||||
let source = SystemSource::new();
|
||||
let family_handle = source
|
||||
.select_family_by_name(&family)
|
||||
.map_err(|e| format!("Font family not found: {e}"))?;
|
||||
|
||||
let is_italic = style.contains("Italic");
|
||||
let weight_str = style.replace(" Italic", "");
|
||||
let weight = match weight_str.as_str() {
|
||||
"Thin" => font_kit::properties::Weight::THIN,
|
||||
"ExtraLight" => font_kit::properties::Weight::EXTRA_LIGHT,
|
||||
"Light" => font_kit::properties::Weight::LIGHT,
|
||||
"Regular" | "" => font_kit::properties::Weight::NORMAL,
|
||||
"Medium" => font_kit::properties::Weight::MEDIUM,
|
||||
"SemiBold" => font_kit::properties::Weight::SEMIBOLD,
|
||||
"Bold" => font_kit::properties::Weight::BOLD,
|
||||
"ExtraBold" => font_kit::properties::Weight::EXTRA_BOLD,
|
||||
"Black" => font_kit::properties::Weight::BLACK,
|
||||
_ => font_kit::properties::Weight::NORMAL,
|
||||
};
|
||||
let style_prop = if is_italic {
|
||||
font_kit::properties::Style::Italic
|
||||
} else {
|
||||
font_kit::properties::Style::Normal
|
||||
};
|
||||
|
||||
for handle in family_handle.fonts() {
|
||||
if let Ok(font) = handle.load() {
|
||||
let props = font.properties();
|
||||
let w_diff = (props.weight.0 - weight.0).abs();
|
||||
if w_diff < 50.0 && props.style == style_prop {
|
||||
if let Some(data) = font.copy_font_data() {
|
||||
return Ok((*data).clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: return first font in family
|
||||
if let Some(handle) = family_handle.fonts().first() {
|
||||
if let Ok(font) = handle.load() {
|
||||
if let Some(data) = font.copy_font_data() {
|
||||
return Ok((*data).clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("Could not load font {family} {style}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn build_fig_file(
|
||||
schema_deflated: Vec<u8>,
|
||||
|
|
@ -63,7 +171,7 @@ fn build_fig_file(
|
|||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![build_fig_file])
|
||||
.invoke_handler(tauri::generate_handler![build_fig_file, list_system_fonts, load_system_font])
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export {
|
|||
export { SkiaRenderer, type RenderOverlays } from './renderer'
|
||||
export { computeLayout, computeAllLayouts } from './layout'
|
||||
export { getCanvasKit, type CanvasKitOptions } from './canvaskit'
|
||||
export { loadFont, listFamilies } from './fonts'
|
||||
export { loadFont, listFamilies, initFontService, getFontProvider, ensureNodeFont } from './fonts'
|
||||
export { parseColor, colorToHex, colorToHexRaw, colorToRgba255 } from './color'
|
||||
export { vectorNetworkToPath, decodeVectorNetworkBlob, encodeVectorNetworkBlob, computeVectorBounds } from './vector'
|
||||
export { computeSelectionBounds, computeSnap, type SnapGuide } from './snap'
|
||||
|
|
|
|||
|
|
@ -1 +1,73 @@
|
|||
export { loadFont, listFamilies } from '@open-pencil/core'
|
||||
export { initFontService, getFontProvider, ensureNodeFont } from '@open-pencil/core'
|
||||
|
||||
import { loadFont as loadFontCore, getFontProvider } from '@open-pencil/core'
|
||||
|
||||
interface TauriFontFamily {
|
||||
family: string
|
||||
styles: string[]
|
||||
}
|
||||
|
||||
let tauriFontsCache: TauriFontFamily[] | null = null
|
||||
|
||||
function isTauri(): boolean {
|
||||
return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window
|
||||
}
|
||||
|
||||
async function getTauriFonts(): Promise<TauriFontFamily[]> {
|
||||
if (tauriFontsCache) return tauriFontsCache
|
||||
try {
|
||||
const { invoke } = await import('@tauri-apps/api/core')
|
||||
tauriFontsCache = await invoke<TauriFontFamily[]>('list_system_fonts')
|
||||
return tauriFontsCache
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function listFamilies(): Promise<string[]> {
|
||||
if (isTauri()) {
|
||||
const fonts = await getTauriFonts()
|
||||
return fonts.map((f) => f.family)
|
||||
}
|
||||
|
||||
const { listFamilies: coreList } = await import('@open-pencil/core')
|
||||
return coreList()
|
||||
}
|
||||
|
||||
export async function loadFont(family: string, style = 'Regular'): Promise<ArrayBuffer | null> {
|
||||
if (isTauri()) {
|
||||
try {
|
||||
const { invoke } = await import('@tauri-apps/api/core')
|
||||
const data = await invoke<number[]>('load_system_font', { family, style })
|
||||
const buffer = new Uint8Array(data).buffer
|
||||
|
||||
const provider = getFontProvider()
|
||||
if (provider) provider.registerFont(buffer, family)
|
||||
|
||||
const weight = styleToWeight(style)
|
||||
const italic = style.toLowerCase().includes('italic') ? 'italic' : 'normal'
|
||||
const face = new FontFace(family, buffer, { weight: String(weight), style: italic })
|
||||
await face.load()
|
||||
document.fonts.add(face)
|
||||
|
||||
return buffer
|
||||
} catch {
|
||||
return loadFontCore(family, style)
|
||||
}
|
||||
}
|
||||
|
||||
return loadFontCore(family, style)
|
||||
}
|
||||
|
||||
function styleToWeight(style: string): number {
|
||||
const s = style.toLowerCase()
|
||||
if (s.includes('thin') || s.includes('hairline')) return 100
|
||||
if (s.includes('extralight') || s.includes('ultralight')) return 200
|
||||
if (s.includes('light')) return 300
|
||||
if (s.includes('medium')) return 500
|
||||
if (s.includes('semibold') || s.includes('demibold')) return 600
|
||||
if (s.includes('extrabold') || s.includes('ultrabold')) return 800
|
||||
if (s.includes('black') || s.includes('heavy')) return 900
|
||||
if (s.includes('bold')) return 700
|
||||
return 400
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue