use kurbo::Affine;
use skrifa::{
instance::{LocationRef, Size},
outline::DrawSettings,
raw::{
tables::{
gsub::{ExtensionSubstFormat1, Gsub, SingleSubst, SubstitutionSubtables},
kern::{self},
layout::Subtables,
},
FontRef, TableProvider,
},
GlyphId, MetadataProvider, Tag,
};
use std::collections::HashMap;
use std::fmt::Write;
use std::result::Result;
use crate::{pathstyle::SvgPathStyle, pens::SvgPathPen, xml_element::XmlElement};
const ARAB_SCRIPT_TAG: Tag = Tag::new(b"arab");
const INIT_FEATURE_TAG: Tag = Tag::new(b"init");
const MEDI_FEATURE_TAG: Tag = Tag::new(b"medi");
const FINA_FEATURE_TAG: Tag = Tag::new(b"fina");
pub fn generate_svg_font(font: &FontRef, font_id: &str) -> Result<Vec<u8>, std::fmt::Error> {
let mut font_el = create_font_element(font, font_id, LocationRef::default());
let gsub_subs = GsubSubs::new(font);
let charmap = font.charmap();
let ranges = [
(0x0020, 0x007e),
(0x00a0, 0x00ff),
(0x2013, 0x2013),
(0x2014, 0x2014),
(0x2018, 0x2018),
(0x2019, 0x2019),
(0x201a, 0x201a),
(0x201c, 0x201c),
(0x201d, 0x201d),
(0x201e, 0x201e),
(0x2022, 0x2022),
(0x2039, 0x2039),
(0x203a, 0x203a),
];
for (start, end) in ranges {
for codepoint in start..=end {
if let Some(glyph_id) = charmap.map(codepoint) {
if glyph_id.to_u32() == 0 {
continue;
}
add_glyph(&mut font_el, font, codepoint, glyph_id, &gsub_subs);
}
}
}
add_kerning(&mut font_el, font);
let svg = XmlElement::new("svg")
.with_attribute("xmlns", "http://www.w3.org/2000/svg")
.with_child(XmlElement::new("defs").with_child(font_el));
let mut svg_string = String::new();
writeln!(svg_string, "<?xml version=\"1.0\" standalone=\"no\"?>")?;
writeln!(
svg_string,
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">"
)?;
writeln!(svg_string, "{:2}", svg)?;
Ok(svg_string.into_bytes())
}
fn get_panose_str(font: &FontRef) -> Option<String> {
font.table_data(Tag::new(b"OS/2")).and_then(|data| {
if data.as_bytes().len() >= 42 {
let panose_bytes = &data.as_bytes()[32..42];
let panose_str = panose_bytes
.iter()
.map(|b| b.to_string())
.collect::<Vec<_>>()
.join(" ");
Some(panose_str)
} else {
None
}
})
}
fn create_font_element(font: &FontRef, id: &str, location: LocationRef) -> XmlElement {
let metrics = font.metrics(Size::unscaled(), location);
let avg_char_width = font.os2().map(|os2| os2.x_avg_char_width()).unwrap_or(0);
let units_per_em = metrics.units_per_em;
let ascent = metrics.ascent;
let descent = metrics.descent;
let font_family = font
.localized_strings(skrifa::string::StringId::FAMILY_NAME)
.english_or_first()
.map(|s| s.to_string())
.unwrap_or_else(|| id.to_string());
let mut font_face = XmlElement::new("font-face")
.with_attribute("font-family", font_family)
.with_attribute("units-per-em", units_per_em);
if let Some(panose_str) = get_panose_str(font) {
font_face.add_attribute("panose-1", panose_str);
}
font_face.add_attribute("ascent", ascent);
font_face.add_attribute("descent", descent);
font_face.add_attribute("alphabetic", "0");
XmlElement::new("font")
.with_attribute("id", id)
.with_attribute("horiz-adv-x", avg_char_width)
.with_child(font_face)
}
fn add_glyph(
font_el: &mut XmlElement,
font: &FontRef,
codepoint: u32,
glyph_id: GlyphId,
gsub_subs: &GsubSubs,
) {
font_el.add_child(create_glyph_element(font, codepoint, glyph_id, None));
if let Some(sub_gid) = gsub_subs.init.get(&glyph_id) {
if sub_gid.to_u32() != 0 {
font_el.add_child(create_glyph_element(
font,
codepoint,
*sub_gid,
Some("initial"),
));
}
}
if let Some(sub_gid) = gsub_subs.medi.get(&glyph_id) {
if sub_gid.to_u32() != 0 {
font_el.add_child(create_glyph_element(
font,
codepoint,
*sub_gid,
Some("medial"),
));
}
}
if let Some(sub_gid) = gsub_subs.fina.get(&glyph_id) {
if sub_gid.to_u32() != 0 {
font_el.add_child(create_glyph_element(
font,
codepoint,
*sub_gid,
Some("terminal"),
));
}
}
}
fn create_glyph_element(
font: &FontRef,
codepoint: u32,
glyph_id: GlyphId,
arabic_form: Option<&str>,
) -> XmlElement {
let glyph_name_map = font.glyph_names();
let glyph_name = glyph_name_map
.get(glyph_id)
.map(|n| n.as_str().to_string())
.unwrap_or_default();
let advance_width = font
.glyph_metrics(Size::unscaled(), LocationRef::default())
.advance_width(glyph_id)
.unwrap_or_default();
let mut pen = SvgPathPen::new_with_transform(Affine::IDENTITY);
if let Some(outline) = font.outline_glyphs().get(glyph_id) {
let _ = outline.draw(
DrawSettings::unhinted(Size::unscaled(), LocationRef::default()),
&mut pen,
);
}
let path = pen.into_inner();
let escaped_codepoint = match char::from_u32(codepoint) {
Some('\'') => "'".to_string(),
Some('\"') => """.to_string(),
Some('&') => "&".to_string(),
Some('<') => "<".to_string(),
Some('>') => ">".to_string(),
Some(c) if (' '..='~').contains(&c) => c.to_string(),
_ => format!("&#x{:x};", codepoint),
};
let mut glyph = XmlElement::new("glyph")
.with_attribute("unicode", escaped_codepoint)
.with_attribute("glyph-name", glyph_name)
.with_attribute("horiz-adv-x", advance_width);
if !path.elements().is_empty() {
glyph.add_attribute("d", SvgPathStyle::Compact(2).write_svg_path(&path));
}
if let Some(form) = arabic_form {
glyph.add_attribute("arabic-form", form);
}
glyph
}
fn add_kerning(font_el: &mut XmlElement, font: &FontRef) {
let glyph_names = font.glyph_names();
if let Ok(kern) = font.kern() {
if let Some(Ok(subtable)) = kern.subtables().next() {
if let Ok(kern::SubtableKind::Format0(format0)) = subtable.kind() {
for pair in format0.pairs() {
let left = glyph_names.get(pair.left().into());
let right = glyph_names.get(pair.right().into());
let mut hkern = XmlElement::new("hkern");
match (left, right) {
(Some(g1), Some(g2)) => {
hkern.add_attribute("g1", g1);
hkern.add_attribute("g2", g2);
}
_ => {
hkern.add_attribute("u1", pair.left().to_u16());
hkern.add_attribute("u2", pair.right().to_u16());
}
}
hkern.add_attribute("k", -pair.value());
font_el.add_child(hkern);
}
}
}
}
}
struct GsubSubs {
init: HashMap<GlyphId, GlyphId>,
medi: HashMap<GlyphId, GlyphId>,
fina: HashMap<GlyphId, GlyphId>,
}
impl GsubSubs {
fn new(font: &FontRef) -> Self {
let mut gsub_subs = GsubSubs {
init: HashMap::new(),
medi: HashMap::new(),
fina: HashMap::new(),
};
if let Ok(gsub) = font.gsub() {
gsub_subs.populate(&gsub);
}
gsub_subs
}
fn populate(&mut self, gsub: &Gsub) {
self.init = get_subst_map(gsub, INIT_FEATURE_TAG).unwrap_or_default();
self.medi = get_subst_map(gsub, MEDI_FEATURE_TAG).unwrap_or_default();
self.fina = get_subst_map(gsub, FINA_FEATURE_TAG).unwrap_or_default();
}
}
fn get_subst_map(gsub: &Gsub, feature_tag: Tag) -> Option<HashMap<GlyphId, GlyphId>> {
let script_list = gsub.script_list().ok()?;
let feature_list = gsub.feature_list().ok()?;
let lookup_list = gsub.lookup_list().ok()?;
let script = script_list
.script_records()
.iter()
.find(|sr| sr.script_tag() == ARAB_SCRIPT_TAG)
.and_then(|sr| sr.script(script_list.offset_data()).ok())?;
let langsys = script.default_lang_sys()?.ok()?;
langsys.feature_indices().iter().find_map(|feature_idx| {
let feature_rec = feature_list
.feature_records()
.get(feature_idx.get() as usize)?;
if feature_rec.feature_tag() != feature_tag {
return None;
}
let feature = feature_rec.feature(feature_list.offset_data()).ok()?;
let lookup_idx = feature.lookup_list_indices().first()?;
let lookup = lookup_list.lookups().get(lookup_idx.get() as usize).ok()?;
if lookup.lookup_type() == 1 {
if let Ok(SubstitutionSubtables::Single(subtables)) = lookup.subtables() {
return collect_single_substitutions(subtables);
}
}
None
})
}
fn collect_single_substitutions<'a>(
subtables: Subtables<'a, SingleSubst<'a>, ExtensionSubstFormat1<'a, SingleSubst<'a>>>,
) -> Option<HashMap<GlyphId, GlyphId>> {
let mut map = HashMap::new();
for subtable in subtables.iter().filter_map(|st| st.ok()) {
match subtable {
SingleSubst::Format1(table) => {
if let Ok(coverage) = table.coverage() {
for glyph_id in coverage.iter() {
map.insert(
glyph_id.into(),
GlyphId::new(
(glyph_id.to_u16() as i32 + table.delta_glyph_id() as i32) as u16
as u32,
),
);
}
}
}
SingleSubst::Format2(table) => {
if let Ok(coverage) = table.coverage() {
for (glyph_id, subst_glyph_id) in
coverage.iter().zip(table.substitute_glyph_ids())
{
map.insert(glyph_id.into(), subst_glyph_id.get().into());
}
}
}
}
}
Some(map)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testdata;
use skrifa::FontRef;
#[test]
fn test_generate_svg_font_caveat() {
let font = FontRef::new(testdata::NOTO_KUFI_ARABIC_FONT).unwrap();
let result = generate_svg_font(&font, "noto_kufi_arabic");
assert!(result.is_ok());
let svg_bytes = result.unwrap();
assert!(!svg_bytes.is_empty());
let svg_string = String::from_utf8(svg_bytes).unwrap();
assert_eq!(svg_string, testdata::NOTO_KUF_ARABIC_SVG);
}
}