use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, OnceLock};
use parley::fontique::{Blob, Collection, CollectionOptions};
use parley::{FontContext, LayoutContext};
use swash::scale::image::Content;
use swash::scale::{Render, ScaleContext, Source, StrikeWith};
use swash::zeno::{Angle, Format, Transform};
use crate::error::{Error, Result};
use crate::layout::{GlyphFont, Ink};
const BUNDLED: &[&[u8]] = &[
include_bytes!("../assets/fonts/NotoSansSC.ttf.zst"),
include_bytes!("../assets/fonts/JetBrainsMono.ttf.zst"),
include_bytes!("../assets/fonts/JetBrainsMono-Italic.ttf.zst"),
];
const ZSTD_MAGIC: [u8; 4] = [0x28, 0xb5, 0x2f, 0xfd];
#[derive(Clone)]
pub struct FontHandle(Arc<Inner>);
struct Inner {
fonts: Mutex<FontContext>,
layouts: Mutex<LayoutContext<Ink>>,
raster: Mutex<RasterState>,
}
struct RasterState {
scale: ScaleContext,
cache: HashMap<GlyphKey, Option<GlyphImage>>,
}
#[derive(Clone, PartialEq, Eq, Hash)]
struct GlyphKey {
blob: u64,
index: u32,
glyph: u16,
size: u32,
coords: Vec<i16>,
skew: u32,
embolden: bool,
}
pub(crate) struct GlyphImage {
pub left: i32,
pub top: i32,
pub width: u32,
pub height: u32,
pub color: bool,
pub data: Vec<u8>,
}
pub(crate) struct GlyphRaster<'a>(&'a mut RasterState);
impl GlyphRaster<'_> {
pub(crate) fn image(&mut self, gf: &GlyphFont, glyph: u16) -> Option<&GlyphImage> {
let key = GlyphKey {
blob: gf.font.data.id(),
index: gf.font.index,
glyph,
size: gf.size.to_bits(),
coords: gf.coords.clone(),
skew: gf.skew.map_or(0, f32::to_bits),
embolden: gf.embolden,
};
if !self.0.cache.contains_key(&key) {
let img = raster(&mut self.0.scale, gf, glyph);
self.0.cache.insert(key.clone(), img);
}
self.0.cache.get(&key).and_then(|o| o.as_ref())
}
}
fn raster(cx: &mut ScaleContext, gf: &GlyphFont, glyph: u16) -> Option<GlyphImage> {
let font_ref = swash::FontRef::from_index(gf.font.data.as_ref(), gf.font.index as usize)?;
let mut scaler = cx.builder(font_ref).size(gf.size).hint(false).normalized_coords(&gf.coords).build();
let mut render = Render::new(&[Source::ColorOutline(0), Source::ColorBitmap(StrikeWith::BestFit), Source::Outline]);
render.format(Format::Alpha);
if let Some(deg) = gf.skew {
render.transform(Some(Transform::skew(Angle::from_degrees(deg), Angle::ZERO)));
}
if gf.embolden {
render.embolden(gf.size * 0.02);
}
let img = render.render(&mut scaler, glyph)?;
let (width, height) = (img.placement.width, img.placement.height);
let (color, data) = match img.content {
Content::Color => (true, img.data),
Content::Mask => (false, img.data),
Content::SubpixelMask => {
(false, img.data.chunks_exact(3).map(|c| ((c[0] as u16 + c[1] as u16 + c[2] as u16) / 3) as u8).collect())
}
};
Some(GlyphImage { left: img.placement.left, top: img.placement.top, width, height, color, data })
}
impl FontHandle {
pub fn builder() -> FontStackBuilder {
FontStackBuilder::new()
}
pub fn shared_default() -> FontHandle {
static DEFAULT: OnceLock<FontHandle> = OnceLock::new();
DEFAULT
.get_or_init(|| {
FontHandle::builder().bundled().system().build().unwrap_or_else(|_| {
FontHandle::from_collection(Collection::new(CollectionOptions {
shared: false,
system_fonts: true,
}))
})
})
.clone()
}
fn from_collection(collection: Collection) -> FontHandle {
let fonts = FontContext { collection, source_cache: Default::default() };
FontHandle(Arc::new(Inner {
fonts: Mutex::new(fonts),
layouts: Mutex::new(LayoutContext::new()),
raster: Mutex::new(RasterState { scale: ScaleContext::new(), cache: HashMap::new() }),
}))
}
pub(crate) fn with_layout<R>(&self, f: impl FnOnce(&mut FontContext, &mut LayoutContext<Ink>) -> R) -> R {
let mut fonts = self.0.fonts.lock().unwrap_or_else(|e| e.into_inner());
let mut layouts = self.0.layouts.lock().unwrap_or_else(|e| e.into_inner());
f(&mut fonts, &mut layouts)
}
pub(crate) fn with_raster<R>(&self, f: impl FnOnce(&mut GlyphRaster) -> R) -> R {
let mut raster = self.0.raster.lock().unwrap_or_else(|e| e.into_inner());
f(&mut GlyphRaster(&mut raster))
}
}
pub struct FontStackBuilder {
bundled: bool,
datas: Vec<Vec<u8>>,
dirs: Vec<PathBuf>,
system: bool,
}
impl FontStackBuilder {
fn new() -> Self {
Self { bundled: true, datas: Vec::new(), dirs: Vec::new(), system: false }
}
pub fn bundled(mut self) -> Self {
self.bundled = true;
self
}
pub fn no_bundled(mut self) -> Self {
self.bundled = false;
self
}
pub fn data(mut self, bytes: impl Into<Vec<u8>>) -> Self {
self.datas.push(bytes.into());
self
}
pub fn dir(mut self, p: impl Into<PathBuf>) -> Self {
self.dirs.push(p.into());
self
}
pub fn system(mut self) -> Self {
self.system = true;
self
}
pub fn build(self) -> Result<FontHandle> {
let mut collection = Collection::new(CollectionOptions { shared: false, system_fonts: self.system });
let mut registered = false;
let mut register = |collection: &mut Collection, raw: Vec<u8>| {
registered |= !collection.register_fonts(Blob::from(raw), None).is_empty();
};
if self.bundled {
for z in BUNDLED {
register(&mut collection, unzstd(z)?);
}
}
for d in self.datas {
if d.starts_with(&ZSTD_MAGIC) {
register(&mut collection, unzstd(&d)?);
} else {
register(&mut collection, d);
}
}
let mut stack: Vec<PathBuf> = self.dirs.clone();
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else { continue };
for e in entries.flatten() {
let p = e.path();
if p.is_dir() {
stack.push(p);
continue;
}
let is_font = p
.extension()
.and_then(|x| x.to_str())
.is_some_and(|x| matches!(x.to_ascii_lowercase().as_str(), "ttf" | "otf" | "ttc" | "otc"));
if is_font {
if let Ok(raw) = std::fs::read(&p) {
register(&mut collection, raw);
}
}
}
}
if !registered && !self.system {
return Err(Error::FontLoad("字体栈为空(未启用任何字体来源)".into()));
}
Ok(FontHandle::from_collection(collection))
}
}
fn unzstd(data: &[u8]) -> Result<Vec<u8>> {
use std::io::Read;
let mut dec =
ruzstd::decoding::StreamingDecoder::new(data).map_err(|e| Error::FontLoad(format!("内置字体解压失败:{e}")))?;
let mut out = Vec::new();
dec.read_to_end(&mut out).map_err(|e| Error::FontLoad(format!("内置字体解压失败:{e}")))?;
Ok(out)
}