use std::ffi::CStr;
use std::path::{Path, PathBuf};
use std::sync::RwLock;
use mupdf_sys::*;
use once_cell::sync::Lazy;
use crate::{context, Buffer, CjkFontOrdering, Font};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct FontHints {
pub bold: bool,
pub italic: bool,
pub serif: bool,
pub needs_exact_metrics: bool,
}
pub trait FontLoader: Send + Sync + 'static {
fn load_font(&self, name: &str, hints: FontHints) -> Option<Font> {
let _ = (name, hints);
None
}
fn load_cjk_font(&self, name: &str, ordering: CjkFontOrdering, serif: bool) -> Option<Font> {
let _ = (name, ordering, serif);
None
}
fn load_fallback_font(&self, script: u32, language: u32, hints: FontHints) -> Option<Font> {
let _ = (script, language, hints);
None
}
}
#[cfg(target_os = "android")]
fn default_loader() -> Option<Box<dyn FontLoader>> {
Some(Box::new(AndroidFontLoader))
}
#[cfg(not(target_os = "android"))]
fn default_loader() -> Option<Box<dyn FontLoader>> {
None
}
static FONT_LOADER: Lazy<RwLock<Option<Box<dyn FontLoader>>>> =
Lazy::new(|| RwLock::new(default_loader()));
pub fn set_font_loader(loader: impl FontLoader) {
*FONT_LOADER.write().unwrap() = Some(Box::new(loader));
}
const BUILT_IN_LOADERS: &[&dyn FontLoader] = &[
#[cfg(feature = "bundled-fonts-runtime")]
&crate::bundled_font::BundledFontLoader,
#[cfg(all(
feature = "system-fonts",
not(target_arch = "wasm32"),
not(target_os = "android")
))]
&crate::system_font::SystemFontLoader,
];
fn with_user_loader(f: impl FnOnce(&dyn FontLoader) -> Option<Font>) -> Option<Font> {
let guard = FONT_LOADER.read().ok()?;
let loader = guard.as_ref()?.as_ref();
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(loader))).unwrap_or(None)
}
pub(crate) fn dispatch(f: impl Fn(&dyn FontLoader) -> Option<Font>) -> Option<Font> {
if let Some(font) = with_user_loader(&f) {
return Some(font);
}
BUILT_IN_LOADERS.iter().find_map(|loader| f(*loader))
}
pub(crate) fn dispatch_cjk(
name: &str,
ordering: Option<CjkFontOrdering>,
serif: bool,
) -> Option<Font> {
if let Some(ordering) = ordering {
if let Some(font) = with_user_loader(|l| l.load_cjk_font(name, ordering, serif)) {
return Some(font);
}
}
if !name.is_empty() {
if let Some(font) = dispatch(|l| l.load_font(name, FontHints::default())) {
return Some(font);
}
}
let ordering = ordering?;
BUILT_IN_LOADERS
.iter()
.find_map(|l| l.load_cjk_font(name, ordering, serif))
}
#[derive(Debug, Default, Clone, Copy)]
pub struct AndroidFontLoader;
const JP: i32 = 0;
const KR: i32 = 1;
const SC: i32 = 2;
const TC: i32 = 3;
fn probe(a: &str, b: &str, c: &str) -> Option<PathBuf> {
for ext in ["ttf", "otf", "ttc"] {
let path = PathBuf::from(format!("/system/fonts/{a}{b}{c}.{ext}"));
if path.exists() {
return Some(path);
}
}
None
}
fn font_from_file(path: &Path, index: i32) -> Option<Font> {
let data = std::fs::read(path).ok()?;
let buffer = Buffer::from_bytes(&data).ok()?;
let inner = unsafe {
ffi_try!(mupdf_new_font_from_buffer(
context(),
std::ptr::null(),
index,
buffer.inner
))
}
.ok()?;
Some(unsafe { Font::from_raw(inner) })
}
fn load_noto(a: &str, b: &str, c: &str, index: i32) -> Option<Font> {
font_from_file(&probe(a, b, c)?, index)
}
fn load_noto_cjk(lang: i32) -> Option<Font> {
load_noto("NotoSerif", "CJK", "-Regular", lang)
.or_else(|| load_noto("NotoSans", "CJK", "-Regular", lang))
.or_else(|| load_noto("DroidSans", "Fallback", "", 0))
}
fn load_noto_arabic() -> Option<Font> {
load_noto("Noto", "Naskh", "-Regular", 0)
.or_else(|| load_noto("Noto", "NaskhArabic", "-Regular", 0))
.or_else(|| load_noto("Droid", "Naskh", "-Regular", 0))
.or_else(|| load_noto("NotoSerif", "Arabic", "-Regular", 0))
.or_else(|| load_noto("NotoSans", "Arabic", "-Regular", 0))
.or_else(|| load_noto("DroidSans", "Arabic", "-Regular", 0))
}
fn load_noto_try(stem: &str) -> Option<Font> {
load_noto("NotoSerif", stem, "-Regular", 0)
.or_else(|| load_noto("NotoSans", stem, "-Regular", 0))
.or_else(|| load_noto("DroidSans", stem, "-Regular", 0))
}
impl FontLoader for AndroidFontLoader {
fn load_font(&self, name: &str, hints: FontHints) -> Option<Font> {
let style = match (hints.bold, hints.italic) {
(true, true) => "-BoldItalic",
(true, false) => "-Bold",
(false, true) => "-Italic",
(false, false) => "-Regular",
};
if name.eq_ignore_ascii_case("Helvetica")
|| name.eq_ignore_ascii_case("Arial")
|| name.contains("Helvetica")
|| name.contains("Arial")
{
return load_noto("Roboto", "", style, 0)
.or_else(|| load_noto("NotoSans", "", style, 0))
.or_else(|| load_noto("DroidSans", "", style, 0));
}
if name.eq_ignore_ascii_case("Times")
|| name.eq_ignore_ascii_case("Times-Roman")
|| name.contains("Times")
{
return load_noto("NotoSerif", "", style, 0)
.or_else(|| load_noto("RobotoSerif", "", style, 0))
.or_else(|| load_noto("DroidSerif", "", style, 0));
}
if name.eq_ignore_ascii_case("Courier") || name.contains("Courier") {
return load_noto("DroidSans", "Mono", "", 0)
.or_else(|| load_noto("NotoSans", "Mono", "-Regular", 0));
}
if name.eq_ignore_ascii_case("Symbol")
|| name.eq_ignore_ascii_case("ZapfDingbats")
|| name.contains("Symbol")
|| name.contains("Dingbats")
{
return load_noto("NotoSans", "Symbols", "-Regular", 0)
.or_else(|| load_noto("NotoSans", "Symbols2", "-Regular", 0));
}
None
}
fn load_cjk_font(&self, _name: &str, ordering: CjkFontOrdering, _serif: bool) -> Option<Font> {
match ordering {
CjkFontOrdering::AdobeCns => load_noto_cjk(TC),
CjkFontOrdering::AdobeGb => load_noto_cjk(SC),
CjkFontOrdering::AdobeJapan => load_noto_cjk(JP),
CjkFontOrdering::AdobeKorea => load_noto_cjk(KR),
}
}
fn load_fallback_font(&self, script: u32, language: u32, _hints: FontHints) -> Option<Font> {
let stem =
unsafe { fz_lookup_noto_stem_from_script(context(), script as i32, language as i32) };
if stem.is_null() {
return None;
}
let stem = unsafe { CStr::from_ptr(stem) }.to_str().ok()?;
match stem {
"JP" => load_noto_cjk(JP),
"KR" => load_noto_cjk(KR),
"SC" => load_noto_cjk(SC),
"TC" => load_noto_cjk(TC),
"Naskh" => load_noto_arabic(),
stem => load_noto_try(stem),
}
}
}
#[cfg(test)]
mod tests {
use std::ffi::CString;
use super::*;
struct TestLoader;
impl FontLoader for TestLoader {
fn load_font(&self, name: &str, hints: FontHints) -> Option<Font> {
if name == "MupdfRsFontLoaderPanic" {
panic!("font loader panicked");
}
(name == "MupdfRsFontLoaderTest" && hints.bold && !hints.italic).then(|| {
let data = include_bytes!("../tests/files/custom.ttf");
Font::from_bytes("MupdfRsFontLoaderTest", data).unwrap()
})
}
}
#[test]
fn user_font_loader_is_consulted_first() {
set_font_loader(TestLoader);
let ctx = crate::context();
let name = CString::new("MupdfRsFontLoaderTest").unwrap();
let font = unsafe { crate::system_font::load_system_font(ctx, name.as_ptr(), 1, 0, 0) };
assert!(!font.is_null());
let actual = unsafe { CStr::from_ptr(fz_font_name(ctx, font)) }
.to_str()
.unwrap()
.to_owned();
unsafe { fz_drop_font(ctx, font) };
assert_eq!(actual, "MupdfRsFontLoaderTest");
let name = CString::new("MupdfRsFontLoaderPanic").unwrap();
let font = unsafe { crate::system_font::load_system_font(ctx, name.as_ptr(), 1, 0, 0) };
if !font.is_null() {
unsafe { fz_drop_font(ctx, font) };
}
}
}