Files
vectorcraft/crates/text/src/fontdb/alternates.rs
T
2026-11-04 22:29:53 +07:00

166 lines
8.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The alternate glyphs a face's `GSUB` table gives one character: the Glyphs panel's Show ›
//! Alternates for Current Selection. The application finds the character's default glyph in the
//! `cmap` and looks it up in the coverage of each lookup of the features that substitute one glyph
//! for another, as the OpenType registry describes `aalt`.
//! <https://learn.microsoft.com/en-us/typography/opentype/spec/features_ae#tag-aalt>
//! <https://learn.microsoft.com/en-us/typography/opentype/spec/features_pt>
use skrifa::MetadataProvider;
use skrifa::raw::TableProvider;
use skrifa::raw::tables::gsub::{AlternateSubstFormat1, SingleSubst, SubstitutionSubtables};
use vectorcraft_doc::GlyphChoice;
use super::FontFace;
/// One glyph [`FontFace::alternates`] lists: its id and the feature it was found in (`aalt`).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct GlyphAlternate {
pub gid: u32,
pub feature: [u8; 4],
}
/// Caps on what a damaged or very large `GSUB` table can make [`FontFace::alternates`] read:
/// feature records, lookups per feature and in all, subtables per lookup and in all, glyphs per
/// alternate set and glyphs listed in all.
const MAX_FEATURES: usize = 4096;
const MAX_FEATURE_LOOKUPS: usize = 256;
const MAX_LOOKUPS: usize = 4096;
const MAX_SUBTABLES: usize = 256;
const MAX_SUBTABLE_READS: usize = 16384;
const MAX_SET: usize = 512;
const MAX_ALTERNATES: usize = 1024;
/// The other discretionary features that substitute one glyph for another (historical forms,
/// justification alternates, alternate annotation forms, ornaments, titling), after `cv99`, in
/// tag order.
const OTHER_FEATURES: [&[u8; 4]; 5] = [b"hist", b"jalt", b"nalt", b"ornm", b"titl"];
/// Where the glyphs of feature `tag` come in the list: `aalt`, `salt`, `ss01` to `ss20`, `swsh`,
/// `cv01` to `cv99`, then [`OTHER_FEATURES`]. `None` for a feature that is not listed.
fn feature_rank(tag: [u8; 4]) -> Option<(u8, u8)> {
let number = |t: &[u8]| std::str::from_utf8(t).ok().filter(|s| s.bytes().all(|b| b.is_ascii_digit())).and_then(|s| s.parse::<u8>().ok());
match &tag {
b"aalt" => Some((0, 0)),
b"salt" => Some((1, 0)),
[b's', b's', ..] => number(&tag[2..]).filter(|n| (1..=20).contains(n)).map(|n| (2, n)),
b"swsh" => Some((3, 0)),
[b'c', b'v', ..] => number(&tag[2..]).filter(|n| (1..=99).contains(n)).map(|n| (4, n)),
_ => OTHER_FEATURES.iter().position(|t| **t == tag).map(|i| (5, i as u8)),
}
}
impl FontFace {
/// The face's alternate glyphs for `c`, from its `GSUB` table: the glyphs single substitutions
/// (lookup type 1) and alternate substitutions (type 3, the whole set in the font's order), in
/// extension lookups (type 7) too, put in place of `c`'s default glyph. `aalt` comes first,
/// then `salt`, `ss01` to `ss20`, `swsh`, `cv01` to `cv99`, `hist`, `jalt`, `nalt`, `ornm` and
/// `titl`; within a feature its lookups go in lookup order. A glyph found again is left out.
/// The default glyph is listed only when `aalt` lists it. Ligatures and contextual
/// substitutions are not read. Empty when the face does not map `c`, has no `GSUB` table, or
/// the table cannot be read; glyph ids past the face's glyph count are left out.
pub fn alternates(&self, c: char) -> Vec<GlyphAlternate> {
let mut out: Vec<GlyphAlternate> = vec![];
let Some(f) = self.skrifa() else { return out };
let Some(default) = f.charmap().map(c) else { return out };
let Ok(gsub) = f.gsub() else { return out };
let (Ok(features), Ok(lookups)) = (gsub.feature_list(), gsub.lookup_list()) else { return out };
let glyphs = f.maxp().map_or(0, |m| u32::from(m.num_glyphs()));
// The lookups of the listed features, by rank and lookup index.
// The listed feature records are read in rank order, so the cap on lookups in all leaves
// out the lowest ranked ones.
let records = features.feature_records();
let mut ranked: Vec<((u8, u8), usize, [u8; 4])> = records
.iter()
.take(MAX_FEATURES)
.enumerate()
.filter_map(|(i, r)| {
let tag = r.feature_tag().to_be_bytes();
feature_rank(tag).map(|rank| (rank, i, tag))
})
.collect();
ranked.sort_unstable_by_key(|r| (r.0, r.1));
let mut wanted: Vec<((u8, u8), u16, [u8; 4])> = vec![];
for (rank, i, tag) in ranked {
let room = MAX_LOOKUPS.saturating_sub(wanted.len());
if room == 0 {
break;
}
let Some(record) = records.get(i) else { continue };
let Ok(feature) = record.feature(features.offset_data()) else { continue };
wanted.extend(feature.lookup_list_indices().iter().take(MAX_FEATURE_LOOKUPS.min(room)).map(|i| (rank, i.get(), tag)));
}
wanted.sort_unstable_by_key(|w| (w.0, w.1));
wanted.dedup_by_key(|w| (w.0, w.1));
let mut push = |gid: u32, feature: [u8; 4]| {
let listed = gid != default.to_u32() || feature == *b"aalt";
if listed && gid < glyphs && out.len() < MAX_ALTERNATES && !out.iter().any(|a| a.gid == gid) {
out.push(GlyphAlternate { gid, feature });
}
};
let all = lookups.lookups();
let mut reads = MAX_SUBTABLE_READS;
for (_, index, tag) in wanted {
if reads == 0 {
break;
}
let Ok(subtables) = all.get(usize::from(index)).and_then(|l| l.subtables()) else { continue };
let n = MAX_SUBTABLES.min(reads);
// A glyph takes the first subtable of a lookup whose coverage holds it.
match subtables {
SubstitutionSubtables::Single(s) => {
if let Some(gid) = s.iter().take(n).inspect(|_| reads -= 1).flatten().find_map(|t| single(&t, default.to_u32())) {
push(gid, tag);
}
}
SubstitutionSubtables::Alternate(s) => {
if let Some(set) = s.iter().take(n).inspect(|_| reads -= 1).flatten().find_map(|t| alternate_set(&t, default.to_u32())) {
set.into_iter().for_each(|gid| push(gid, tag));
}
}
_ => {}
}
}
out
}
/// How many glyphs the face has (its `maxp` table; 0 when it can't be read).
pub fn glyph_count(&self) -> u32 {
self.skrifa().and_then(|f| f.maxp().ok()).map_or(0, |m| u32::from(m.num_glyphs()))
}
/// Does this face draw `choice` for character `c`? It does when the choice is for `c`, names
/// this face's PostScript name and version, and its glyph id is one of the face's glyphs.
pub fn draws(&self, choice: &GlyphChoice, c: char) -> bool {
choice.ch == c && choice.version == self.version && choice.gid < self.glyph_count() && choice.font == self.postscript_name()
}
/// `c` drawn with glyph `gid` of this face ([`CharStyle::glyph_alternate`]).
///
/// [`CharStyle::glyph_alternate`]: vectorcraft_doc::CharStyle::glyph_alternate
pub fn choice(&self, c: char, gid: u32) -> GlyphChoice {
GlyphChoice { ch: c, gid, font: self.postscript_name(), version: self.version.clone() }
}
}
/// The glyph single substitution `t` puts in place of `gid`, when its coverage holds `gid`.
fn single(t: &SingleSubst<'_>, gid: u32) -> Option<u32> {
match t {
SingleSubst::Format1(t) => {
t.coverage().ok()?.get(skrifa::GlyphId::new(gid))?;
// The delta is added modulo 65536.
Some((gid as i32 + i32::from(t.delta_glyph_id())).rem_euclid(0x1_0000) as u32)
}
SingleSubst::Format2(t) => {
let i = t.coverage().ok()?.get(skrifa::GlyphId::new(gid))?;
t.substitute_glyph_ids().get(usize::from(i)).map(|g| u32::from(g.get().to_u16()))
}
}
}
/// The alternate set of alternate substitution `t` for `gid`, when its coverage holds `gid`.
fn alternate_set(t: &AlternateSubstFormat1<'_>, gid: u32) -> Option<Vec<u32>> {
let i = t.coverage().ok()?.get(skrifa::GlyphId::new(gid))?;
let set = t.alternate_sets().get(usize::from(i)).ok()?;
Some(set.alternate_glyph_ids().iter().take(MAX_SET).map(|g| u32::from(g.get().to_u16())).collect())
}