use std::collections::HashSet;
use crate::{
base::{BaseScript, BaseTable},
error::AutobaseError,
utils::iso15924_to_opentype,
};
use skrifa::{
metrics::BoundingBox,
prelude::{LocationRef, Size},
raw::TableProvider,
GlyphId, MetadataProvider, Tag,
};
pub const CJK_SCRIPTS: [&str; 10] = [
"Kana", "Hani", "Bopo", "Hira", "Hang", "kana", "hani", "bopo", "hira", "hang",
];
pub fn is_cjk_script(s: &str) -> bool {
CJK_SCRIPTS.contains(&s)
}
#[derive(Debug, Clone)]
pub struct CjkMetrics {
h_icfb: Option<f32>,
h_icft: Option<f32>,
h_ideo: Option<f32>,
h_idtp: Option<f32>,
h_romn: Option<f32>,
v_icfb: Option<f32>,
v_icft: Option<f32>,
v_ideo: Option<f32>,
v_idtp: Option<f32>,
v_romn: Option<f32>,
}
impl CjkMetrics {
fn from_bounds(bounds: &[BoundingBox], upem: f32, average_width: f32) -> Self {
let bbox_y_average = bounds
.iter()
.map(|b| (b.y_max + b.y_min) / 2.0)
.sum::<f32>()
/ bounds.len() as f32;
let h_idtp = bbox_y_average + upem / 2.0;
let h_ideo = bbox_y_average - upem / 2.0;
let average_top = bounds.iter().map(|b| b.y_max).sum::<f32>() / bounds.len() as f32;
let average_bottom = bounds.iter().map(|b| b.y_min).sum::<f32>() / bounds.len() as f32;
let average_left = bounds.iter().map(|b| b.x_min).sum::<f32>() / bounds.len() as f32;
let average_right = bounds.iter().map(|b| b.x_max).sum::<f32>() / bounds.len() as f32;
CjkMetrics {
h_icfb: Some(average_bottom),
h_icft: Some(average_top),
h_ideo: Some(h_ideo),
h_idtp: Some(h_idtp),
h_romn: Some(0.0),
v_icfb: Some(average_left),
v_icft: Some(average_right),
v_ideo: Some(0.0),
v_idtp: Some(average_width),
v_romn: Some(-h_ideo),
}
}
pub fn insert_into_base(
&self,
upem: f32,
supported_scripts: &HashSet<&str>,
base: &mut BaseTable,
) {
let average_width = self.v_idtp.unwrap();
let font_is_square = (average_width - upem).abs() / upem < 0.01;
for ot_script in supported_scripts
.iter()
.flat_map(|s| iso15924_to_opentype(s))
{
let default_baseline = if is_cjk_script(&ot_script.to_string()) {
Tag::new(b"ideo")
} else {
Tag::new(b"romn")
};
let h_basescript =
if let Some(bs) = base.horizontal.iter_mut().find(|bs| bs.script == ot_script) {
bs
} else {
base.horizontal.push(BaseScript::new(ot_script));
base.horizontal.last_mut().unwrap()
};
h_basescript.default_baseline = Some(default_baseline);
let hbaselines = &mut h_basescript.baselines;
if let Some(icfb) = self.h_icfb {
hbaselines.insert(Tag::new(b"icfb"), icfb as i16);
}
if let Some(icft) = self.h_icft {
hbaselines.insert(Tag::new(b"icft"), icft as i16);
}
if let Some(ideo) = self.h_ideo {
hbaselines.insert(Tag::new(b"ideo"), ideo as i16);
}
if let Some(romn) = self.h_romn {
hbaselines.insert(Tag::new(b"romn"), romn as i16);
}
if !font_is_square {
if let Some(idtp) = self.h_idtp {
hbaselines.insert(Tag::new(b"idtp"), idtp as i16);
}
}
let v_basescript =
if let Some(bs) = base.vertical.iter_mut().find(|bs| bs.script == ot_script) {
bs
} else {
base.vertical.push(BaseScript::new(ot_script));
base.vertical.last_mut().unwrap()
};
v_basescript.default_baseline = Some(default_baseline);
let vbaselines = &mut v_basescript.baselines;
if let Some(icfb) = self.v_icfb {
vbaselines.insert(Tag::new(b"icfb"), icfb as i16);
}
if let Some(icft) = self.v_icft {
vbaselines.insert(Tag::new(b"icft"), icft as i16);
}
if let Some(ideo) = self.v_ideo {
vbaselines.insert(Tag::new(b"ideo"), ideo as i16);
}
if let Some(romn) = self.v_romn {
vbaselines.insert(Tag::new(b"romn"), romn as i16);
}
if !font_is_square {
if let Some(idtp) = self.v_idtp {
vbaselines.insert(Tag::new(b"idtp"), idtp as i16);
}
}
}
}
}
fn cjk_glyphs(f: &skrifa::FontRef) -> Vec<GlyphId> {
let mut cjk_glyphs = f
.charmap()
.mappings()
.filter(|(cp, _gid)| {
(0x4E00..0x9FFF).contains(cp)
|| (0x3400..0x4DBF).contains(cp) || (0x20000..0x2A6DF).contains(cp) })
.map(|(_cp, gid)| gid)
.collect::<Vec<_>>();
if cjk_glyphs.is_empty() {
cjk_glyphs = f
.charmap()
.mappings()
.filter(
|(cp, _gid)|
(0xAC00..=0xD7AF).contains(cp)
|| (0x3040..=0x30FF).contains(cp)
|| (0xFF00..=0xFFEF).contains(cp), )
.map(|(_cp, gid)| gid)
.collect();
}
cjk_glyphs
}
pub fn compute_bounds(f: &skrifa::FontRef) -> Result<CjkMetrics, AutobaseError> {
let upem = f.head()?.units_per_em() as f32;
let glyph_metrics = f.glyph_metrics(Size::unscaled(), LocationRef::default());
let hmtx = f.hmtx()?;
let relevant_glyphs = cjk_glyphs(f);
let average_width = relevant_glyphs
.iter()
.map(|&gid| hmtx.advance(gid).map(|x| x as f32).unwrap_or(upem)) .sum::<f32>()
/ relevant_glyphs.len() as f32;
Ok(CjkMetrics::from_bounds(
&relevant_glyphs
.iter()
.filter_map(|&gid| glyph_metrics.bounds(gid))
.collect::<Vec<_>>(),
upem,
average_width,
))
}