use fontcull_read_fonts::{
tables::name::{CharIter, Name, NameRecord, NameString},
FontRef, TableProvider,
};
use core::fmt;
#[doc(inline)]
pub use fontcull_read_fonts::types::NameId as StringId;
#[derive(Clone)]
pub struct Chars<'a> {
inner: Option<CharIter<'a>>,
}
impl Iterator for Chars<'_> {
type Item = char;
fn next(&mut self) -> Option<Self::Item> {
self.inner.as_mut()?.next()
}
}
#[derive(Clone)]
pub struct LocalizedStrings<'a> {
name: Option<Name<'a>>,
records: core::slice::Iter<'a, NameRecord>,
id: StringId,
}
impl<'a> LocalizedStrings<'a> {
pub fn new(font: &FontRef<'a>, id: StringId) -> Self {
let name = font.name().ok();
let records = name
.as_ref()
.map(|name| name.name_record().iter())
.unwrap_or([].iter());
Self { name, records, id }
}
pub fn id(&self) -> StringId {
self.id
}
pub fn english_or_first(self) -> Option<LocalizedString<'a>> {
let mut best_rank = -1;
let mut best_string = None;
for (i, string) in self.enumerate() {
let rank = match (i, string.language()) {
(_, Some("en-US")) => return Some(string),
(_, Some("en")) => 2,
(_, None) => 1,
(0, _) => 0,
_ => continue,
};
if rank > best_rank {
best_rank = rank;
best_string = Some(string);
}
}
best_string
}
}
impl<'a> Iterator for LocalizedStrings<'a> {
type Item = LocalizedString<'a>;
fn next(&mut self) -> Option<Self::Item> {
let name = self.name.as_ref()?;
loop {
let record = self.records.next()?;
if record.name_id() == self.id {
return Some(LocalizedString::new(name, record));
}
}
}
}
impl Default for LocalizedStrings<'_> {
fn default() -> Self {
Self {
name: None,
records: [].iter(),
id: StringId::default(),
}
}
}
#[derive(Clone, Debug)]
pub struct LocalizedString<'a> {
language: Option<Language>,
value: Option<NameString<'a>>,
}
impl<'a> LocalizedString<'a> {
pub fn new(name: &Name<'a>, record: &NameRecord) -> Self {
let language = Language::new(name, record);
let value = record.string(name.string_data()).ok();
Self { language, value }
}
pub fn language(&self) -> Option<&str> {
self.language.as_ref().map(|language| language.as_str())
}
pub fn chars(&self) -> Chars<'a> {
Chars {
inner: self.value.map(|value| value.chars()),
}
}
}
impl fmt::Display for LocalizedString<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for ch in self.chars() {
ch.fmt(f)?;
}
Ok(())
}
}
const MAX_INLINE_LANGUAGE_LEN: usize = 30;
#[derive(Copy, Clone, Debug)]
#[repr(u8)]
enum Language {
Inline {
buf: [u8; MAX_INLINE_LANGUAGE_LEN],
len: u8,
},
Static(&'static str),
}
impl Language {
fn new(name: &Name, record: &NameRecord) -> Option<Self> {
let language_id = record.language_id();
const BASE_LANGUAGE_TAG_ID: u16 = 0x8000;
if name.version() == 1 && language_id >= BASE_LANGUAGE_TAG_ID {
let index = (language_id - BASE_LANGUAGE_TAG_ID) as usize;
let language_string = name
.lang_tag_record()?
.get(index)?
.lang_tag(name.string_data())
.ok()?;
Self::from_name_string(&language_string)
} else {
match record.platform_id() {
1 | 3 => Self::from_language_id(language_id),
_ => None,
}
}
}
fn from_name_string(s: &NameString) -> Option<Self> {
let mut buf = [0u8; MAX_INLINE_LANGUAGE_LEN];
let mut len = 0;
for ch in s.chars() {
if !ch.is_ascii() || len == MAX_INLINE_LANGUAGE_LEN {
return None;
}
buf[len] = ch as u8;
len += 1;
}
Some(Self::Inline {
buf,
len: len as u8,
})
}
fn from_language_id(language_id: u16) -> Option<Self> {
Some(Self::Static(language_id_to_bcp47(language_id)?))
}
fn as_str(&self) -> &str {
match self {
Self::Inline { buf: data, len } => {
let data = &data[..*len as usize];
core::str::from_utf8(data).unwrap_or_default()
}
Self::Static(str) => str,
}
}
}
fn language_id_to_bcp47(language_id: u16) -> Option<&'static str> {
match LANGUAGE_ID_TO_BCP47.binary_search_by(|entry| entry.0.cmp(&language_id)) {
Ok(ix) => LANGUAGE_ID_TO_BCP47.get(ix).map(|entry| entry.1),
_ => None,
}
}
const LANGUAGE_ID_TO_BCP47: &[(u16, &str)] = &[
(0, "en"), (1, "fr"), (2, "de"), (3, "it"), (4, "nl"), (5, "sv"), (6, "es"), (7, "da"), (8, "pt"), (9, "nb"), (10, "he"), (11, "ja"), (12, "ar"), (13, "fi"), (14, "el"), (15, "is"), (16, "mt"), (17, "tr"), (18, "hr"), (19, "zh-Hant"), (20, "ur"), (21, "hi"), (22, "th"), (23, "ko"), (24, "lt"), (25, "pl"), (26, "hu"), (27, "et"), (28, "lv"), (29, "se"), (30, "fo"), (31, "fa"), (32, "ru"), (33, "zh-Hans"), (34, "nl"), (35, "ga"), (36, "sq"), (37, "ro"), (38, "cs"), (39, "sk"), (40, "sl"), (41, "yi"), (42, "sr"), (43, "mk"), (44, "bg"), (45, "uk"), (46, "be"), (47, "uz"), (48, "kk"), (49, "az-Cyrl"), (50, "az-Arab"), (51, "hy"), (52, "ka"), (53, "mo"), (54, "ky"), (55, "tg"), (56, "tk"), (57, "mn-Mong"), (58, "mn-Cyrl"), (59, "ps"), (60, "ku"), (61, "ks"), (62, "sd"), (63, "bo"), (64, "ne"), (65, "sa"), (66, "mr"), (67, "bn"), (68, "as"), (69, "gu"), (70, "pa"), (71, "or"), (72, "ml"), (73, "kn"), (74, "ta"), (75, "te"), (76, "si"), (77, "my"), (78, "km"), (79, "lo"), (80, "vi"), (81, "id"), (82, "tl"), (83, "ms-Latn"), (84, "ms-Arab"), (85, "am"), (86, "ti"), (87, "om"), (88, "so"), (89, "sw"), (90, "rw"), (91, "rn"), (92, "ny"), (93, "mg"), (94, "eo"), (128, "cy"), (129, "eu"), (130, "ca"), (131, "la"), (132, "qu"), (133, "gn"), (134, "ay"), (135, "tt"), (136, "ug"), (137, "dz"), (138, "jv-Latn"), (139, "su-Latn"), (140, "gl"), (141, "af"), (142, "br"), (143, "iu"), (144, "gd"), (145, "gv"), (146, "ga"), (147, "to"), (148, "el"), (149, "kl"), (150, "az-Latn"), (151, "nn"),
(0x0401, "ar-SA"), (0x0402, "bg-BG"), (0x0403, "ca-ES"), (0x0404, "zh-TW"), (0x0405, "cs-CZ"), (0x0406, "da-DK"), (0x0407, "de-DE"), (0x0408, "el-GR"), (0x0409, "en-US"), (0x040a, "es-ES_tradnl"), (0x040b, "fi-FI"), (0x040c, "fr-FR"), (0x040d, "he-IL"), (0x040d, "he"), (0x040e, "hu-HU"), (0x040e, "hu"), (0x040f, "is-IS"), (0x0410, "it-IT"), (0x0411, "ja-JP"), (0x0412, "ko-KR"), (0x0413, "nl-NL"), (0x0414, "nb-NO"), (0x0415, "pl-PL"), (0x0416, "pt-BR"), (0x0417, "rm-CH"), (0x0418, "ro-RO"), (0x0419, "ru-RU"), (0x041a, "hr-HR"), (0x041b, "sk-SK"), (0x041c, "sq-AL"), (0x041d, "sv-SE"), (0x041e, "th-TH"), (0x041f, "tr-TR"), (0x0420, "ur-PK"), (0x0421, "id-ID"), (0x0422, "uk-UA"), (0x0423, "be-BY"), (0x0424, "sl-SI"), (0x0425, "et-EE"), (0x0426, "lv-LV"), (0x0427, "lt-LT"), (0x0428, "tg-Cyrl-TJ"), (0x0429, "fa-IR"), (0x042a, "vi-VN"), (0x042b, "hy-AM"), (0x042c, "az-Latn-AZ"), (0x042d, "eu-ES"), (0x042e, "hsb-DE"), (0x042f, "mk-MK"), (0x0432, "tn-ZA"), (0x0434, "xh-ZA"), (0x0435, "zu-ZA"), (0x0436, "af-ZA"), (0x0437, "ka-GE"), (0x0438, "fo-FO"), (0x0439, "hi-IN"), (0x043a, "mt-MT"), (0x043b, "se-NO"), (0x043e, "ms-MY"), (0x043f, "kk-KZ"), (0x0440, "ky-KG"), (0x0441, "sw-KE"), (0x0442, "tk-TM"), (0x0443, "uz-Latn-UZ"), (0x0443, "uz"), (0x0444, "tt-RU"), (0x0445, "bn-IN"), (0x0446, "pa-IN"), (0x0447, "gu-IN"), (0x0448, "or-IN"), (0x0449, "ta-IN"), (0x044a, "te-IN"), (0x044b, "kn-IN"), (0x044c, "ml-IN"), (0x044d, "as-IN"), (0x044e, "mr-IN"), (0x044f, "sa-IN"), (0x0450, "mn-Cyrl"), (0x0451, "bo-CN"), (0x0452, "cy-GB"), (0x0453, "km-KH"), (0x0454, "lo-LA"), (0x0456, "gl-ES"), (0x0457, "kok-IN"), (0x045a, "syr-SY"), (0x045b, "si-LK"), (0x045d, "iu-Cans-CA"), (0x045e, "am-ET"), (0x0461, "ne-NP"), (0x0462, "fy-NL"), (0x0463, "ps-AF"), (0x0464, "fil-PH"), (0x0465, "dv-MV"), (0x0468, "ha-Latn-NG"), (0x046a, "yo-NG"), (0x046b, "quz-BO"), (0x046c, "nso-ZA"), (0x046d, "ba-RU"), (0x046e, "lb-LU"), (0x046f, "kl-GL"), (0x0470, "ig-NG"), (0x0478, "ii-CN"), (0x047a, "arn-CL"), (0x047c, "moh-CA"), (0x047e, "br-FR"), (0x0480, "ug-CN"), (0x0481, "mi-NZ"), (0x0482, "oc-FR"), (0x0483, "co-FR"), (0x0484, "gsw-FR"), (0x0485, "sah-RU"), (0x0486, "qut-GT"), (0x0487, "rw-RW"), (0x0488, "wo-SN"), (0x048c, "prs-AF"), (0x0491, "gd-GB"), (0x0801, "ar-IQ"), (0x0804, "zh-Hans"), (0x0807, "de-CH"), (0x0809, "en-GB"), (0x080a, "es-MX"), (0x080c, "fr-BE"), (0x0810, "it-CH"), (0x0813, "nl-BE"), (0x0814, "nn-NO"), (0x0816, "pt-PT"), (0x081a, "sr-Latn-CS"), (0x081d, "sv-FI"), (0x082c, "az-Cyrl-AZ"), (0x082e, "dsb-DE"), (0x082e, "dsb"), (0x083b, "se-SE"), (0x083c, "ga-IE"), (0x083e, "ms-BN"), (0x0843, "uz-Cyrl-UZ"), (0x0845, "bn-BD"), (0x0850, "mn-Mong-CN"), (0x085d, "iu-Latn-CA"), (0x085f, "tzm-Latn-DZ"), (0x086b, "quz-EC"), (0x0c01, "ar-EG"), (0x0c04, "zh-Hant"), (0x0c07, "de-AT"), (0x0c09, "en-AU"), (0x0c0a, "es-ES"), (0x0c0c, "fr-CA"), (0x0c1a, "sr-Cyrl-CS"), (0x0c3b, "se-FI"), (0x0c6b, "quz-PE"), (0x1001, "ar-LY"), (0x1004, "zh-SG"), (0x1007, "de-LU"), (0x1009, "en-CA"), (0x100a, "es-GT"), (0x100c, "fr-CH"), (0x101a, "hr-BA"), (0x103b, "smj-NO"), (0x1401, "ar-DZ"), (0x1404, "zh-MO"), (0x1407, "de-LI"), (0x1409, "en-NZ"), (0x140a, "es-CR"), (0x140c, "fr-LU"), (0x141a, "bs-Latn-BA"), (0x141a, "bs"), (0x143b, "smj-SE"), (0x143b, "smj"), (0x1801, "ar-MA"), (0x1809, "en-IE"), (0x180a, "es-PA"), (0x180c, "fr-MC"), (0x181a, "sr-Latn-BA"), (0x183b, "sma-NO"), (0x1c01, "ar-TN"), (0x1c09, "en-ZA"), (0x1c0a, "es-DO"), (0x1c1a, "sr-Cyrl-BA"), (0x1c3b, "sma-SE"), (0x1c3b, "sma"), (0x2001, "ar-OM"), (0x2009, "en-JM"), (0x200a, "es-VE"), (0x201a, "bs-Cyrl-BA"), (0x201a, "bs-Cyrl"), (0x203b, "sms-FI"), (0x203b, "sms"), (0x2401, "ar-YE"), (0x2409, "en-029"), (0x240a, "es-CO"), (0x241a, "sr-Latn-RS"), (0x243b, "smn-FI"), (0x2801, "ar-SY"), (0x2809, "en-BZ"), (0x280a, "es-PE"), (0x281a, "sr-Cyrl-RS"), (0x2c01, "ar-JO"), (0x2c09, "en-TT"), (0x2c0a, "es-AR"), (0x2c1a, "sr-Latn-ME"), (0x3001, "ar-LB"), (0x3009, "en-ZW"), (0x300a, "es-EC"), (0x301a, "sr-Cyrl-ME"), (0x3401, "ar-KW"), (0x3409, "en-PH"), (0x340a, "es-CL"), (0x3801, "ar-AE"), (0x380a, "es-UY"), (0x3c01, "ar-BH"), (0x3c0a, "es-PY"), (0x4001, "ar-QA"), (0x4009, "en-IN"), (0x400a, "es-BO"), (0x4409, "en-MY"), (0x440a, "es-SV"), (0x4809, "en-SG"), (0x480a, "es-HN"), (0x4c0a, "es-NI"), (0x500a, "es-PR"), (0x540a, "es-US"), ];
#[cfg(test)]
mod tests {
use crate::MetadataProvider;
use super::*;
use fontcull_read_fonts::FontRef;
#[test]
fn localized() {
let font = FontRef::new(fontcull_font_test_data::NAMES_ONLY).unwrap();
let mut subfamily_names = font
.localized_strings(StringId::SUBFAMILY_NAME)
.map(|s| (s.language().unwrap().to_string(), s.to_string()))
.collect::<Vec<_>>();
subfamily_names.sort_by(|a, b| a.0.cmp(&b.0));
let expected = [
(String::from("ar-SA"), String::from("عادي")),
(String::from("el-GR"), String::from("Κανονικά")),
(String::from("en"), String::from("Regular")),
(String::from("eu-ES"), String::from("Arrunta")),
(String::from("pl-PL"), String::from("Normalny")),
(String::from("zh-Hans"), String::from("正常")),
];
assert_eq!(subfamily_names.as_slice(), expected);
}
#[test]
fn find_by_language() {
let font = FontRef::new(fontcull_font_test_data::NAMES_ONLY).unwrap();
assert_eq!(
font.localized_strings(StringId::SUBFAMILY_NAME)
.find(|s| s.language() == Some("pl-PL"))
.unwrap()
.to_string(),
"Normalny"
);
}
#[test]
fn english_or_first() {
let font = FontRef::new(fontcull_font_test_data::NAMES_ONLY).unwrap();
assert_eq!(
font.localized_strings(StringId::SUBFAMILY_NAME)
.english_or_first()
.unwrap()
.to_string(),
"Regular"
);
}
}