use super::{FeatureList, LangSys, ReadError, Script, ScriptList, Tag, TaggedElement};
use std::ops::Deref;
#[derive(Copy, Clone, PartialEq, Debug)]
pub struct SelectedScript {
pub tag: Tag,
pub index: u16,
pub is_fallback: bool,
}
impl<'a> ScriptList<'a> {
pub fn index_for_tag(&self, tag: Tag) -> Option<u16> {
self.script_records()
.binary_search_by_key(&tag, |rec| rec.script_tag())
.map(|index| index as u16)
.ok()
}
pub fn get(&self, index: u16) -> Result<TaggedElement<Script<'a>>, ReadError> {
self.script_records()
.get(index as usize)
.ok_or(ReadError::OutOfBounds)
.and_then(|rec| {
Ok(TaggedElement::new(
rec.script_tag(),
rec.script(self.offset_data())?,
))
})
}
pub fn select(&self, tags: &[Tag]) -> Option<SelectedScript> {
for &tag in tags {
if let Some(index) = self.index_for_tag(tag) {
return Some(SelectedScript {
tag,
index,
is_fallback: false,
});
}
}
for tag in [
Tag::new(b"DFLT"),
Tag::new(b"dflt"),
Tag::new(b"latn"),
] {
if let Some(index) = self.index_for_tag(tag) {
return Some(SelectedScript {
tag,
index,
is_fallback: true,
});
}
}
None
}
}
impl<'a> Script<'a> {
pub fn lang_sys_index_for_tag(&self, tag: Tag) -> Option<u16> {
self.lang_sys_records()
.binary_search_by_key(&tag, |rec| rec.lang_sys_tag())
.map(|index| index as u16)
.ok()
}
pub fn lang_sys(&self, index: u16) -> Result<TaggedElement<LangSys<'a>>, ReadError> {
self.lang_sys_records()
.get(index as usize)
.ok_or(ReadError::OutOfBounds)
.and_then(|rec| {
Ok(TaggedElement::new(
rec.lang_sys_tag(),
rec.lang_sys(self.offset_data())?,
))
})
}
}
impl LangSys<'_> {
pub fn feature_index_for_tag(&self, list: &FeatureList, tag: Tag) -> Option<u16> {
let records = list.feature_records();
self.feature_indices()
.iter()
.map(|ix| ix.get())
.find(|&feature_ix| {
records
.get(feature_ix as usize)
.map(|rec| rec.feature_tag())
== Some(tag)
})
}
}
#[derive(Copy, Clone, PartialEq, Eq, Default)]
pub struct ScriptTags {
tags: [Tag; 3],
len: usize,
}
impl ScriptTags {
pub fn from_unicode(unicode_script: Tag) -> Self {
let mut tags = [Tag::default(); 3];
let mut len = 0;
if let Some(new_tag) = new_tag_from_unicode(unicode_script) {
if new_tag != Tag::new(b"mym2") {
let mut bytes = new_tag.to_be_bytes();
bytes[3] = b'3';
tags[len] = Tag::new(&bytes);
len += 1;
}
tags[len] = new_tag;
len += 1;
}
tags[len] = old_tag_from_unicode(unicode_script);
len += 1;
Self { tags, len }
}
pub fn as_slice(&self) -> &[Tag] {
&self.tags[..self.len]
}
}
impl Deref for ScriptTags {
type Target = [Tag];
fn deref(&self) -> &Self::Target {
&self.tags[..self.len]
}
}
impl std::fmt::Debug for ScriptTags {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:?}", self.as_slice())
}
}
fn old_tag_from_unicode(unicode_script: Tag) -> Tag {
let mut bytes = unicode_script.to_be_bytes();
let tag_bytes = match &bytes {
b"Zmth" => b"math",
b"Hira" => b"kana",
b"Laoo" => b"lao ",
b"Yiii" => b"yi ",
b"Nkoo" => b"nko ",
b"Vaii" => b"vai ",
_ => {
bytes[0] = bytes[0].to_ascii_lowercase();
&bytes
}
};
Tag::new(tag_bytes)
}
#[doc(hidden)]
pub const UNICODE_TO_NEW_OPENTYPE_SCRIPT_TAGS: &[(&[u8; 4], Tag)] = &[
(b"Beng", Tag::new(b"bng2")),
(b"Deva", Tag::new(b"dev2")),
(b"Gujr", Tag::new(b"gjr2")),
(b"Guru", Tag::new(b"gur2")),
(b"Knda", Tag::new(b"knd2")),
(b"Mlym", Tag::new(b"mlm2")),
(b"Mymr", Tag::new(b"mym2")),
(b"Orya", Tag::new(b"ory2")),
(b"Taml", Tag::new(b"tml2")),
(b"Telu", Tag::new(b"tel2")),
];
fn new_tag_from_unicode(unicode_script: Tag) -> Option<Tag> {
let ix = UNICODE_TO_NEW_OPENTYPE_SCRIPT_TAGS
.binary_search_by_key(&unicode_script.to_be_bytes(), |entry| *entry.0)
.ok()?;
UNICODE_TO_NEW_OPENTYPE_SCRIPT_TAGS
.get(ix)
.map(|entry| entry.1)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{FontRef, TableProvider};
#[test]
fn script_index_for_tag() {
let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
let gsub_scripts = font.gsub().unwrap().script_list().unwrap();
let ordered_scripts = [b"DFLT", b"cyrl", b"grek", b"hebr", b"latn"];
for (index, tag) in ordered_scripts.into_iter().enumerate() {
let tag = Tag::new(tag);
assert_eq!(gsub_scripts.index_for_tag(tag), Some(index as u16));
}
}
#[test]
fn simple_script_tag_from_unicode() {
let unicode_tags = [b"Cyrl", b"Grek", b"Hebr", b"Latn"];
for unicode_tag in unicode_tags {
let mut bytes = *unicode_tag;
bytes[0] = bytes[0].to_ascii_lowercase();
let expected_tag = Tag::new(&bytes);
let result = ScriptTags::from_unicode(Tag::new(unicode_tag));
assert_eq!(&*result, &[expected_tag]);
}
}
#[test]
fn exception_script_tag_from_unicode() {
let cases = [
(b"Kana", b"kana"),
(b"Hira", b"kana"),
(b"Nkoo", b"nko "),
(b"Yiii", b"yi "),
(b"Vaii", b"vai "),
];
for (unicode_tag, ot_tag) in cases {
let result = ScriptTags::from_unicode(Tag::new(unicode_tag));
assert_eq!(&*result, &[Tag::new(ot_tag)]);
}
}
#[test]
fn multi_script_tags_from_unicode() {
let cases = [
(b"Beng", &[b"bng3", b"bng2", b"beng"][..]),
(b"Orya", &[b"ory3", b"ory2", b"orya"]),
(b"Mlym", &[b"mlm3", b"mlm2", b"mlym"]),
(b"Mymr", &[b"mym2", b"mymr"]),
];
for (unicode_tag, ot_tags) in cases {
let result = ScriptTags::from_unicode(Tag::new(unicode_tag));
let ot_tags = ot_tags
.iter()
.map(|bytes| Tag::new(bytes))
.collect::<Vec<_>>();
assert_eq!(&*result, &ot_tags);
}
}
#[test]
fn select_scripts_from_unicode() {
let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
let gsub_scripts = font.gsub().unwrap().script_list().unwrap();
let hebr = gsub_scripts
.select(&ScriptTags::from_unicode(Tag::new(b"Hebr")))
.unwrap();
assert_eq!(
hebr,
SelectedScript {
tag: Tag::new(b"hebr"),
index: 3,
is_fallback: false,
}
);
let beng = gsub_scripts
.select(&ScriptTags::from_unicode(Tag::new(b"Beng")))
.unwrap();
assert_eq!(
beng,
SelectedScript {
tag: Tag::new(b"DFLT"),
index: 0,
is_fallback: true,
}
);
}
#[test]
fn script_list_get() {
const LATN: Tag = Tag::new(b"latn");
let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
let gsub = font.gsub().unwrap();
let script_list = gsub.script_list().unwrap();
let latn_script_index = script_list.index_for_tag(LATN).unwrap();
assert_eq!(latn_script_index, 1);
let script = script_list.get(latn_script_index).unwrap();
assert_eq!(script.tag, LATN);
}
#[test]
fn script_lang_sys_helpers() {
const TRK: Tag = Tag::new(b"TRK ");
let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
let gsub = font.gsub().unwrap();
let script_list = gsub.script_list().unwrap();
let script = script_list.get(1).unwrap();
let lang_sys_index = script.lang_sys_index_for_tag(TRK).unwrap();
assert_eq!(lang_sys_index, 0);
assert_eq!(script.lang_sys(lang_sys_index).unwrap().tag, TRK);
}
#[test]
fn feature_index_for_tag() {
let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
let gsub = font.gsub().unwrap();
let script_list = gsub.script_list().unwrap();
let feature_list = gsub.feature_list().unwrap();
let lang_sys = script_list
.get(1)
.unwrap()
.default_lang_sys()
.unwrap()
.unwrap();
assert_eq!(
lang_sys.feature_index_for_tag(&feature_list, Tag::new(b"rclt")),
Some(0)
);
assert_eq!(
lang_sys.feature_index_for_tag(&feature_list, Tag::new(b"rlig")),
Some(1)
);
for tag in [b"locl", b"abvs", b"liga"] {
assert_eq!(
lang_sys.feature_index_for_tag(&feature_list, Tag::new(tag)),
None
);
}
}
}