* Fix #2330: discover installed fonts through Fontconfig * Fix wasm cfg for Fontconfig fallback * Add Linux font discovery regression tests * Reject Fontconfig matches without CJK coverage
This commit is contained in:
parent
5e0a6d404d
commit
82c58fabab
|
|
@ -220,7 +220,147 @@ pub fn families(script: CjkScript) -> &'static [&'static str] {
|
|||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct FontFile {
|
||||
pub path: std::path::PathBuf,
|
||||
pub family: &'static str,
|
||||
pub family: String,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
struct FontconfigCjkScan {
|
||||
started: std::sync::atomic::AtomicBool,
|
||||
result: std::sync::Mutex<Option<[Option<FontFile>; 4]>>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn fontconfig_cjk_scan() -> &'static FontconfigCjkScan {
|
||||
static SCAN: std::sync::OnceLock<FontconfigCjkScan> = std::sync::OnceLock::new();
|
||||
SCAN.get_or_init(|| FontconfigCjkScan { started: std::sync::atomic::AtomicBool::new(false), result: std::sync::Mutex::new(None) })
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
/// Start the optional Fontconfig CJK-family lookup in the background.
|
||||
///
|
||||
/// The scan uses `fc-match` when available and leaves the built-in path candidates unchanged
|
||||
/// when it is missing or cannot resolve a font.
|
||||
pub fn start_fontconfig_cjk_scan() {
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
let scan = fontconfig_cjk_scan();
|
||||
if scan.started.swap(true, Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let scan = fontconfig_cjk_scan();
|
||||
if std::thread::Builder::new()
|
||||
.name("photocraft-fontconfig-cjk".into())
|
||||
.spawn(move || {
|
||||
let result = [CjkScript::Japanese, CjkScript::SimplifiedChinese, CjkScript::TraditionalChinese, CjkScript::Korean].map(fontconfig_match);
|
||||
*scan.result.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = Some(result);
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
*fontconfig_cjk_scan().result.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = Some(std::array::from_fn(|_| None));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
/// No-op outside Linux, where the Fontconfig lookup is unavailable.
|
||||
pub fn start_fontconfig_cjk_scan() {}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
/// Whether the optional background Fontconfig CJK lookup is still running.
|
||||
pub fn fontconfig_cjk_scan_pending() -> bool {
|
||||
let scan = fontconfig_cjk_scan();
|
||||
scan.started.load(std::sync::atomic::Ordering::Relaxed) && scan.result.lock().unwrap_or_else(std::sync::PoisonError::into_inner).is_none()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
/// Fontconfig lookup is unavailable outside Linux.
|
||||
pub fn fontconfig_cjk_scan_pending() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn fontconfig_match(script: CjkScript) -> Option<FontFile> {
|
||||
use std::ffi::OsString;
|
||||
use std::io::Read as _;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
|
||||
const MAX_FONT_BYTES: u64 = 128 << 20;
|
||||
|
||||
let language = match script {
|
||||
CjkScript::Japanese => "ja",
|
||||
CjkScript::SimplifiedChinese => "zh-cn",
|
||||
CjkScript::TraditionalChinese => "zh-tw",
|
||||
CjkScript::Korean => "ko",
|
||||
};
|
||||
let output = std::process::Command::new("fc-match").args(["--format=%{file}\\t%{family[0]}\\n", &format!(":lang={language}")]).output().ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
let line = output.stdout.split(|byte| *byte == b'\n').find(|line| !line.is_empty())?;
|
||||
let separator = line.iter().position(|byte| *byte == b'\t')?;
|
||||
let path = line.get(..separator)?;
|
||||
let family = line.get(separator.checked_add(1)?..)?;
|
||||
let path = std::path::PathBuf::from(OsString::from_vec(path.to_vec()));
|
||||
if !path.is_absolute()
|
||||
|| !path.extension().and_then(|ext| ext.to_str()).is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "ttf" | "otf" | "ttc" | "otc"))
|
||||
|| !path.is_file()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let family = String::from_utf8_lossy(family).trim().to_string();
|
||||
if family.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let file = std::fs::File::open(&path).ok()?;
|
||||
let length = file.metadata().ok()?.len();
|
||||
if length == 0 || length > MAX_FONT_BYTES {
|
||||
return None;
|
||||
}
|
||||
let mut bytes = Vec::new();
|
||||
bytes.try_reserve_exact(usize::try_from(length).ok()?).ok()?;
|
||||
file.take(MAX_FONT_BYTES + 1).read_to_end(&mut bytes).ok()?;
|
||||
if u64::try_from(bytes.len()).ok()? > MAX_FONT_BYTES || !fontconfig_font_covers_sample(&bytes, &family, script) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(FontFile { path: path.canonicalize().unwrap_or(path), family })
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn fontconfig_font_covers_sample(bytes: &[u8], family: &str, script: CjkScript) -> bool {
|
||||
use skrifa::MetadataProvider;
|
||||
|
||||
let index = face_index_for_family(bytes, family);
|
||||
let Ok(font) = skrifa::FontRef::from_index(bytes, index) else { return false };
|
||||
let sample = match script {
|
||||
CjkScript::Japanese => "レイヤー図",
|
||||
CjkScript::SimplifiedChinese => "图层",
|
||||
CjkScript::TraditionalChinese => "圖層",
|
||||
CjkScript::Korean => "카드 배경",
|
||||
};
|
||||
sample.chars().all(|character| font.charmap().map(character).is_some())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn fontconfig_cjk_font(script: CjkScript) -> Option<FontFile> {
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
let scan = fontconfig_cjk_scan();
|
||||
if !scan.started.load(Ordering::Relaxed) {
|
||||
return None;
|
||||
}
|
||||
let index = match script {
|
||||
CjkScript::Japanese => 0,
|
||||
CjkScript::SimplifiedChinese => 1,
|
||||
CjkScript::TraditionalChinese => 2,
|
||||
CjkScript::Korean => 3,
|
||||
};
|
||||
scan.result.lock().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref()?.get(index)?.clone()
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_os = "linux"), not(target_arch = "wasm32")))]
|
||||
fn fontconfig_cjk_font(_: CjkScript) -> Option<FontFile> {
|
||||
None
|
||||
}
|
||||
|
||||
/// System font files for `script`, most preferred first. Paths that don't exist are included;
|
||||
|
|
@ -228,21 +368,21 @@ pub struct FontFile {
|
|||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn font_files(script: CjkScript) -> Vec<FontFile> {
|
||||
use CjkScript::*;
|
||||
let f = |p: &str, family: &'static str| FontFile { path: p.into(), family };
|
||||
let f = |p: &str, family: &'static str| FontFile { path: p.into(), family: family.to_string() };
|
||||
if cfg!(target_os = "macos") {
|
||||
let pingfang = |family| mac_pingfang().map(|path| FontFile { path, family });
|
||||
match script {
|
||||
Japanese => vec![f("/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc", ""), f("/System/Library/Fonts/ヒラギノ角ゴシック W4.ttc", "")],
|
||||
SimplifiedChinese => pingfang("PingFang SC")
|
||||
SimplifiedChinese => pingfang("PingFang SC".to_string())
|
||||
.into_iter()
|
||||
.chain([f("/System/Library/Fonts/Hiragino Sans GB.ttc", ""), f("/System/Library/Fonts/STHeiti Light.ttc", "Heiti SC")])
|
||||
.collect(),
|
||||
TraditionalChinese => pingfang("PingFang TC").into_iter().chain([f("/System/Library/Fonts/STHeiti Light.ttc", "Heiti TC")]).collect(),
|
||||
TraditionalChinese => pingfang("PingFang TC".to_string()).into_iter().chain([f("/System/Library/Fonts/STHeiti Light.ttc", "Heiti TC")]).collect(),
|
||||
Korean => vec![f("/System/Library/Fonts/AppleSDGothicNeo.ttc", "Apple SD Gothic Neo")],
|
||||
}
|
||||
} else if cfg!(target_os = "windows") {
|
||||
let dir = std::env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".into());
|
||||
let w = |file: &str, family: &'static str| FontFile { path: std::path::Path::new(&dir).join("Fonts").join(file), family };
|
||||
let w = |file: &str, family: &'static str| FontFile { path: std::path::Path::new(&dir).join("Fonts").join(file), family: family.to_string() };
|
||||
match script {
|
||||
Japanese => vec![w("YuGothR.ttc", "Yu Gothic"), w("YuGothM.ttc", "Yu Gothic"), w("meiryo.ttc", "Meiryo"), w("msgothic.ttc", "")],
|
||||
SimplifiedChinese => vec![w("msyh.ttc", "Microsoft YaHei"), w("msyh.ttf", ""), w("simsun.ttc", "")],
|
||||
|
|
@ -292,7 +432,13 @@ pub fn font_files(script: CjkScript) -> Vec<FontFile> {
|
|||
f("/usr/share/fonts/un-core/UnDotum.ttf", ""),
|
||||
],
|
||||
});
|
||||
with_flatpak_host_fonts(v)
|
||||
let mut files = with_flatpak_host_fonts(v);
|
||||
if let Some(font) = fontconfig_cjk_font(script)
|
||||
&& !files.iter().any(|file| file.path == font.path)
|
||||
{
|
||||
files.push(font);
|
||||
}
|
||||
files
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +452,7 @@ fn with_flatpak_host_fonts(files: Vec<FontFile>) -> Vec<FontFile> {
|
|||
file.path
|
||||
.strip_prefix("/usr/share/fonts")
|
||||
.ok()
|
||||
.map(|relative| FontFile { path: std::path::Path::new("/run/host/fonts").join(relative), family: file.family })
|
||||
.map(|relative| FontFile { path: std::path::Path::new("/run/host/fonts").join(relative), family: file.family.clone() })
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
@ -320,12 +466,12 @@ fn with_flatpak_host_fonts(files: Vec<FontFile>) -> Vec<FontFile> {
|
|||
/// but look worse than the script fonts).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn last_resort_files() -> Vec<FontFile> {
|
||||
let f = |p: &str| FontFile { path: p.into(), family: "" };
|
||||
let f = |p: &str| FontFile { path: p.into(), family: String::new() };
|
||||
if cfg!(target_os = "macos") {
|
||||
vec![f("/System/Library/Fonts/Supplemental/Arial Unicode.ttf"), f("/Library/Fonts/Arial Unicode.ttf")]
|
||||
} else if cfg!(target_os = "windows") {
|
||||
let dir = std::env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".into());
|
||||
vec![FontFile { path: std::path::Path::new(&dir).join("Fonts").join("ARIALUNI.TTF"), family: "" }]
|
||||
vec![FontFile { path: std::path::Path::new(&dir).join("Fonts").join("ARIALUNI.TTF"), family: String::new() }]
|
||||
} else {
|
||||
with_flatpak_host_fonts(vec![
|
||||
f("/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf"),
|
||||
|
|
@ -490,6 +636,25 @@ mod tests {
|
|||
assert_eq!(face_index_for_family(inter, ""), 0);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn fontconfig_candidates_must_cover_the_requested_script() {
|
||||
let inter = include_bytes!("../../../assets/fonts/Inter-Regular.ttf");
|
||||
for script in [Japanese, SimplifiedChinese, TraditionalChinese, Korean] {
|
||||
assert!(!fontconfig_font_covers_sample(inter, "Inter", script), "{script:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn fontconfig_matches_cover_the_requested_script() {
|
||||
for script in [Japanese, SimplifiedChinese, TraditionalChinese, Korean] {
|
||||
let Some(file) = fontconfig_match(script) else { continue };
|
||||
let bytes = std::fs::read(&file.path).expect("Fontconfig match remains readable");
|
||||
assert!(fontconfig_font_covers_sample(&bytes, &file.family, script), "{script:?}: {}", file.path.display());
|
||||
}
|
||||
}
|
||||
|
||||
/// On a Mac with PingFang, the SC and TC faces are found by name (not a hard-coded index).
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
//! Font database: bundled fonts (always available, also on the web), the optional craft-fonts
|
||||
//! Japanese fonts ([`crate::craft_fonts`], when built with `CRAFT_FONTS_DIR`), optional system
|
||||
//! fonts found by scanning the platform font directories (no fontconfig), user-registered font
|
||||
//! data, and PostScript-name lookup for PSD import.
|
||||
//! fonts found by scanning platform directories (plus Fontconfig's file list on Linux),
|
||||
//! user-registered font data, and PostScript-name lookup for PSD import.
|
||||
|
||||
use std::collections::HashMap;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::collections::HashSet;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
|
|
@ -179,8 +181,8 @@ impl FontDb {
|
|||
db
|
||||
}
|
||||
|
||||
/// Scans the platform font directories (once). Font files are memory-mapped lazily by
|
||||
/// fontique when a face is first used.
|
||||
/// Scans the platform font directories once, plus Fontconfig's file list on Linux. Font files
|
||||
/// are memory-mapped lazily by fontique when a face is first used.
|
||||
pub fn load_system_fonts(&mut self) {
|
||||
if self.system_loaded {
|
||||
return;
|
||||
|
|
@ -193,12 +195,18 @@ impl FontDb {
|
|||
for d in system_font_dirs() {
|
||||
collect_font_files(&d, 0, &mut files);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
files.extend(fontconfig_font_paths());
|
||||
// PingFang (the macOS Chinese UI font) lives outside the font folders.
|
||||
if cfg!(target_os = "macos") {
|
||||
files.extend(crate::cjk::mac_pingfang());
|
||||
}
|
||||
for f in files {
|
||||
self.fcx.collection.load_fonts_from_paths([f]);
|
||||
let mut seen = HashSet::new();
|
||||
for path in files {
|
||||
let key = path.canonicalize().unwrap_or_else(|_| path.clone());
|
||||
if seen.insert(key) {
|
||||
self.fcx.collection.load_fonts_from_paths([path]);
|
||||
}
|
||||
}
|
||||
self.ps_cache.clear();
|
||||
self.face_cache.clear();
|
||||
|
|
@ -560,6 +568,54 @@ pub(crate) fn collect_font_files(dir: &std::path::Path, depth: u32, out: &mut Ve
|
|||
}
|
||||
}
|
||||
|
||||
/// Fontconfig knows about configured font directories that are not in our portable path list.
|
||||
/// Keep this optional and shell-free: directory scans remain the fallback when `fc-list` is not
|
||||
/// installed or Fontconfig cannot enumerate fonts.
|
||||
#[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
|
||||
fn fontconfig_font_paths() -> Vec<std::path::PathBuf> {
|
||||
let Ok(output) = std::process::Command::new("fc-list").arg("--format=%{file}\\n").output() else {
|
||||
return Vec::new();
|
||||
};
|
||||
if !output.status.success() {
|
||||
return Vec::new();
|
||||
}
|
||||
fontconfig_paths_from_output(&output.stdout).into_iter().filter(|path| path.is_file()).collect()
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
|
||||
fn fontconfig_paths_from_output(output: &[u8]) -> Vec<std::path::PathBuf> {
|
||||
use std::ffi::OsString;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
output
|
||||
.split(|byte| *byte == b'\n')
|
||||
.filter(|line| !line.is_empty())
|
||||
.map(|line| std::path::PathBuf::from(OsString::from_vec(line.to_vec())))
|
||||
.filter(|path| {
|
||||
path.is_absolute()
|
||||
&& path.extension().and_then(|ext| ext.to_str()).is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "ttf" | "otf" | "ttc" | "otc"))
|
||||
})
|
||||
.map(|path| path.canonicalize().unwrap_or(path))
|
||||
.filter(|path| seen.insert(path.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_os = "linux", not(target_arch = "wasm32")))]
|
||||
mod fontconfig_tests {
|
||||
use super::fontconfig_paths_from_output;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn parses_unique_absolute_font_paths_from_fontconfig() {
|
||||
let paths = fontconfig_paths_from_output(
|
||||
b"/usr/share/fonts/example.ttf\nrelative.otf\n/usr/share/fonts/README.txt\n/usr/local/fonts/collection.OTC\n/usr/share/fonts/example.ttf\n\n",
|
||||
);
|
||||
|
||||
assert_eq!(paths, vec![PathBuf::from("/usr/share/fonts/example.ttf"), PathBuf::from("/usr/local/fonts/collection.OTC")]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn system_font_dirs() -> Vec<std::path::PathBuf> {
|
||||
use std::path::PathBuf;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
//! Photocraft's type engine.
|
||||
//!
|
||||
//! * [`fonts::FontDb`]: bundled fonts (Inter, JetBrains Mono; always available, including on the
|
||||
//! web), optional system fonts from a directory scan (no fontconfig), user font data, TrueType
|
||||
//! collections, PostScript-name lookup. Builds made with the optional craft-fonts input
|
||||
//! web), optional system fonts from platform directory scans plus Fontconfig's Linux file list,
|
||||
//! user font data, TrueType collections, PostScript-name lookup. Builds made with the optional
|
||||
//! craft-fonts input
|
||||
//! (`CRAFT_FONTS_DIR`, see [`craft_fonts`]) also carry its Japanese fonts as fallbacks.
|
||||
//! * [`layout`]: shaping and line layout with [parley] (HarfRust shaping, bidi, line breaking),
|
||||
//! point and paragraph (box) text, per-run styles, Photoshop leading/indent/spacing rules.
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ pub fn no_embedded(_: CjkScript) -> Vec<&'static CraftFont> {
|
|||
impl Sources {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn system() -> Self {
|
||||
cjk::start_fontconfig_cjk_scan();
|
||||
Self {
|
||||
locale: || match crate::i18n::current().code() {
|
||||
code @ ("ja" | "ko" | "zh-hans" | "zh-hant") => Some(code.to_string()),
|
||||
|
|
@ -90,6 +91,18 @@ impl Sources {
|
|||
}
|
||||
}
|
||||
|
||||
/// Headless test frame pumps do not wait for `request_repaint_after`, so let the optional
|
||||
/// Fontconfig worker finish before asserting on installed system fonts.
|
||||
#[cfg(all(test, target_os = "linux"))]
|
||||
pub(crate) fn wait_for_fontconfig_cjk_scan() {
|
||||
cjk::start_fontconfig_cjk_scan();
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
|
||||
while cjk::fontconfig_cjk_scan_pending() && std::time::Instant::now() < deadline {
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
assert!(!cjk::fontconfig_cjk_scan_pending(), "Fontconfig CJK lookup did not finish within 10 seconds");
|
||||
}
|
||||
|
||||
/// The lazy loader's state: which scripts were tried and which files were read.
|
||||
pub struct CjkFallback {
|
||||
sources: Sources,
|
||||
|
|
@ -139,14 +152,20 @@ impl CjkFallback {
|
|||
if self.tried.contains(&s) {
|
||||
continue;
|
||||
}
|
||||
self.tried.push(s);
|
||||
if let Some(f) = self.load_embedded(s) {
|
||||
self.tried.push(s);
|
||||
return Some(f);
|
||||
}
|
||||
let fontconfig_pending = cjk::fontconfig_cjk_scan_pending();
|
||||
let files = (self.sources.files)(s);
|
||||
if let Some(f) = self.load_first(&files) {
|
||||
self.tried.push(s);
|
||||
return Some(f);
|
||||
}
|
||||
if fontconfig_pending {
|
||||
return None;
|
||||
}
|
||||
self.tried.push(s);
|
||||
}
|
||||
if !self.last_resort_tried {
|
||||
self.last_resort_tried = true;
|
||||
|
|
@ -211,7 +230,7 @@ impl CjkFallback {
|
|||
continue;
|
||||
}
|
||||
let Some(bytes) = read_font(&f.path) else { continue };
|
||||
let index = cjk::face_index_for_family(&bytes, f.family);
|
||||
let index = cjk::face_index_for_family(&bytes, &f.family);
|
||||
self.loaded.push(f.path.clone());
|
||||
let name = format!("{FONT_PREFIX}-{}", self.registered.len());
|
||||
log::info!("UI font fallback: registered {} (face {index}) as {name}", f.path.display());
|
||||
|
|
@ -348,6 +367,8 @@ impl egui::Plugin for CjkFontPlugin {
|
|||
if let Some((name, data)) = font {
|
||||
add_to_all_families(ctx, name, data);
|
||||
ctx.request_repaint();
|
||||
} else if cjk::classify(c).is_some() && cjk::fontconfig_cjk_scan_pending() {
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(50));
|
||||
} else if !self.0.exhausted() || !self.0.scripts_exhausted() {
|
||||
// Nothing readable for this script; try the next one on the next frame.
|
||||
ctx.request_repaint();
|
||||
|
|
@ -375,7 +396,6 @@ pub fn install_with(ctx: &egui::Context, sources: Sources) {
|
|||
ctx.add_plugin(CjkFontPlugin(fallback));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod tests {
|
||||
|
|
@ -447,7 +467,7 @@ mod tests {
|
|||
}
|
||||
|
||||
fn fake(name: &str) -> FontFile {
|
||||
FontFile { path: fake_dir().join(name), family: "" }
|
||||
FontFile { path: fake_dir().join(name), family: String::new() }
|
||||
}
|
||||
|
||||
fn fake_sources(locale: fn() -> Option<String>) -> Sources {
|
||||
|
|
@ -805,6 +825,8 @@ mod tests {
|
|||
/// machine has no font for it, e.g. CI Linux without Noto CJK).
|
||||
#[test]
|
||||
fn system_fonts_cover_cjk_samples() {
|
||||
#[cfg(target_os = "linux")]
|
||||
wait_for_fontconfig_cjk_scan();
|
||||
let available = |s: CjkScript| cjk::font_files(s).iter().any(|f| f.path.is_file());
|
||||
let samples = [
|
||||
("图层", CjkScript::SimplifiedChinese),
|
||||
|
|
|
|||
|
|
@ -166,6 +166,8 @@ fn cjk_font_order_follows_the_selected_language() {
|
|||
#[test]
|
||||
fn korean_glyphs_remain_available_after_repeated_language_switches() {
|
||||
use photocraft_text::cjk::{self, CjkChar, CjkScript};
|
||||
#[cfg(target_os = "linux")]
|
||||
crate::cjk_fonts::wait_for_fontconfig_cjk_scan();
|
||||
if !cjk::font_files(CjkScript::Korean).iter().any(|font| font.path.is_file()) {
|
||||
eprintln!("skipping: no installed Korean font");
|
||||
return;
|
||||
|
|
|
|||
Loading…
Reference in New Issue