blob: b77f4795781883ba455842226f4e343c19f8321c [file] [log] [blame] [edit]
//! try to define Subset trait so I can add methods for Hmtx
//! TODO: make it generic for all tables
mod base;
mod cblc;
mod cmap;
mod colr;
mod cpal;
mod fvar;
mod gdef;
mod glyf_loca;
mod gpos;
mod gsub;
mod gvar;
mod hdmx;
mod head;
mod hmtx;
mod hvar;
mod inc_bimap;
mod layout;
mod maxp;
mod name;
mod offset;
mod offset_array;
mod os2;
mod parsing_util;
mod post;
mod sbix;
pub mod serialize;
mod stat;
mod variations;
mod vorg;
mod vvar;
use gdef::CollectUsedMarkSets;
use inc_bimap::IncBiMap;
use layout::{
collect_features_with_retained_subs, find_duplicate_features, prune_features,
PruneLangSysContext, SubsetLayoutContext,
};
pub use parsing_util::{
parse_name_ids, parse_name_languages, parse_tag_list, parse_unicodes, populate_gids,
};
use fnv::FnvHashMap;
use serialize::SerializeErrorFlags;
use serialize::Serializer;
use skrifa::MetadataProvider;
use thiserror::Error;
use write_fonts::types::GlyphId;
use write_fonts::types::Tag;
use write_fonts::{
read::{
collections::{int_set::Domain, IntSet},
tables::{
base::Base,
cbdt::Cbdt,
cblc::Cblc,
cff::Cff,
cff2::Cff2,
cmap::{Cmap, CmapSubtable},
colr::Colr,
cpal::Cpal,
cvar::Cvar,
gasp,
gdef::Gdef,
glyf::{Glyf, Glyph},
gpos::Gpos,
gsub::Gsub,
gvar::Gvar,
hdmx::Hdmx,
head::Head,
hvar::Hvar,
loca::Loca,
name::Name,
os2::Os2,
post::Post,
sbix::Sbix,
vorg::Vorg,
vvar::Vvar,
},
types::NameId,
FontRef, TableProvider, TopLevelTable,
},
tables::cmap::PlatformId,
};
use write_fonts::{tables::hhea::Hhea, tables::hmtx::Hmtx, tables::maxp::Maxp, FontBuilder};
const MAX_COMPOSITE_OPERATIONS_PER_GLYPH: u8 = 64;
const MAX_NESTING_LEVEL: u8 = 64;
// Support 24-bit gids. This should probably be extended to u32::MAX but
// this causes tests to fail with 'subtract with overflow error'.
// See <https://github.com/googlefonts/fontations/issues/997>
const MAX_GID: GlyphId = GlyphId::new(0xFFFFFFFF);
// ref: <https://github.com/harfbuzz/harfbuzz/blob/021b44388667903d7bc9c92c924ad079f13b90ce/src/hb-subset-input.cc#L82>
pub static DEFAULT_LAYOUT_FEATURES: &[Tag] = &[
// default shaper
// common
Tag::new(b"rvrn"),
Tag::new(b"ccmp"),
Tag::new(b"liga"),
Tag::new(b"locl"),
Tag::new(b"mark"),
Tag::new(b"mkmk"),
Tag::new(b"rlig"),
//fractions
Tag::new(b"frac"),
Tag::new(b"numr"),
Tag::new(b"dnom"),
// horizontal
Tag::new(b"calt"),
Tag::new(b"clig"),
Tag::new(b"curs"),
Tag::new(b"kern"),
Tag::new(b"rclt"),
//vertical
Tag::new(b"valt"),
Tag::new(b"vert"),
Tag::new(b"vkrn"),
Tag::new(b"vpal"),
Tag::new(b"vrt2"),
//ltr
Tag::new(b"ltra"),
Tag::new(b"ltrm"),
//rtl
Tag::new(b"rtla"),
Tag::new(b"rtlm"),
//random
Tag::new(b"rand"),
//justify
Tag::new(b"jalt"),
//east asian spacing
Tag::new(b"chws"),
Tag::new(b"vchw"),
Tag::new(b"halt"),
Tag::new(b"vhal"),
//private
Tag::new(b"Harf"),
Tag::new(b"HARF"),
Tag::new(b"Buzz"),
Tag::new(b"BUZZ"),
//complex shapers
//arabic
Tag::new(b"init"),
Tag::new(b"medi"),
Tag::new(b"fina"),
Tag::new(b"isol"),
Tag::new(b"med2"),
Tag::new(b"fin2"),
Tag::new(b"fin3"),
Tag::new(b"cswh"),
Tag::new(b"mset"),
Tag::new(b"stch"),
//hangul
Tag::new(b"ljmo"),
Tag::new(b"vjmo"),
Tag::new(b"tjmo"),
//tibetan
Tag::new(b"abvs"),
Tag::new(b"blws"),
Tag::new(b"abvm"),
Tag::new(b"blwm"),
//indic
Tag::new(b"nukt"),
Tag::new(b"akhn"),
Tag::new(b"rphf"),
Tag::new(b"rkrf"),
Tag::new(b"pref"),
Tag::new(b"blwf"),
Tag::new(b"half"),
Tag::new(b"abvf"),
Tag::new(b"pstf"),
Tag::new(b"cfar"),
Tag::new(b"vatu"),
Tag::new(b"cjct"),
Tag::new(b"init"),
Tag::new(b"pres"),
Tag::new(b"abvs"),
Tag::new(b"blws"),
Tag::new(b"psts"),
Tag::new(b"haln"),
Tag::new(b"dist"),
Tag::new(b"abvm"),
Tag::new(b"blwm"),
];
#[derive(Clone, Copy, Debug)]
pub struct SubsetFlags(u16);
impl SubsetFlags {
//all flags at their default value of false.
pub const SUBSET_FLAGS_DEFAULT: Self = Self(0x0000);
//If set hinting instructions will be dropped in the produced subset.
//Otherwise hinting instructions will be retained.
pub const SUBSET_FLAGS_NO_HINTING: Self = Self(0x0001);
//If set glyph indices will not be modified in the produced subset.
//If glyphs are dropped their indices will be retained as an empty glyph.
pub const SUBSET_FLAGS_RETAIN_GIDS: Self = Self(0x0002);
//If set and subsetting a CFF font the subsetter will attempt to remove subroutines from the CFF glyphs.
//This flag is UNIMPLEMENTED yet
pub const SUBSET_FLAGS_DESUBROUTINIZE: Self = Self(0x0004);
//If set non-unicode name records will be retained in the subset.
//This flag is UNIMPLEMENTED yet
pub const SUBSET_FLAGS_NAME_LEGACY: Self = Self(0x0008);
//If set the subsetter will set the OVERLAP_SIMPLE flag on each simple glyph.
pub const SUBSET_FLAGS_SET_OVERLAPS_FLAG: Self = Self(0x0010);
//If set the subsetter will not drop unrecognized tables and instead pass them through untouched.
//This flag is UNIMPLEMENTED yet
pub const SUBSET_FLAGS_PASSTHROUGH_UNRECOGNIZED: Self = Self(0x0020);
//If set the notdef glyph outline will be retained in the final subset.
pub const SUBSET_FLAGS_NOTDEF_OUTLINE: Self = Self(0x0040);
//If set the PS glyph names will be retained in the final subset.
//This flag is UNIMPLEMENTED yet
pub const SUBSET_FLAGS_GLYPH_NAMES: Self = Self(0x0080);
//If set then the unicode ranges in OS/2 will not be recalculated.
//This flag is UNIMPLEMENTED yet
pub const SUBSET_FLAGS_NO_PRUNE_UNICODE_RANGES: Self = Self(0x0100);
//If set don't perform glyph closure on layout substitution rules (GSUB)
//This flag is UNIMPLEMENTED yet
pub const SUBSET_FLAGS_NO_LAYOUT_CLOSURE: Self = Self(0x0200);
//If set perform IUP delta optimization on the remaining gvar table's deltas.
//This flag is UNIMPLEMENTED yet
pub const SUBSET_FLAGS_OPTIMIZE_IUP_DELTAS: Self = Self(0x0400);
/// Returns `true` if all of the flags in `other` are contained within `self`.
#[inline]
pub const fn contains(&self, other: Self) -> bool {
(self.0 & other.0) == other.0
}
}
impl Default for SubsetFlags {
fn default() -> Self {
Self::SUBSET_FLAGS_DEFAULT
}
}
impl PartialEq for SubsetFlags {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl std::ops::BitOr for SubsetFlags {
type Output = Self;
/// Returns the union of the two sets of flags.
#[inline]
fn bitor(self, other: SubsetFlags) -> Self {
Self(self.0 | other.0)
}
}
impl From<u16> for SubsetFlags {
fn from(value: u16) -> Self {
Self(value)
}
}
impl std::ops::BitOrAssign for SubsetFlags {
/// Adds the set of flags.
#[inline]
fn bitor_assign(&mut self, other: Self) {
self.0 |= other.0;
}
}
#[allow(dead_code)]
#[derive(Default)]
pub struct Plan {
unicodes: IntSet<u32>,
glyphs_requested: IntSet<GlyphId>,
glyphset_gsub: IntSet<GlyphId>,
glyphset_colred: IntSet<GlyphId>,
glyphset: IntSet<GlyphId>,
/// Old->New glyph id mapping,
glyph_map: FnvHashMap<GlyphId, GlyphId>,
// Old->New glyph id (in glyph_set_gsub) mapping
glyph_map_gsub: FnvHashMap<GlyphId, GlyphId>,
/// New->Old glyph id mapping,
reverse_glyph_map: FnvHashMap<GlyphId, GlyphId>,
new_to_old_gid_list: Vec<(GlyphId, GlyphId)>,
num_output_glyphs: usize,
font_num_glyphs: usize,
unicode_to_new_gid_list: Vec<(u32, GlyphId)>,
codepoint_to_glyph: FnvHashMap<u32, GlyphId>,
subset_flags: SubsetFlags,
no_subset_tables: IntSet<Tag>,
drop_tables: IntSet<Tag>,
name_ids: IntSet<NameId>,
name_languages: IntSet<u16>,
layout_scripts: IntSet<Tag>,
layout_features: IntSet<Tag>,
//active old->new feature index map after removing redundant langsys and prune_features
gsub_features: FnvHashMap<u16, u16>,
gpos_features: FnvHashMap<u16, u16>,
// active old->new lookup index map
gsub_lookups: FnvHashMap<u16, u16>,
gpos_lookups: FnvHashMap<u16, u16>,
// active script-langsys
gsub_script_langsys: FnvHashMap<u16, IntSet<u16>>,
gpos_script_langsys: FnvHashMap<u16, IntSet<u16>>,
// used_mark_sets mapping: old->new
used_mark_sets_map: FnvHashMap<u16, u16>,
//old->new colrv1 layer index map
colrv1_layers: FnvHashMap<u32, u32>,
//old->new CPAL palette index map
colr_palettes: FnvHashMap<u16, u16>,
// COLR varstore retained varidx mapping
colr_varstore_inner_maps: Vec<IncBiMap>,
// COLR table old variation index -> (New varidx, new delta) mapping
colr_varidx_delta_map: FnvHashMap<u32, (u32, i32)>,
// COLR table new delta set index -> new var index mapping
colr_new_deltaset_idx_varidx_map: FnvHashMap<u32, u32>,
os2_info: Os2Info,
//BASE table old variation index -> (New varidx, new delta) mapping
base_varidx_delta_map: FnvHashMap<u32, (u32, i32)>,
//BASE table varstore retained varidx mapping
base_varstore_inner_maps: Vec<IncBiMap>,
//Old layout item variation index -> (New varidx, delta) mapping
layout_varidx_delta_map: FnvHashMap<u32, (u32, i32)>,
//GDEF table varstore retained varidx mapping
gdef_varstore_inner_maps: Vec<IncBiMap>,
}
#[derive(Default)]
struct Os2Info {
min_cmap_codepoint: u32,
max_cmap_codepoint: u32,
}
impl Plan {
#[allow(clippy::too_many_arguments)]
pub fn new(
input_gids: &IntSet<GlyphId>,
input_unicodes: &IntSet<u32>,
font: &FontRef,
flags: SubsetFlags,
drop_tables: &IntSet<Tag>,
layout_scripts: &IntSet<Tag>,
layout_features: &IntSet<Tag>,
name_ids: &IntSet<NameId>,
name_languages: &IntSet<u16>,
) -> Self {
let mut this = Plan {
glyphs_requested: input_gids.clone(),
font_num_glyphs: get_font_num_glyphs(font),
subset_flags: flags,
drop_tables: drop_tables.clone(),
layout_scripts: layout_scripts.clone(),
layout_features: layout_features.clone(),
name_ids: name_ids.clone(),
name_languages: name_languages.clone(),
..Default::default()
};
// ref: <https://github.com/harfbuzz/harfbuzz/blob/b5a65e0f20c30a7f13b2f6619479a6d666e603e0/src/hb-subset-input.cc#L71>
let default_no_subset_tables = [gasp::Gasp::TAG, FPGM, PREP, VDMX, DSIG];
this.no_subset_tables
.extend(default_no_subset_tables.iter().copied());
this.populate_unicodes_to_retain(input_gids, input_unicodes, font);
this.populate_gids_to_retain(font);
this.create_old_gid_to_new_gid_map();
this.create_glyph_map_gsub();
//update the unicode to new gid list
let num = this.unicode_to_new_gid_list.len();
for i in 0..num {
let old_gid = this.unicode_to_new_gid_list[i].1;
let new_gid = this.glyph_map.get(&old_gid).unwrap();
this.unicode_to_new_gid_list[i].1 = *new_gid;
}
this.collect_base_var_indices(font);
this
}
fn populate_unicodes_to_retain(
&mut self,
input_gids: &IntSet<GlyphId>,
input_unicodes: &IntSet<u32>,
font: &FontRef,
) {
let charmap = font.charmap();
if input_gids.is_empty() && input_unicodes.len() < (self.font_num_glyphs as u64) {
let cap: usize = input_unicodes.len().try_into().unwrap_or(usize::MAX);
self.unicode_to_new_gid_list.reserve(cap);
self.codepoint_to_glyph.reserve(cap);
//TODO: add support for subset accelerator?
for cp in input_unicodes.iter() {
match charmap.map(cp) {
Some(gid) => {
self.codepoint_to_glyph.insert(cp, gid);
self.unicode_to_new_gid_list.push((cp, gid));
}
None => {
continue;
}
}
}
} else {
//TODO: add support for subset accelerator?
let cmap_unicodes = charmap.mappings().map(|t| t.0).collect::<IntSet<u32>>();
let unicode_gid_map = charmap.mappings().collect::<FnvHashMap<u32, GlyphId>>();
let vec_cap: u64 = input_gids.len() + input_unicodes.len();
let vec_cap: usize = vec_cap
.min(cmap_unicodes.len())
.try_into()
.unwrap_or(usize::MAX);
self.codepoint_to_glyph.reserve(vec_cap);
self.unicode_to_new_gid_list.reserve(vec_cap);
for range in cmap_unicodes.iter_ranges() {
for cp in range {
match unicode_gid_map.get(&cp) {
Some(gid) => {
if !input_gids.contains(*gid) && !input_unicodes.contains(cp) {
continue;
}
self.codepoint_to_glyph.insert(cp, *gid);
self.unicode_to_new_gid_list.push((cp, *gid));
}
None => {
continue;
}
}
}
}
/* Add gids which where requested, but not mapped in cmap */
for range in input_gids.iter_ranges() {
if range.start().to_u32() as usize >= self.font_num_glyphs {
break;
}
let mut last = range.end().to_u32() as usize;
if last >= self.font_num_glyphs {
last = self.font_num_glyphs - 1;
}
self.glyphset_gsub
.insert_range(*range.start()..=GlyphId::from(last as u32));
}
}
self.unicode_to_new_gid_list.sort();
self.glyphset_gsub
.extend(self.unicode_to_new_gid_list.iter().map(|t| t.1));
self.unicodes
.extend(self.unicode_to_new_gid_list.iter().map(|t| t.0));
// ref: <https://github.com/harfbuzz/harfbuzz/blob/e451e91ec3608a2ebfec34d0c4f0b3d880e00e33/src/hb-subset-plan.cc#L802>
self.os2_info.min_cmap_codepoint = self.unicodes.first().unwrap_or(0xFFFF_u32);
self.os2_info.max_cmap_codepoint = self.unicodes.last().unwrap_or(0xFFFF_u32);
self.collect_variation_selectors(font, input_unicodes);
}
fn collect_variation_selectors(&mut self, font: &FontRef, input_unicodes: &IntSet<u32>) {
if let Ok(cmap) = font.cmap() {
let encoding_records = cmap.encoding_records();
if let Ok(i) = encoding_records.binary_search_by(|r| {
if r.platform_id() != PlatformId::Unicode {
r.platform_id().cmp(&PlatformId::Unicode)
} else if r.encoding_id() != 5 {
r.encoding_id().cmp(&5)
} else {
std::cmp::Ordering::Equal
}
}) {
if let Ok(CmapSubtable::Format14(cmap14)) = encoding_records
.get(i)
.unwrap()
.subtable(cmap.offset_data())
{
self.unicodes.extend(
cmap14
.var_selector()
.iter()
.map(|s| s.var_selector().to_u32())
.filter(|v| input_unicodes.contains(*v)),
);
}
}
}
}
fn populate_gids_to_retain(&mut self, font: &FontRef) {
//not-def
self.glyphset_gsub.insert(GlyphId::NOTDEF);
//glyph closure for cmap
let cmap = font.cmap().expect("Error reading cmap table");
cmap.closure_glyphs(&self.unicodes, &mut self.glyphset_gsub);
remove_invalid_gids(&mut self.glyphset_gsub, self.font_num_glyphs);
// layout closure
self.layout_populate_gids_to_retain(font);
//skip glyph closure for MATH table, it's not supported yet
//glyph closure for COLR
if !self.drop_tables.contains(Tag::new(b"COLR")) {
self.colr_closure(font);
remove_invalid_gids(&mut self.glyphset_colred, self.font_num_glyphs);
} else {
self.glyphset_colred = self.glyphset_gsub.clone();
}
/* Populate a full set of glyphs to retain by adding all referenced composite glyphs. */
if let Ok(loca) = font.loca(None) {
let glyf = font.glyf().expect("Error reading glyf table");
let operation_count =
self.glyphset_gsub.len() * (MAX_COMPOSITE_OPERATIONS_PER_GLYPH as u64);
for gid in self.glyphset_colred.iter() {
glyf_closure_glyphs(
&loca,
&glyf,
gid,
&mut self.glyphset,
operation_count as i32,
0,
);
}
remove_invalid_gids(&mut self.glyphset, self.font_num_glyphs);
} else {
self.glyphset = self.glyphset_colred.clone();
}
self.nameid_closure(font);
self.collect_layout_var_indices(font);
}
fn layout_populate_gids_to_retain(&mut self, font: &FontRef) {
if !self.drop_tables.contains(Tag::new(b"GSUB")) {
if let Ok(gsub) = font.gsub() {
gsub.closure_glyphs_lookups_features(self);
}
}
if !self.drop_tables.contains(Tag::new(b"GPOS")) {
if let Ok(gpos) = font.gpos() {
gpos.closure_glyphs_lookups_features(self);
}
}
}
fn create_old_gid_to_new_gid_map(&mut self) {
let pop = self.glyphset.len();
self.glyph_map.reserve(pop as usize);
self.reverse_glyph_map.reserve(pop as usize);
self.new_to_old_gid_list.reserve(pop as usize);
//TODO: Add support for requested_glyph_map, command line option --gid-map
if !self
.subset_flags
.contains(SubsetFlags::SUBSET_FLAGS_RETAIN_GIDS)
{
self.new_to_old_gid_list.extend(
self.glyphset
.iter()
.zip(0u16..)
.map(|x| (GlyphId::from(x.1), x.0)),
);
self.num_output_glyphs = self.new_to_old_gid_list.len();
} else {
self.new_to_old_gid_list
.extend(self.glyphset.iter().map(|x| (x, x)));
let Some(max_glyph) = self.glyphset.last() else {
return;
};
self.num_output_glyphs = max_glyph.to_u32() as usize + 1;
}
self.glyph_map
.extend(self.new_to_old_gid_list.iter().map(|x| (x.1, x.0)));
self.reverse_glyph_map
.extend(self.new_to_old_gid_list.iter().map(|x| (x.0, x.1)));
}
fn create_glyph_map_gsub(&mut self) {
let map: FnvHashMap<GlyphId, GlyphId> = self
.glyphset_gsub
.iter()
.filter_map(|g| self.glyph_map.get(&g).map(|new_gid| (g, *new_gid)))
.collect();
let _ = std::mem::replace(&mut self.glyph_map_gsub, map);
}
fn colr_closure(&mut self, font: &FontRef) {
if let Ok(colr) = font.colr() {
colr.v0_closure_glyphs(&self.glyphset_gsub, &mut self.glyphset_colred);
let mut layer_indices = IntSet::empty();
let mut palette_indices = IntSet::empty();
let mut variation_indices = IntSet::empty();
colr.v1_closure(
&mut self.glyphset_colred,
&mut layer_indices,
&mut palette_indices,
&mut variation_indices,
);
colr.v0_closure_palette_indices(&self.glyphset_colred, &mut palette_indices);
let _ = std::mem::replace(&mut self.colrv1_layers, remap_indices(layer_indices));
let _ = std::mem::replace(
&mut self.colr_palettes,
remap_palette_indices(palette_indices),
);
if variation_indices.is_empty() {
return;
}
// generate 3 maps:
// colr_varidx_delta_map
// When delta set index map is not included, it's a mapping from varIdx-> (new varIdx,delta).
// Otherwise, it's a mapping from old delta set idx-> (new delta set idx, delta).
// Mapping delta set indices is the same as gid mapping.
//
// colr_varstore_inner_maps:
// mapping from old varidx -> new varidx
//
// colr_new_deltaset_idx_varidx_map:
// generate new delta set idx-> new var_idx map if DeltsSetIndexMap exists
if let Some(Ok(var_store)) = colr.item_variation_store() {
let vardata_count = var_store.item_variation_data_count() as u32;
let Ok(var_index_map) = colr.var_index_map().transpose() else {
return;
};
let mut delta_set_indices = IntSet::empty();
let mut deltaset_idx_var_idx_map = FnvHashMap::default();
// when a DeltaSetIndexMap is included, collected variation indices are actually delta set indices,
// we need to map them into variation indices
if var_index_map.is_some() {
let var_index_map = var_index_map.as_ref().unwrap();
delta_set_indices.extend(variation_indices.iter());
variation_indices.clear();
for idx in delta_set_indices.iter() {
if let Ok(var_idx) = var_index_map.get(idx) {
let var_idx = ((var_idx.outer as u32) << 16) + var_idx.inner as u32;
variation_indices.insert(var_idx);
deltaset_idx_var_idx_map.insert(idx, var_idx);
}
}
}
remap_variation_indices(
vardata_count,
&variation_indices,
&mut self.colr_varidx_delta_map,
);
generate_varstore_inner_maps(
&variation_indices,
vardata_count,
&mut self.colr_varstore_inner_maps,
);
// if DeltaSetIndexMap exists, we need to use deltaset index instead of var_idx
if var_index_map.is_some() {
let (new_deltaset_idx_varidx_map, deltaset_idx_delta_map) =
remap_delta_set_indices(
&delta_set_indices,
&deltaset_idx_var_idx_map,
&self.colr_varidx_delta_map,
);
let _ = std::mem::replace(
&mut self.colr_new_deltaset_idx_varidx_map,
new_deltaset_idx_varidx_map,
);
let _ =
std::mem::replace(&mut self.colr_varidx_delta_map, deltaset_idx_delta_map);
}
}
} else {
self.glyphset_colred.union(&self.glyphset_gsub);
}
}
fn nameid_closure(&mut self, font: &FontRef) {
if !self.drop_tables.contains(Tag::new(b"STAT")) {
if let Ok(stat) = font.stat() {
stat.collect_name_ids(self);
}
};
//TODO: skip fvar table when all axes are pinned
if !self.drop_tables.contains(Tag::new(b"fvar")) {
if let Ok(fvar) = font.fvar() {
fvar.collect_name_ids(self);
}
}
if !self.drop_tables.contains(Tag::new(b"CPAL")) {
if let Ok(cpal) = font.cpal() {
cpal.collect_name_ids(self);
}
}
if !self.drop_tables.contains(Tag::new(b"GSUB")) {
if let Ok(gsub) = font.gsub() {
gsub.collect_name_ids(self);
}
}
if !self.drop_tables.contains(Tag::new(b"GPOS")) {
if let Ok(gpos) = font.gpos() {
gpos.collect_name_ids(self);
}
}
}
fn collect_layout_var_indices(&mut self, font: &FontRef) {
if self.drop_tables.contains(Tag::new(b"GDEF")) {
return;
}
let Ok(gdef) = font.gdef() else {
return;
};
let mut used_mark_sets = IntSet::empty();
gdef.collect_used_mark_sets(self, &mut used_mark_sets);
let _ = std::mem::replace(&mut self.used_mark_sets_map, remap_indices(used_mark_sets));
let Some(Ok(var_store)) = gdef.item_var_store() else {
return;
};
let mut varidx_set = IntSet::empty();
gdef.collect_variation_indices(self, &mut varidx_set);
//TODO: collect variation indices from GPOS
let vardata_count = var_store.item_variation_data_count() as u32;
remap_variation_indices(
vardata_count,
&varidx_set,
&mut self.layout_varidx_delta_map,
);
generate_varstore_inner_maps(
&varidx_set,
vardata_count,
&mut self.gdef_varstore_inner_maps,
);
}
fn collect_base_var_indices(&mut self, font: &FontRef) {
if self.drop_tables.contains(Tag::new(b"BASE")) {
return;
}
if font.fvar().is_err() {
return;
}
let Ok(base) = font.base() else {
return;
};
let Some(Ok(var_store)) = base.item_var_store() else {
return;
};
let mut varidx_set = IntSet::empty();
{
base.collect_variation_indices(self, &mut varidx_set);
}
let vardata_count = var_store.item_variation_data_count() as u32;
remap_variation_indices(vardata_count, &varidx_set, &mut self.base_varidx_delta_map);
generate_varstore_inner_maps(
&varidx_set,
vardata_count,
&mut self.base_varstore_inner_maps,
);
}
}
// TODO: when instancing, calculate delta value and set new varidx to NO_VARIATIONS_IDX if all axes are pinned
fn remap_variation_indices(
vardata_count: u32,
varidx_set: &IntSet<u32>,
varidx_delta_map: &mut FnvHashMap<u32, (u32, i32)>,
) {
if vardata_count == 0 || varidx_set.is_empty() {
return;
}
let mut new_major: u32 = 0;
let mut new_minor: u32 = 0;
let mut last_major = varidx_set.first().unwrap() >> 16;
for var_idx in varidx_set.iter() {
let major = var_idx >> 16;
if major >= vardata_count {
break;
}
if major != last_major {
new_minor = 0;
new_major += 1;
}
let new_idx = (new_major << 16) + new_minor;
varidx_delta_map.insert(var_idx, (new_idx, 0));
new_minor += 1;
last_major = major;
}
}
fn generate_varstore_inner_maps(
varidx_set: &IntSet<u32>,
vardata_count: u32,
inner_maps: &mut Vec<IncBiMap>,
) {
if varidx_set.is_empty() || vardata_count == 0 {
return;
}
inner_maps.resize_with(vardata_count as usize, Default::default);
for idx in varidx_set.iter() {
let major = idx >> 16;
let minor = idx & 0xFFFF;
if major >= vardata_count {
break;
}
inner_maps[major as usize].add(minor);
}
}
//
fn remap_delta_set_indices(
delta_set_indices: &IntSet<u32>,
deltaset_idx_var_idx_map: &FnvHashMap<u32, u32>,
varidx_delta_map: &FnvHashMap<u32, (u32, i32)>,
) -> (FnvHashMap<u32, u32>, FnvHashMap<u32, (u32, i32)>) {
let mut new_deltaset_idx_varidx_map = FnvHashMap::default();
let mut deltaset_idx_delta_map = FnvHashMap::default();
let mut new_idx = 0_u32;
for deltaset_idx in delta_set_indices.iter() {
let Some(var_idx) = deltaset_idx_var_idx_map.get(&deltaset_idx) else {
continue;
};
let Some((new_var_idx, delta)) = varidx_delta_map.get(var_idx) else {
continue;
};
new_deltaset_idx_varidx_map.insert(new_idx, *new_var_idx);
deltaset_idx_delta_map.insert(deltaset_idx, (new_idx, *delta));
new_idx += 1;
}
(new_deltaset_idx_varidx_map, deltaset_idx_delta_map)
}
/// glyph closure for Composite glyphs in glyf table
/// limit the number of operations through returning an operation count
fn glyf_closure_glyphs(
loca: &Loca,
glyf: &Glyf,
gid: GlyphId,
gids_to_retain: &mut IntSet<GlyphId>,
operation_count: i32,
depth: u8,
) -> i32 {
if gids_to_retain.contains(gid) {
return operation_count;
}
gids_to_retain.insert(gid);
if depth > MAX_NESTING_LEVEL {
return operation_count;
}
let depth = depth + 1;
let mut operation_count = operation_count - 1;
if operation_count < 0 {
return operation_count;
}
if let Some(Glyph::Composite(glyph)) = loca.get_glyf(gid, glyf).ok().flatten() {
for child in glyph.components() {
operation_count = glyf_closure_glyphs(
loca,
glyf,
child.glyph.into(),
gids_to_retain,
operation_count,
depth,
);
}
}
operation_count
}
fn remove_invalid_gids(gids: &mut IntSet<GlyphId>, num_glyphs: usize) {
gids.remove_range(GlyphId::new(num_glyphs as u32)..=MAX_GID);
}
fn get_font_num_glyphs(font: &FontRef) -> usize {
let ret = font.loca(None).map(|loca| loca.len()).unwrap_or_default();
let maxp = font.maxp().expect("Error reading maxp table");
ret.max(maxp.num_glyphs() as usize)
}
pub(crate) fn remap_indices<T: Domain + std::cmp::Eq + std::hash::Hash + From<u16>>(
indices: IntSet<T>,
) -> FnvHashMap<T, T> {
indices
.iter()
.enumerate()
.map(|x| (x.1, T::from(x.0 as u16)))
.collect()
}
fn remap_palette_indices(indices: IntSet<u16>) -> FnvHashMap<u16, u16> {
indices
.iter()
.enumerate()
.map(|x| {
if x.1 == 0xFFFF {
(0xFFFF, 0xFFFF)
} else {
(x.1, x.0 as u16)
}
})
.collect()
}
/// mutable struct, updated during table subsetting
/// some tables depend on other tables' subset output
#[derive(Default)]
pub struct SubsetState {
// whether GDEF ItemVariationStore is retained after subsetting
has_gdef_varstore: bool,
}
#[derive(Debug, Error)]
pub enum SubsetError {
#[error("Invalid input gid {0}")]
InvalidGid(String),
#[error("Invalid gid range {start}-{end}")]
InvalidGidRange { start: u32, end: u32 },
#[error("Invalid input unicode {0}")]
InvalidUnicode(String),
#[error("Invalid unicode range {start}-{end}")]
InvalidUnicodeRange { start: u32, end: u32 },
#[error("Invalid tag {0}")]
InvalidTag(String),
#[error("Invalid ID {0}")]
InvalidId(String),
#[error("Subsetting table '{0}' failed")]
SubsetTableError(Tag),
}
pub trait NameIdClosure {
/// collect name_ids
fn collect_name_ids(&self, plan: &mut Plan);
}
pub(crate) trait CollectVariationIndices {
fn collect_variation_indices(&self, plan: &Plan, varidx_set: &mut IntSet<u32>);
}
pub(crate) trait LayoutClosure {
/// Remove unreferenced features
fn prune_features(
&self,
lookup_indices: &IntSet<u16>,
feature_indices: IntSet<u16>,
) -> IntSet<u16>;
/// Return a duplicate feature(after subsetting) index map
/// feature index -> the first index of all duplicates for this feature
fn find_duplicate_features(
&self,
lookup_indices: &IntSet<u16>,
feature_indices: IntSet<u16>,
) -> FnvHashMap<u16, u16>;
//remove unreferenced langsys and return (script->langsys mapping, retained feature indices)
fn prune_langsys(
&self,
duplicate_feature_index_map: &FnvHashMap<u16, u16>,
layout_scripts: &IntSet<Tag>,
) -> (FnvHashMap<u16, IntSet<u16>>, IntSet<u16>);
fn closure_glyphs_lookups_features(&self, plan: &mut Plan);
}
pub const CVT: Tag = Tag::new(b"cvt ");
pub const DSIG: Tag = Tag::new(b"DSIG");
pub const EBSC: Tag = Tag::new(b"EBSC");
pub const FPGM: Tag = Tag::new(b"fpgm");
pub const GLAT: Tag = Tag::new(b"Glat");
pub const GLOC: Tag = Tag::new(b"Gloc");
pub const JSTF: Tag = Tag::new(b"JSTF");
pub const LTSH: Tag = Tag::new(b"LTSH");
pub const MORX: Tag = Tag::new(b"morx");
pub const MORT: Tag = Tag::new(b"mort");
pub const KERX: Tag = Tag::new(b"kerx");
pub const KERN: Tag = Tag::new(b"kern");
pub const PCLT: Tag = Tag::new(b"PCLT");
pub const PREP: Tag = Tag::new(b"prep");
pub const SILF: Tag = Tag::new(b"Silf");
pub const SILL: Tag = Tag::new(b"Sill");
pub const VDMX: Tag = Tag::new(b"VDMX");
// This trait is implemented for all font top-level tables
pub trait Subset {
/// Subset this table, if successful a subset version of this table will be added to builder
fn subset(
&self,
_plan: &Plan,
_font: &FontRef,
_s: &mut Serializer,
_builder: &mut FontBuilder,
) -> Result<(), SubsetError> {
Ok(())
}
/// Subset this table with a mutable Subsetstate
/// This is needed when some tables have dependencies on other table's subset output
fn subset_with_state(
&self,
_plan: &Plan,
_font: &FontRef,
_state: &mut SubsetState,
_s: &mut Serializer,
_builder: &mut FontBuilder,
) -> Result<(), SubsetError> {
Ok(())
}
}
// A helper trait providing a 'subset' method for various subtables that have no associated tag
pub(crate) trait SubsetTable<'a> {
type ArgsForSubset: 'a;
type Output: 'a;
/// Subset this table and write a subset version of this table into serializer
fn subset(
&self,
plan: &Plan,
s: &mut Serializer,
args: Self::ArgsForSubset,
) -> Result<Self::Output, SerializeErrorFlags>;
}
// A helper trait providing a 'serialize' method
trait Serialize<'a> {
type Args: 'a;
/// Serialize this table
fn serialize(s: &mut Serializer, args: Self::Args) -> Result<(), SerializeErrorFlags>;
}
pub fn subset_font(font: &FontRef, plan: &Plan) -> Result<Vec<u8>, SubsetError> {
let mut builder = FontBuilder::default();
let mut state = SubsetState::default();
let mut tags_with_dependencies = Vec::with_capacity(5);
for record in font.table_directory().table_records() {
let tag = record.tag();
if should_drop_table(tag, plan) {
continue;
}
// TODO: add more tags with dependencies for instancing
match tag {
Gpos::TAG => tags_with_dependencies.push((tag, record.length())),
_ => subset(tag, font, plan, &mut builder, record.length(), &mut state)?,
}
}
for (tag, table_len) in tags_with_dependencies {
subset(tag, font, plan, &mut builder, table_len, &mut state)?;
}
Ok(builder.build())
}
fn should_drop_table(tag: Tag, plan: &Plan) -> bool {
if plan.drop_tables.contains(tag) {
return true;
}
let no_hinting = plan
.subset_flags
.contains(SubsetFlags::SUBSET_FLAGS_NO_HINTING);
match tag {
// hint tables
Cvar::TAG | CVT | FPGM | PREP | Hdmx::TAG | VDMX => no_hinting,
//TODO: drop var tables during instancing when all axes are pinned
_ => false,
}
}
fn subset<'a>(
table_tag: Tag,
font: &FontRef<'a>,
plan: &Plan,
builder: &mut FontBuilder<'a>,
table_len: u32,
state: &mut SubsetState,
) -> Result<(), SubsetError> {
let buf_size = estimate_subset_table_size(font, table_tag, plan);
let mut s = Serializer::new(buf_size);
let needed = try_subset(table_tag, font, plan, builder, &mut s, table_len, state);
if s.in_error() && !s.only_offset_overflow() {
return Err(SubsetError::SubsetTableError(table_tag));
}
// table subsetted to empty
if needed.is_err() {
return Ok(());
}
//TODO: repack when there's an offset overflow
let subsetted_data = s.copy_bytes();
if !subsetted_data.is_empty() {
builder.add_raw(table_tag, subsetted_data);
}
Ok(())
}
fn try_subset<'a>(
table_tag: Tag,
font: &FontRef<'a>,
plan: &Plan,
builder: &mut FontBuilder<'a>,
s: &mut Serializer,
table_len: u32,
state: &mut SubsetState,
) -> Result<(), SubsetError> {
s.start_serialize()
.map_err(|_| SubsetError::SubsetTableError(table_tag))?;
let ret = subset_table(table_tag, font, plan, builder, s, state);
if !s.ran_out_of_room() {
s.end_serialize();
return ret;
}
// ran out of room, reallocate more bytes
let buf_size = s.allocated() * 2 + 16;
if buf_size > (table_len as usize) * 256 {
return ret;
}
s.reset_size(buf_size);
try_subset(table_tag, font, plan, builder, s, table_len, state)
}
fn subset_table<'a>(
tag: Tag,
font: &FontRef<'a>,
plan: &Plan,
builder: &mut FontBuilder<'a>,
s: &mut Serializer,
state: &mut SubsetState,
) -> Result<(), SubsetError> {
if plan.no_subset_tables.contains(tag) {
return passthrough_table(tag, font, s);
}
match tag {
Base::TAG => font
.base()
.map_err(|_| SubsetError::SubsetTableError(Base::TAG))?
.subset(plan, font, s, builder),
//Skip, handled by Cblc
Cbdt::TAG => Ok(()),
Cblc::TAG => font
.cblc()
.map_err(|_| SubsetError::SubsetTableError(Cblc::TAG))?
.subset(plan, font, s, builder),
Cmap::TAG => font
.cmap()
.map_err(|_| SubsetError::SubsetTableError(Cmap::TAG))?
.subset(plan, font, s, builder),
Colr::TAG => font
.colr()
.map_err(|_| SubsetError::SubsetTableError(Colr::TAG))?
.subset(plan, font, s, builder),
//TODO: if SVG is present and we support subsetting SVG table, pass through CPAL table
// see fonttools: <https://github.com/fonttools/fonttools/blob/64e5277d040e1a5c84f21f8fb8a5dc7d8ad3c3fa/Lib/fontTools/subset/__init__.py#L2545>
Cpal::TAG => font
.cpal()
.map_err(|_| SubsetError::SubsetTableError(Cpal::TAG))?
.subset(plan, font, s, builder),
Gdef::TAG => font
.gdef()
.map_err(|_| SubsetError::SubsetTableError(Gdef::TAG))?
.subset_with_state(plan, font, state, s, builder),
Glyf::TAG => font
.glyf()
.map_err(|_| SubsetError::SubsetTableError(Glyf::TAG))?
.subset(plan, font, s, builder),
Gvar::TAG => font
.gvar()
.map_err(|_| SubsetError::SubsetTableError(Gvar::TAG))?
.subset(plan, font, s, builder),
Hdmx::TAG => font
.hdmx()
.map_err(|_| SubsetError::SubsetTableError(Hdmx::TAG))?
.subset(plan, font, s, builder),
//handled by glyf table if exists
Head::TAG => font.glyf().map(|_| ()).or_else(|_| {
font.head()
.map_err(|_| SubsetError::SubsetTableError(Head::TAG))?
.subset(plan, font, s, builder)
}),
//Skip, handled by Hmtx
Hhea::TAG => Ok(()),
Hmtx::TAG => font
.hmtx()
.map_err(|_| SubsetError::SubsetTableError(Hmtx::TAG))?
.subset(plan, font, s, builder),
Hvar::TAG => font
.hvar()
.map_err(|_| SubsetError::SubsetTableError(Hvar::TAG))?
.subset(plan, font, s, builder),
Vvar::TAG => font
.vvar()
.map_err(|_| SubsetError::SubsetTableError(Vvar::TAG))?
.subset(plan, font, s, builder),
//Skip, handled by glyf
Loca::TAG => Ok(()),
Maxp::TAG => font
.maxp()
.map_err(|_| SubsetError::SubsetTableError(Maxp::TAG))?
.subset(plan, font, s, builder),
Name::TAG => font
.name()
.map_err(|_| SubsetError::SubsetTableError(Name::TAG))?
.subset(plan, font, s, builder),
Os2::TAG => font
.os2()
.map_err(|_| SubsetError::SubsetTableError(Os2::TAG))?
.subset(plan, font, s, builder),
Post::TAG => font
.post()
.map_err(|_| SubsetError::SubsetTableError(Post::TAG))?
.subset(plan, font, s, builder),
Sbix::TAG => font
.sbix()
.map_err(|_| SubsetError::SubsetTableError(Sbix::TAG))?
.subset(plan, font, s, builder),
Vorg::TAG => font
.vorg()
.map_err(|_| SubsetError::SubsetTableError(Vorg::TAG))?
.subset(plan, font, s, builder),
_ => passthrough_table(tag, font, s),
}
}
fn passthrough_table(tag: Tag, font: &FontRef<'_>, s: &mut Serializer) -> Result<(), SubsetError> {
if let Some(data) = font.data_for_tag(tag) {
s.embed_bytes(data.as_bytes())
.map_err(|_| SubsetError::SubsetTableError(tag))?;
}
Ok(())
}
pub fn estimate_subset_table_size(font: &FontRef, table_tag: Tag, plan: &Plan) -> usize {
let Some(table_data) = font.data_for_tag(table_tag) else {
return 0;
};
let table_len = table_data.len();
let mut bulk: usize = 8192;
let src_glyphs = plan.font_num_glyphs;
let dst_glyphs = plan.num_output_glyphs;
// ported from HB: Tables that we want to allocate same space as the source table.
// For GSUB/GPOS it's because those are expensive to subset, so giving them more room is fine.
let same_size: bool =
table_tag == Gsub::TAG || table_tag == Gpos::TAG || table_tag == Name::TAG;
if plan
.subset_flags
.contains(SubsetFlags::SUBSET_FLAGS_RETAIN_GIDS)
{
if table_tag == Cff::TAG {
//Add some extra room for the CFF charset
bulk += src_glyphs * 16;
} else if table_tag == Cff2::TAG {
// Just extra CharString offsets
bulk += src_glyphs * 4;
}
}
if src_glyphs == 0 || same_size {
return bulk + table_len;
}
bulk + table_len * ((dst_glyphs as f32 / src_glyphs as f32).sqrt() as usize)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn populate_unicodes_wo_input_gid() {
let mut plan = Plan::default();
let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
plan.font_num_glyphs = get_font_num_glyphs(&font);
let input_gids = IntSet::empty();
let mut input_unicodes = IntSet::empty();
input_unicodes.insert(0x2c_u32);
input_unicodes.insert(0x31_u32);
plan.populate_unicodes_to_retain(&input_gids, &input_unicodes, &font);
assert_eq!(plan.unicodes.len(), 2);
assert!(plan.unicodes.contains(0x2c_u32));
assert!(plan.unicodes.contains(0x31_u32));
assert_eq!(plan.glyphset_gsub.len(), 2);
assert!(plan.glyphset_gsub.contains(GlyphId::new(2)));
assert!(plan.glyphset_gsub.contains(GlyphId::new(4)));
assert_eq!(plan.unicode_to_new_gid_list.len(), 2);
assert_eq!(plan.unicode_to_new_gid_list[0], (0x2c_u32, GlyphId::new(2)));
assert_eq!(plan.unicode_to_new_gid_list[1], (0x31_u32, GlyphId::new(4)));
assert_eq!(plan.codepoint_to_glyph.len(), 2);
assert_eq!(
plan.codepoint_to_glyph.get(&0x2c_u32),
Some(GlyphId::new(2)).as_ref()
);
assert_eq!(
plan.codepoint_to_glyph.get(&0x31_u32),
Some(GlyphId::new(4)).as_ref()
);
}
#[test]
fn populate_unicodes_w_input_gid() {
let mut plan = Plan::default();
let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
plan.font_num_glyphs = get_font_num_glyphs(&font);
let mut input_gids = IntSet::empty();
let input_unicodes = IntSet::empty();
input_gids.insert(GlyphId::new(2));
input_gids.insert(GlyphId::new(4));
plan.populate_unicodes_to_retain(&input_gids, &input_unicodes, &font);
assert_eq!(plan.unicodes.len(), 2);
assert!(plan.unicodes.contains(0x2c_u32));
assert!(plan.unicodes.contains(0x31_u32));
assert_eq!(plan.glyphset_gsub.len(), 2);
assert!(plan.glyphset_gsub.contains(GlyphId::new(2)));
assert!(plan.glyphset_gsub.contains(GlyphId::new(4)));
assert_eq!(plan.unicode_to_new_gid_list.len(), 2);
assert_eq!(plan.unicode_to_new_gid_list[0], (0x2c_u32, GlyphId::new(2)));
assert_eq!(plan.unicode_to_new_gid_list[1], (0x31_u32, GlyphId::new(4)));
assert_eq!(plan.codepoint_to_glyph.len(), 2);
assert_eq!(
plan.codepoint_to_glyph.get(&0x2c_u32),
Some(GlyphId::new(2)).as_ref()
);
assert_eq!(
plan.codepoint_to_glyph.get(&0x31_u32),
Some(GlyphId::new(4)).as_ref()
);
}
#[test]
fn glyf_closure_composite_glyphs() {
let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
let loca = font.loca(None).unwrap();
let glyf = font.glyf().unwrap();
let mut gids = IntSet::empty();
glyf_closure_glyphs(&loca, &glyf, GlyphId::new(5), &mut gids, 64, 0);
assert_eq!(gids.len(), 2);
assert!(gids.contains(GlyphId::new(5)));
assert!(gids.contains(GlyphId::new(1)));
}
#[test]
fn populate_gids_wo_cmap_colr_layout() {
let mut plan = Plan::default();
let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
plan.font_num_glyphs = get_font_num_glyphs(&font);
plan.unicodes.insert(0x2c_u32);
plan.unicodes.insert(0x34_u32);
plan.glyphset_gsub.insert(GlyphId::new(2));
plan.glyphset_gsub.insert(GlyphId::new(7));
plan.populate_gids_to_retain(&font);
assert_eq!(plan.glyphset_gsub.len(), 3);
assert!(plan.glyphset_gsub.contains(GlyphId::new(0)));
assert!(plan.glyphset_gsub.contains(GlyphId::new(2)));
assert!(plan.glyphset_gsub.contains(GlyphId::new(7)));
assert_eq!(plan.glyphset.len(), 5);
assert!(plan.glyphset.contains(GlyphId::new(0)));
assert!(plan.glyphset.contains(GlyphId::new(1)));
assert!(plan.glyphset.contains(GlyphId::new(2)));
assert!(plan.glyphset.contains(GlyphId::new(4)));
assert!(plan.glyphset.contains(GlyphId::new(7)));
}
}