blob: 1dba39cfb04c4bc1aebd03f089e34dc8eeb54883 [file] [log] [blame]
//! Styles, scripts and glyph style mapping.
use super::metrics::BlueZones;
use super::shape::{ShaperCoverageKind, VisitedLookupSet};
use alloc::vec::Vec;
use raw::types::{GlyphId, Tag};
/// 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.
const STYLE_INDEX_MASK: u16 = 0xFF;
const UNASSIGNED: u16 = Self::STYLE_INDEX_MASK;
// A non-base character, perhaps more commonly referred to as a "mark"
const NON_BASE: u16 = 0x100;
const DIGIT: u16 = 0x200;
// Used as intermediate state to mark when a glyph appears as GSUB output
// for a given script
const FROM_GSUB_OUTPUT: u16 = 0x8000;
pub const fn is_unassigned(self) -> bool {
self.0 & Self::STYLE_INDEX_MASK == 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 style_class(self) -> Option<&'static StyleClass> {
StyleClass::from_index(self.style_index()?)
}
pub fn style_index(self) -> Option<u16> {
let ix = self.0 & Self::STYLE_INDEX_MASK;
if ix != Self::UNASSIGNED {
Some(ix)
} else {
None
}
}
fn maybe_assign(&mut self, other: Self) {
// FreeType walks the style array in order so earlier styles
// have precedence. Since we walk the cmap and binary search
// on the full range mapping, our styles are mapped in a
// different order. This check allows us to replace a currently
// mapped style if the new style index is lower which matches
// FreeType's behavior.
//
// Note that we keep the extra bits because FreeType allows
// setting the NON_BASE bit to an already mapped style.
if other.0 & Self::STYLE_INDEX_MASK <= self.0 & Self::STYLE_INDEX_MASK {
self.0 = (self.0 & !Self::STYLE_INDEX_MASK) | other.0;
}
}
pub(super) fn set_from_gsub_output(&mut self) {
self.0 |= Self::FROM_GSUB_OUTPUT
}
pub(super) fn clear_from_gsub(&mut self) {
self.0 &= !Self::FROM_GSUB_OUTPUT;
}
/// Assign a style if we've been marked as GSUB output _and_ the
/// we don't currently have an assigned style.
///
/// This also clears the GSUB output bit.
///
/// Returns `true` if this style was applied.
pub(super) fn maybe_assign_gsub_output_style(&mut self, style: &StyleClass) -> bool {
let style_ix = style.index as u16;
if self.0 & Self::FROM_GSUB_OUTPUT != 0 && self.is_unassigned() {
self.clear_from_gsub();
self.0 = (self.0 & !Self::STYLE_INDEX_MASK) | style_ix;
true
} else {
false
}
}
}
impl Default for GlyphStyle {
fn default() -> Self {
Self(Self::UNASSIGNED)
}
}
/// Sentinel for unused styles in [`GlyphStyleMap::metrics_map`].
const UNMAPPED_STYLE: u8 = 0xFF;
/// Maps glyph identifiers to glyph styles.
///
/// Also keeps track of the styles that are actually used so we can allocate
/// an appropriately sized metrics array.
#[derive(Debug)]
pub(crate) struct GlyphStyleMap {
/// List of styles, indexed by glyph id.
styles: Vec<GlyphStyle>,
/// Maps an actual style class index into a compacted index for the
/// metrics table.
///
/// Uses `0xFF` to signify unused styles.
metrics_map: [u8; MAX_STYLES],
/// Number of metrics styles in use.
metrics_count: u8,
}
impl GlyphStyleMap {
/// Computes a new glyph style map for the given glyph count and character
/// map.
///
/// Roughly based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L126>
pub fn new(glyph_count: u32, shaper: &Shaper) -> Self {
let lookup_count = shaper.lookup_count() as usize;
if lookup_count > 0 {
// If we're processing lookups, allocate some temporary memory to
// store the visited set
let lookup_set_byte_size = lookup_count.div_ceil(8);
super::super::memory::with_temporary_memory(lookup_set_byte_size, |bytes| {
Self::new_inner(glyph_count, shaper, VisitedLookupSet::new(bytes))
})
} else {
Self::new_inner(glyph_count, shaper, VisitedLookupSet::new(&mut []))
}
}
fn new_inner(glyph_count: u32, shaper: &Shaper, mut visited_set: VisitedLookupSet) -> Self {
let mut map = Self {
styles: vec![GlyphStyle::default(); glyph_count as usize],
metrics_map: [UNMAPPED_STYLE; MAX_STYLES],
metrics_count: 0,
};
// Step 1: compute styles for glyphs covered by OpenType features
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L233>
for style in super::style::STYLE_CLASSES {
if style.feature.is_some()
&& shaper.compute_coverage(
style,
ShaperCoverageKind::Script,
&mut map.styles,
&mut visited_set,
)
{
map.use_style(style.index);
}
}
// Step 2: compute styles for glyphs contained in the cmap
// 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, StyleRange)> = None;
for (ch, gid) in shaper.charmap().mappings() {
let Some(style) = map.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.maybe_assign(last.1.style);
continue;
}
}
let ix = match STYLE_RANGES.binary_search_by(|x| x.first.cmp(&ch)) {
Ok(i) => i,
Err(i) => i.saturating_sub(1),
};
let Some(range) = STYLE_RANGES.get(ix).copied() else {
continue;
};
if range.contains(ch) {
style.maybe_assign(range.style);
if let Some(style_ix) = range.style.style_index() {
map.use_style(style_ix as usize);
}
last_range = Some((ix, range));
}
}
// Step 3a: compute script based coverage
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L239>
for style in super::style::STYLE_CLASSES {
if style.feature.is_none()
&& shaper.compute_coverage(
style,
ShaperCoverageKind::Script,
&mut map.styles,
&mut visited_set,
)
{
map.use_style(style.index);
}
}
// Step 3b: compute coverage for "default" script which is always set
// to Latin in FreeType
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L248>
let default_style = &STYLE_CLASSES[StyleClass::LATN];
if shaper.compute_coverage(
default_style,
ShaperCoverageKind::Default,
&mut map.styles,
&mut visited_set,
) {
map.use_style(default_style.index);
}
// Step 4: Assign a default to all remaining glyphs
// For some reason, FreeType uses Hani as a default fallback style so
// let's do the same.
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.h#L69>
let mut need_hani = false;
for style in map.styles.iter_mut() {
if style.is_unassigned() {
style.0 &= !GlyphStyle::STYLE_INDEX_MASK;
style.0 |= StyleClass::HANI as u16;
need_hani = true;
}
}
if need_hani {
map.use_style(StyleClass::HANI);
}
// Step 5: Mark ASCII digits
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L251>
for digit_char in '0'..='9' {
if let Some(style) = shaper
.charmap()
.map(digit_char)
.and_then(|gid| map.styles.get_mut(gid.to_u32() as usize))
{
style.0 |= GlyphStyle::DIGIT;
}
}
map
}
pub fn style(&self, glyph_id: GlyphId) -> Option<GlyphStyle> {
self.styles.get(glyph_id.to_u32() as usize).copied()
}
/// Returns a compacted metrics index for the given glyph style.
pub fn metrics_index(&self, style: GlyphStyle) -> Option<usize> {
let ix = style.style_index()? as usize;
let metrics_ix = *self.metrics_map.get(ix)? as usize;
if metrics_ix != UNMAPPED_STYLE as usize {
Some(metrics_ix)
} else {
None
}
}
/// Returns the required size of the compacted metrics array.
pub fn metrics_count(&self) -> usize {
self.metrics_count as usize
}
/// Returns an ordered iterator yielding each style class referenced by
/// this map.
pub fn metrics_styles(&self) -> impl Iterator<Item = &'static StyleClass> + '_ {
// Need to build a reverse map so that these are properly ordered
let mut reverse_map = [UNMAPPED_STYLE; MAX_STYLES];
for (ix, &entry) in self.metrics_map.iter().enumerate() {
if entry != UNMAPPED_STYLE {
reverse_map[entry as usize] = ix as u8;
}
}
reverse_map
.into_iter()
.enumerate()
.filter_map(move |(mapped, style_ix)| {
if mapped == UNMAPPED_STYLE as usize {
None
} else {
STYLE_CLASSES.get(style_ix as usize)
}
})
}
fn use_style(&mut self, style_ix: usize) {
let mapped = &mut self.metrics_map[style_ix];
if *mapped == UNMAPPED_STYLE {
// This the first time we've seen this style so record
// it in the metrics map
*mapped = self.metrics_count;
self.metrics_count += 1;
}
}
}
impl Default for GlyphStyleMap {
fn default() -> Self {
Self {
styles: Default::default(),
metrics_map: [UNMAPPED_STYLE; MAX_STYLES],
metrics_count: 0,
}
}
}
/// Determines which algorithms the autohinter will use while generating
/// metrics and processing a glyph outline.
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub(crate) enum ScriptGroup {
/// All scripts that are not CJK or Indic.
///
/// FreeType calls this Latin.
#[default]
Default,
Cjk,
Indic,
}
/// Defines the basic properties for each script supported by the
/// autohinter.
#[derive(Clone, Debug)]
pub(crate) struct ScriptClass {
#[allow(unused)]
pub name: &'static str,
/// Group that defines how glyphs belonging to this script are hinted.
pub group: ScriptGroup,
/// Unicode tag for the script.
#[allow(unused)]
pub tag: Tag,
/// True if outline edges are processed top to bottom.
pub hint_top_to_bottom: bool,
/// Characters used to define standard width and height of stems.
pub std_chars: &'static str,
/// "Blue" characters used to define alignment zones.
pub blues: &'static [(&'static str, BlueZones)],
}
/// Defines the basic properties for each style supported by the
/// autohinter.
///
/// There's mostly a 1:1 correspondence between styles and scripts except
/// in the cases where style coverage is determined by OpenType feature
/// coverage.
#[derive(Clone, Debug)]
pub(crate) struct StyleClass {
#[allow(unused)]
pub name: &'static str,
/// Index of self in the STYLE_CLASSES array.
pub index: usize,
/// Associated Unicode script.
pub script: &'static ScriptClass,
/// OpenType feature tag for styles that derive coverage from layout
/// tables.
#[allow(unused)]
pub feature: Option<Tag>,
}
impl StyleClass {
pub(crate) fn from_index(index: u16) -> Option<&'static StyleClass> {
STYLE_CLASSES.get(index as usize)
}
}
/// Associates a basic glyph style with a range of codepoints.
#[derive(Copy, Clone, Debug)]
pub(super) struct StyleRange {
pub first: u32,
pub last: u32,
pub style: GlyphStyle,
}
impl StyleRange {
pub fn contains(&self, ch: u32) -> bool {
(self.first..=self.last).contains(&ch)
}
}
// The following are helpers for generated code.
const fn base_range(first: u32, last: u32, style_index: u16) -> StyleRange {
StyleRange {
first,
last,
style: GlyphStyle(style_index),
}
}
const fn non_base_range(first: u32, last: u32, style_index: u16) -> StyleRange {
StyleRange {
first,
last,
style: GlyphStyle(style_index | GlyphStyle::NON_BASE),
}
}
const MAX_STYLES: usize = STYLE_CLASSES.len();
use super::shape::Shaper;
include!("../../../generated/generated_autohint_styles.rs");
#[cfg(test)]
mod tests {
use super::{super::shape::ShaperMode, *};
use crate::{raw::TableProvider, FontRef, MetadataProvider};
/// Ensure that style mapping accurately applies the DIGIT bit to
/// ASCII digit glyphs.
#[test]
fn capture_digit_styles() {
let font = FontRef::new(font_test_data::AHEM).unwrap();
let shaper = Shaper::new(&font, ShaperMode::Nominal);
let num_glyphs = font.maxp().unwrap().num_glyphs() as u32;
let style_map = GlyphStyleMap::new(num_glyphs, &shaper);
let charmap = font.charmap();
let mut digit_count = 0;
for (ch, gid) in charmap.mappings() {
let style = style_map.style(gid).unwrap();
let is_char_digit = char::from_u32(ch).unwrap().is_ascii_digit();
assert_eq!(style.is_digit(), is_char_digit);
digit_count += is_char_digit as u32;
}
// This font has all 10 ASCII digits
assert_eq!(digit_count, 10);
}
#[test]
fn glyph_styles() {
// generated by printf debugging in FreeType
// (gid, Option<(script_name, is_non_base_char)>)
// where "is_non_base_char" more common means "is_mark"
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))),
];
check_styles(font_test_data::AUTOHINT_CMAP, ShaperMode::Nominal, expected);
}
#[test]
fn shaped_glyph_styles() {
// generated by printf debugging in FreeType
// (gid, Option<(script_name, is_non_base_char)>)
// where "is_non_base_char" more common means "is_mark"
let expected = &[
(0, Some(("CJKV ideographs", false))),
(1, Some(("Latin", false))),
(2, Some(("Latin", false))),
(3, Some(("Latin", false))),
(4, Some(("Latin", false))),
// Note: ligatures starting with 'f' are assigned the Cyrillic
// script which matches FreeType
(5, Some(("Cyrillic", false))),
(6, Some(("Cyrillic", false))),
(7, Some(("Cyrillic", false))),
// Capture the Latin c2sc feature
(8, Some(("Latin small capitals from capitals", false))),
];
check_styles(
font_test_data::NOTOSERIF_AUTOHINT_SHAPING,
ShaperMode::BestEffort,
expected,
);
}
fn check_styles(font_data: &[u8], mode: ShaperMode, expected: &[(u32, Option<(&str, bool)>)]) {
let font = FontRef::new(font_data).unwrap();
let shaper = Shaper::new(&font, mode);
let num_glyphs = font.maxp().unwrap().num_glyphs() as u32;
let style_map = GlyphStyleMap::new(num_glyphs, &shaper);
let results = style_map
.styles
.iter()
.enumerate()
.map(|(gid, style)| {
(
gid as u32,
style
.style_class()
.map(|style_class| (style_class.name, style.is_non_base())),
)
})
.collect::<Vec<_>>();
for (i, result) in results.iter().enumerate() {
assert_eq!(result, &expected[i]);
}
// Ensure each style has a remapped metrics index
for style in &style_map.styles {
style_map.metrics_index(*style).unwrap();
}
}
}