use std::fmt::Write as _;
use sha2::{Digest as _, Sha256};
use skrifa::string::StringId;
use skrifa::{FontRef, MetadataProvider as _};
use crate::font::instance::AxisValue;
const MAX_LENGTH: usize = 63;
const SAME: f32 = 0.5;
pub(crate) fn instance_name(font: &FontRef<'_>, values: &[AxisValue]) -> Option<Vec<u8>> {
if values.is_empty() {
return None;
}
let coords = full_coords(font, values);
let named = font.named_instances().iter().find(|instance| {
instance
.user_coords()
.zip(&coords)
.all(|(named, (_, value))| (named - value).abs() < SAME)
});
let prefix = prefix(font)?;
let name = match named {
Some(instance) => instance
.postscript_name_id()
.and_then(|id| string(font, id))
.or_else(|| {
let style = string(font, instance.subfamily_name_id())?;
Some(format!("{prefix}-{style}"))
})?,
None => unnamed(&prefix, &coords),
};
Some(shortened(&prefix, name).into_bytes())
}
fn full_coords(font: &FontRef<'_>, values: &[AxisValue]) -> Vec<([u8; 4], f32)> {
font.axes()
.iter()
.map(|axis| {
let tag = axis.tag().to_be_bytes();
let value = values
.iter()
.find(|given| given.tag == tag)
.map_or(axis.default_value(), |given| given.value);
(tag, value)
})
.collect()
}
fn unnamed(prefix: &str, coords: &[([u8; 4], f32)]) -> String {
let mut name = prefix.to_owned();
for (tag, value) in coords {
name.push('_');
name.push_str(&number(*value));
name.extend(
tag.iter()
.map(|&byte| char::from(byte))
.filter(char::is_ascii_alphanumeric),
);
}
name
}
fn number(value: f32) -> String {
let text = format!("{value:.5}");
let text = text.trim_end_matches('0').trim_end_matches('.');
if text == "-0" {
"0".to_owned()
} else {
text.to_owned()
}
}
fn shortened(prefix: &str, name: String) -> String {
if name.len() <= MAX_LENGTH {
return name;
}
let digest = Sha256::digest(name.as_bytes());
let hex = digest.iter().take(10).fold(String::new(), |mut hex, byte| {
let _ = write!(hex, "{byte:02X}");
hex
});
let keep = MAX_LENGTH.saturating_sub(hex.len() + 4);
format!("{}-{hex}...", prefix.get(..keep).unwrap_or(prefix))
}
fn prefix(font: &FontRef<'_>) -> Option<String> {
[
StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
StringId::TYPOGRAPHIC_FAMILY_NAME,
StringId::FAMILY_NAME,
]
.into_iter()
.find_map(|id| string(font, id))
}
fn string(font: &FontRef<'_>, id: StringId) -> Option<String> {
let text: String = font
.localized_strings(id)
.english_or_first()?
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-')
.collect();
(!text.is_empty()).then_some(text)
}
#[cfg(test)]
mod tests {
use super::{number, shortened, unnamed};
#[test]
fn values_lose_trailing_zeros() {
for (value, want) in [
(700.0, "700"),
(550.5, "550.5"),
(-0.0, "0"),
(0.125, "0.125"),
] {
assert_eq!(number(value), want, "{value}");
}
}
#[test]
fn an_unnamed_instance_lists_every_axis_in_order() {
let name = unnamed("Family", &[(*b"wght", 550.0), (*b"wdth", 87.5)]);
assert_eq!(name, "Family_550wght_87.5wdth");
}
#[test]
fn a_long_name_is_shortened_with_a_hash() {
let long = format!("Family{}", "_1000wght".repeat(10));
let short = shortened("Family", long);
assert!(short.len() <= 63, "{short}");
assert!(
short.starts_with("Family-") && short.ends_with("..."),
"{short}"
);
}
}