use crate::font::UNITS_PER_EM;
use crate::font::outline::OutlinePath;
use kurbo::BezPath;
use skrifa::instance::{LocationRef, Size};
use skrifa::metrics::GlyphMetrics;
use skrifa::outline::{DrawSettings, Engine, HintingInstance, HintingOptions, Target};
use skrifa::raw::TableProvider;
use skrifa::raw::ps::cff::{CffFontRef, Subfont, charset::Charset, v1::Cff};
use skrifa::raw::ps::string::Sid;
use skrifa::raw::ps::type1::Type1Font;
use skrifa::raw::tables::post::DEFAULT_GLYPH_NAMES;
use skrifa::raw::{FontData as ReadFontData, FontRead};
use skrifa::{FontRef, GlyphId, MetadataProvider, OutlineGlyphCollection};
use std::collections::HashMap;
use std::fmt::{Debug, Formatter};
use std::sync::Arc;
use yoke::{Yoke, Yokeable};
type FontData = Arc<dyn AsRef<[u8]> + Send + Sync>;
type OpenTypeFontYoke = Yoke<OTFYoke<'static>, FontData>;
type CffFontYoke = Yoke<CFFYoke<'static>, FontData>;
#[derive(Clone)]
pub(crate) struct Type1FontBlob(Arc<Type1Font>);
impl Debug for Type1FontBlob {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "Type1 Font {{ .. }}")
}
}
impl Type1FontBlob {
pub(crate) fn new(data: FontData) -> Option<Self> {
let table = Type1Font::new(data.as_ref().as_ref()).ok()?;
Some(Self(Arc::new(table)))
}
pub(crate) fn table(&self) -> &Type1Font {
self.0.as_ref()
}
pub(crate) fn outline_glyph(&self, gid: GlyphId) -> BezPath {
let mut path = OutlinePath::new();
let _ = self.table().draw(gid, Some(UNITS_PER_EM), &mut path);
path.take()
}
}
#[derive(Clone)]
pub(crate) struct CffFontBlob(Arc<CffFontYoke>);
impl Debug for CffFontBlob {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "Type1 Font {{ .. }}")
}
}
impl CffFontBlob {
pub(crate) fn new(data: FontData) -> Option<Self> {
let font = CffFontRef::new(data.as_ref().as_ref(), 0, None).ok()?;
let cff = Cff::read(ReadFontData::new(data.as_ref().as_ref())).ok()?;
let charset = font.charset();
let subfonts = (0..font.num_subfonts())
.map(|index| font.subfont(index, &[]).ok())
.collect::<Option<Vec<_>>>()?;
let yoke = Yoke::<CFFYoke<'static>, FontData>::attach_to_cart(data.clone(), |data| {
let bytes = data.as_ref();
let font = CffFontRef::new(bytes, 0, None).unwrap();
let cff = Cff::read(ReadFontData::new(bytes)).unwrap();
let charset = font.charset();
let subfonts = (0..font.num_subfonts())
.map(|index| font.subfont(index, &[]).unwrap())
.collect();
CFFYoke {
font,
cff,
charset,
subfonts,
}
});
let _ = (cff, charset, subfonts);
Some(Self(Arc::new(yoke)))
}
pub(crate) fn font_data(&self) -> FontData {
self.0.backing_cart().clone()
}
pub(crate) fn font(&self) -> &CffFontRef<'_> {
&self.0.as_ref().get().font
}
fn charset(&self) -> Option<&Charset<'_>> {
self.0.as_ref().get().charset.as_ref()
}
fn subfont(&self, glyph: GlyphId) -> Option<&Subfont> {
let index = self.font().subfont_index(glyph)? as usize;
self.0.as_ref().get().subfonts.get(index)
}
pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath {
let mut path = OutlinePath::new();
let Some(subfont) = self.subfont(glyph) else {
return BezPath::new();
};
let _ = self
.font()
.draw(subfont, glyph, &[], Some(UNITS_PER_EM), &mut path);
path.take()
}
pub(crate) fn glyph_names(&self) -> Vec<(GlyphId, String)> {
let Some(charset) = self.charset() else {
return Vec::new();
};
charset
.iter()
.filter_map(|(gid, sid)| {
let bytes = self.0.as_ref().get().cff.string(sid)?;
let name = std::str::from_utf8(bytes).ok()?.to_string();
Some((gid, name))
})
.collect()
}
pub(crate) fn glyph_index_by_name(&self, name: &str) -> Option<GlyphId> {
self.charset()?.iter().find_map(|(gid, sid)| {
let bytes = self.0.as_ref().get().cff.string(sid)?;
(bytes == name.as_bytes()).then_some(gid)
})
}
pub(crate) fn glyph_index_by_cid(&self, cid: u16) -> Option<GlyphId> {
self.charset()?.glyph_id(Sid::new(cid)).ok()
}
pub(crate) fn glyph_index(&self, code: u8) -> Option<GlyphId> {
self.font()
.encoding()
.and_then(|encoding| encoding.map(code))
}
pub(crate) fn num_glyphs(&self) -> u32 {
self.font().num_glyphs()
}
pub(crate) fn is_cid(&self) -> bool {
self.font().is_cid()
}
}
#[derive(Clone)]
pub(crate) struct OpenTypeFontBlob {
yoke: Arc<OpenTypeFontYoke>,
cff_blob: Option<CffFontBlob>,
}
impl Debug for OpenTypeFontBlob {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "OpenType Font {{ .. }}")
}
}
impl OpenTypeFontBlob {
pub(crate) fn new(data: FontData, index: u32) -> Option<Self> {
let f = FontRef::from_index(data.as_ref().as_ref(), index).ok()?;
let invalid = f.post().is_ok_and(|p| {
!matches!(
p.version().to_major_minor(),
(1, 0) | (2, 0) | (2, 5) | (3, 0)
)
});
if invalid {
return None;
}
let cff_blob = f
.cff()
.ok()
.map(|cff| Arc::new(cff.offset_data().as_ref().to_vec()) as FontData)
.and_then(CffFontBlob::new);
let font_ref_yoke =
Yoke::<OTFYoke<'static>, FontData>::attach_to_cart(data.clone(), |data| {
let font_ref = FontRef::from_index(data.as_ref(), index).unwrap();
let hinting_instance = if font_ref.outline_glyphs().require_interpreter() {
HintingInstance::new(
&font_ref.outline_glyphs(),
Size::new(UNITS_PER_EM),
LocationRef::default(),
HintingOptions {
engine: Engine::Interpreter,
target: Target::Mono,
},
)
.ok()
} else {
None
};
OTFYoke {
font_ref: font_ref.clone(),
outline_glyphs: font_ref.outline_glyphs(),
hinting_instance,
glyph_metrics: font_ref
.glyph_metrics(Size::new(UNITS_PER_EM), LocationRef::default()),
}
});
Some(Self {
yoke: Arc::new(font_ref_yoke),
cff_blob,
})
}
pub(crate) fn font_data(&self) -> FontData {
self.yoke.backing_cart().clone()
}
pub(crate) fn font_ref(&self) -> &FontRef<'_> {
&self.yoke.as_ref().get().font_ref
}
pub(crate) fn glyph_metrics(&self) -> &GlyphMetrics<'_> {
&self.yoke.as_ref().get().glyph_metrics
}
pub(crate) fn glyph_names(&self) -> HashMap<String, GlyphId> {
let mut glyph_names = HashMap::new();
let Ok(post) = self.font_ref().post() else {
return glyph_names;
};
match post.version().to_major_minor() {
(1, 0) => {
for (gid, name) in DEFAULT_GLYPH_NAMES.iter().enumerate() {
glyph_names.insert((*name).to_string(), GlyphId::new(gid as u32));
}
}
(2, 0) => {
let Some(name_indices) = post.glyph_name_index() else {
return glyph_names;
};
let Some(string_data) = post.string_data() else {
return glyph_names;
};
let custom_names = string_data
.iter()
.map(|entry| entry.ok().map(|name| name.as_str()))
.collect::<Vec<_>>();
for (gid, idx) in name_indices.iter().enumerate() {
let idx = idx.get() as usize;
if let Some(name) = DEFAULT_GLYPH_NAMES.get(idx).copied().or_else(|| {
custom_names
.get(idx.saturating_sub(DEFAULT_GLYPH_NAMES.len()))
.copied()
.flatten()
}) {
glyph_names.insert(name.to_string(), GlyphId::new(gid as u32));
}
}
}
_ => {}
}
glyph_names
}
fn outline_glyphs(&self) -> &OutlineGlyphCollection<'_> {
&self.yoke.as_ref().get().outline_glyphs
}
pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath {
if let Some(cff_blob) = self.cff_blob.as_ref() {
return cff_blob.outline_glyph(glyph);
}
let mut path = OutlinePath::new();
let draw_settings = if let Some(instance) = self.yoke.get().hinting_instance.as_ref() {
DrawSettings::hinted(instance, false)
} else {
DrawSettings::unhinted(Size::new(UNITS_PER_EM), LocationRef::default())
};
let Some(outline) = self.outline_glyphs().get(glyph) else {
return BezPath::new();
};
let _ = outline.draw(draw_settings, &mut path);
path.take()
}
}
#[derive(Yokeable, Clone)]
struct OTFYoke<'a> {
font_ref: FontRef<'a>,
glyph_metrics: GlyphMetrics<'a>,
hinting_instance: Option<HintingInstance>,
outline_glyphs: OutlineGlyphCollection<'a>,
}
#[derive(Yokeable, Clone)]
struct CFFYoke<'a> {
font: CffFontRef<'a>,
cff: Cff<'a>,
charset: Option<Charset<'a>>,
subfonts: Vec<Subfont>,
}