//! Per-glyph style assignment.
use super::script::{ScriptClass, ScriptRange, SCRIPT_RANGES};
use crate::GlyphId;
/// Defines the script and style associated with a single glyph.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[repr(transparent)]
pub(super) struct GlyphStyle(pub(super) u16);
impl GlyphStyle {
// The following flags roughly correspond to those defined in FreeType
// here: https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.h#L76
// but with different values because we intend to store "meta style"
// information differently.
pub const SCRIPT_INDEX_MASK: u16 = 0xFF;
pub const UNASSIGNED: u16 = Self::SCRIPT_INDEX_MASK;
pub const NON_BASE: u16 = 0x100;
pub const DIGIT: u16 = 0x200;
pub const fn is_unassigned(self) -> bool {
self.0 == Self::UNASSIGNED
}
pub const fn is_non_base(self) -> bool {
self.0 & Self::NON_BASE != 0
}
pub const fn is_digit(self) -> bool {
self.0 & Self::DIGIT != 0
}
pub fn script_class(self) -> Option<&'static ScriptClass> {
let ix = self.0 & Self::SCRIPT_INDEX_MASK;
if ix != Self::UNASSIGNED {
ScriptClass::from_index(ix)
} else {
None
}
}
// Helper for generated code
pub(super) const fn from_script_index_and_flags(script_index: u16, flags: u16) -> Self {
Self(flags | script_index)
}
}
impl Default for GlyphStyle {
fn default() -> Self {
Self(Self::UNASSIGNED)
}
}
/// Computes glyph styles for those glyphs directly addressed by a character
/// map.
///
/// This generally follows the FreeType code here: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L126>
pub(super) fn compute_mapped_styles(
charmap: impl Iterator<Item = (u32, GlyphId)>,
styles: &mut [GlyphStyle],
) {
styles.fill(GlyphStyle::default());
// cmap entries are sorted so we keep track of the most recent range to
// avoid a binary search per character
let mut last_range: Option<(usize, ScriptRange)> = None;
for (ch, gid) in charmap {
let Some(style) = styles.get_mut(gid.to_u32() as usize) else {
continue;
};
// Charmaps enumerate in order so we're likely to encounter at least
// a few codepoints in the same range.
if let Some(last) = last_range {
if last.1.contains(ch) {
*style = last.1.style;
continue;
}
}
let ix = match SCRIPT_RANGES.binary_search_by(|x| x.first.cmp(&ch)) {
Ok(i) => i,
Err(i) => i.saturating_sub(1),
};
let Some(range) = SCRIPT_RANGES.get(ix).copied() else {
continue;
};
if range.contains(ch) {
*style = range.style;
last_range = Some((ix, range));
}
}
}
/// Sets a fallback style for any glyph that is still unassigned.
pub(super) fn assign_fallback_styles(styles: &mut [GlyphStyle]) {
// For some reason, FreeType uses Hani as a default fallback script so
// let's do the same.
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.h#L69>
for style in styles.iter_mut() {
if style.is_unassigned() {
style.0 &= !GlyphStyle::SCRIPT_INDEX_MASK;
style.0 |= ScriptClass::HANI as u16;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{raw::TableProvider, FontRef, MetadataProvider};
#[test]
fn cmap_glyph_styles() {
let font = FontRef::new(font_test_data::AUTOHINT_CMAP).unwrap();
let num_glyphs = font.maxp().unwrap().num_glyphs() as usize;
let mut styles = vec![GlyphStyle::default(); num_glyphs];
super::compute_mapped_styles(font.charmap().mappings(), &mut styles);
super::assign_fallback_styles(&mut styles);
// generated by printf debugging in FreeType
// (gid, Option<(script_name, is_non_base)>)
let expected = &[
(0, Some(("CJKV ideographs", false))),
(1, Some(("Latin", true))),
(2, Some(("Armenian", true))),
(3, Some(("Hebrew", true))),
(4, Some(("Arabic", false))),
(5, Some(("Arabic", false))),
(6, Some(("Arabic", true))),
(7, Some(("Devanagari", true))),
(8, Some(("Devanagari", false))),
(9, Some(("Bengali", true))),
(10, Some(("Bengali", false))),
(11, Some(("Gurmukhi", true))),
(12, Some(("Gurmukhi", false))),
(13, Some(("Gujarati", true))),
(14, Some(("Gujarati", true))),
(15, Some(("Oriya", true))),
(16, Some(("Oriya", false))),
(17, Some(("Tamil", true))),
(18, Some(("Tamil", false))),
(19, Some(("Telugu", true))),
(20, Some(("Telugu", false))),
(21, Some(("Kannada", true))),
(22, Some(("Kannada", false))),
(23, Some(("Malayalam", true))),
(24, Some(("Malayalam", false))),
(25, Some(("Sinhala", true))),
(26, Some(("Sinhala", false))),
(27, Some(("Thai", true))),
(28, Some(("Thai", false))),
(29, Some(("Lao", true))),
(30, Some(("Lao", false))),
(31, Some(("Tibetan", true))),
(32, Some(("Tibetan", false))),
(33, Some(("Myanmar", true))),
(34, Some(("Ethiopic", true))),
(35, Some(("Buhid", true))),
(36, Some(("Buhid", false))),
(37, Some(("Khmer", true))),
(38, Some(("Khmer", false))),
(39, Some(("Mongolian", true))),
(40, Some(("Canadian Syllabics", false))),
(41, Some(("Limbu", true))),
(42, Some(("Limbu", false))),
(43, Some(("Khmer Symbols", false))),
(44, Some(("Sundanese", true))),
(45, Some(("Ol Chiki", false))),
(46, Some(("Georgian (Mkhedruli)", false))),
(47, Some(("Sundanese", false))),
(48, Some(("Latin Superscript Fallback", false))),
(49, Some(("Latin", true))),
(50, Some(("Greek", true))),
(51, Some(("Greek", false))),
(52, Some(("Latin Subscript Fallback", false))),
(53, Some(("Coptic", true))),
(54, Some(("Coptic", false))),
(55, Some(("Georgian (Khutsuri)", false))),
(56, Some(("Tifinagh", false))),
(57, Some(("Ethiopic", false))),
(58, Some(("Cyrillic", true))),
(59, Some(("CJKV ideographs", true))),
(60, Some(("CJKV ideographs", false))),
(61, Some(("Lisu", false))),
(62, Some(("Vai", false))),
(63, Some(("Cyrillic", true))),
(64, Some(("Bamum", true))),
(65, Some(("Syloti Nagri", true))),
(66, Some(("Syloti Nagri", false))),
(67, Some(("Saurashtra", true))),
(68, Some(("Saurashtra", false))),
(69, Some(("Kayah Li", true))),
(70, Some(("Kayah Li", false))),
(71, Some(("Myanmar", false))),
(72, Some(("Tai Viet", true))),
(73, Some(("Tai Viet", false))),
(74, Some(("Cherokee", false))),
(75, Some(("Armenian", false))),
(76, Some(("Hebrew", false))),
(77, Some(("Arabic", false))),
(78, Some(("Carian", false))),
(79, Some(("Gothic", false))),
(80, Some(("Deseret", false))),
(81, Some(("Shavian", false))),
(82, Some(("Osmanya", false))),
(83, Some(("Osage", false))),
(84, Some(("Cypriot", false))),
(85, Some(("Avestan", true))),
(86, Some(("Avestan", true))),
(87, Some(("Old Turkic", false))),
(88, Some(("Hanifi Rohingya", false))),
(89, Some(("Chakma", true))),
(90, Some(("Chakma", false))),
(91, Some(("Mongolian", false))),
(92, Some(("CJKV ideographs", false))),
(93, Some(("Medefaidrin", false))),
(94, Some(("Glagolitic", true))),
(95, Some(("Glagolitic", true))),
(96, Some(("Adlam", true))),
(97, Some(("Adlam", false))),
];
let results = styles
.iter()
.enumerate()
.map(|(gid, style)| {
(
gid as i32,
style
.script_class()
.map(|script| (script.name, style.is_non_base())),
)
})
.collect::<Vec<_>>();
for (i, result) in results.iter().enumerate() {
assert_eq!(result, &expected[i]);
}
}
}