pub(crate) mod collect;
pub(crate) mod embed;
pub(crate) mod overrides;
use std::collections::BTreeMap;
use crate::error::Error;
use crate::write::id::IdSource;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct GidMap {
map: BTreeMap<u16, u16>,
}
impl GidMap {
#[must_use]
pub fn from_pairs(pairs: impl IntoIterator<Item = (u16, u16)>) -> Self {
Self {
map: pairs.into_iter().collect(),
}
}
#[must_use]
pub fn get(&self, old: u16) -> Option<u16> {
self.map.get(&old).copied()
}
pub fn pairs(&self) -> impl Iterator<Item = (u16, u16)> + '_ {
self.map.iter().map(|(a, b)| (*a, *b))
}
#[must_use]
pub fn len(&self) -> usize {
self.map.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Subsetted {
pub bytes: Vec<u8>,
pub gid_map: GidMap,
}
pub fn subset(font_bytes: &[u8], gids: &[u16]) -> Result<Subsetted, Error> {
let mut wanted: Vec<u16> = gids.to_vec();
wanted.push(0);
wanted.sort_unstable();
wanted.dedup();
let remapper = subsetter::GlyphRemapper::new_from_glyphs_sorted(&wanted);
let bytes =
subsetter::subset(font_bytes, 0, &remapper).map_err(|e| Error::Subset(e.to_string()))?;
let gid_map = GidMap::from_pairs(
wanted
.iter()
.filter_map(|old| remapper.get(*old).map(|new| (*old, new))),
);
Ok(Subsetted { bytes, gid_map })
}
#[must_use]
pub(crate) fn subset_tag(source: IdSource) -> [u8; 6] {
let mut out = [b'A'; 6];
for (i, slot) in out.iter_mut().enumerate() {
*slot = b'A' + (source.tag_byte(i as u64) % 26);
}
out
}
#[must_use]
pub(crate) fn subset_name(base: &[u8], tag: [u8; 6]) -> Vec<u8> {
let mut out = Vec::with_capacity(base.len() + 7);
out.extend_from_slice(&tag);
out.push(b'+');
out.extend_from_slice(strip_subset_prefix(base));
out
}
#[must_use]
pub(crate) fn strip_subset_prefix(name: &[u8]) -> &[u8] {
if name.len() <= 7 {
return name;
}
if name.get(6) != Some(&b'+') {
return name;
}
if !name
.get(..6)
.is_some_and(|p| p.iter().all(u8::is_ascii_uppercase))
{
return name;
}
name.get(7..).unwrap_or(name)
}
#[must_use]
pub(crate) fn is_opentype_cff(bytes: &[u8]) -> bool {
bytes.get(..4) == Some(b"OTTO")
}
#[cfg(test)]
mod tests {
use super::{GidMap, is_opentype_cff, strip_subset_prefix, subset, subset_name, subset_tag};
use crate::write::id::IdSource;
const TINY: &[u8] = include_bytes!("../../tests/files/tiny.ttf");
#[test]
fn subsetting_keeps_the_asked_for_glyphs_and_notdef() {
let out = subset(TINY, &[1]).expect("subsets");
assert_eq!(out.gid_map.get(0), Some(0));
assert!(out.gid_map.get(1).is_some());
assert_eq!(out.gid_map.len(), 2);
}
#[test]
fn glyph_ids_are_renumbered_into_a_contiguous_space() {
let out = subset(TINY, &[0, 1, 2]).expect("subsets");
let new: Vec<u16> = out.gid_map.pairs().map(|(_, n)| n).collect();
assert_eq!(new, vec![0, 1, 2], "contiguous from zero");
}
#[test]
fn a_subset_is_smaller_than_the_original() {
let out = subset(TINY, &[1]).expect("subsets");
assert!(
out.bytes.len() <= TINY.len(),
"{} vs {}",
out.bytes.len(),
TINY.len()
);
}
#[test]
fn junk_is_refused_rather_than_panicking() {
assert!(subset(b"not a font at all", &[1]).is_err());
assert!(subset(&[], &[]).is_err());
}
#[test]
fn an_existing_prefix_is_replaced_not_stacked() {
let name = subset_name(b"AAAAAA+Arimo-Regular", *b"XXXXXX");
assert_eq!(name, b"XXXXXX+Arimo-Regular");
assert_eq!(name.iter().filter(|b| **b == b'+').take(2).count(), 1);
}
#[test]
fn a_name_with_no_prefix_gains_one() {
assert_eq!(
subset_name(b"Arimo-Regular", *b"ABCDEF"),
b"ABCDEF+Arimo-Regular"
);
}
#[test]
fn only_a_real_prefix_is_stripped() {
assert_eq!(strip_subset_prefix(b"ABCDEF+Name"), b"Name");
assert_eq!(strip_subset_prefix(b"abcdef+Name"), b"abcdef+Name");
assert_eq!(strip_subset_prefix(b"ABCDE+Name"), b"ABCDE+Name");
assert_eq!(strip_subset_prefix(b"ABC123+Name"), b"ABC123+Name");
assert_eq!(strip_subset_prefix(b"ABCDEFName"), b"ABCDEFName");
assert_eq!(strip_subset_prefix(b"ABCDEF+"), b"ABCDEF+");
assert_eq!(strip_subset_prefix(b""), b"");
}
#[test]
fn a_tag_is_six_uppercase_letters() {
let tag = subset_tag(IdSource::Fixed([3u8; 16]));
assert_eq!(tag.len(), 6);
assert!(tag.iter().all(u8::is_ascii_uppercase), "{tag:?}");
}
#[test]
fn a_fixed_source_gives_the_same_tag_every_time() {
let seed = IdSource::Fixed([9u8; 16]);
assert_eq!(subset_tag(seed), subset_tag(seed));
assert_ne!(subset_tag(seed), subset_tag(IdSource::Fixed([8u8; 16])));
}
#[test]
fn opentype_cff_is_an_otto_tag() {
assert!(is_opentype_cff(b"OTTO\x00\x01"));
assert!(!is_opentype_cff(b"\x00\x01\x00\x00"));
assert!(!is_opentype_cff(b"true"));
assert!(!is_opentype_cff(b"OTT"));
assert!(!is_opentype_cff(b""));
}
#[test]
fn an_empty_map_reports_itself_empty() {
let map = GidMap::default();
assert!(map.is_empty());
assert_eq!(map.get(0), None);
}
}