#![doc = include_str!("../README.md")]
use std::collections::HashSet;
use fontcull_read_fonts::collections::IntSet;
use fontcull_skrifa::Tag;
#[cfg(feature = "static-analysis")]
mod static_analysis;
#[cfg(feature = "static-analysis")]
pub use static_analysis::*;
#[derive(Debug)]
pub enum SubsetError {
FontParse(String),
Subset(String),
Woff2(String),
WoffDecompress(String),
}
pub type OpenTypeFeatureTag = [u8; 4];
impl std::fmt::Display for SubsetError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SubsetError::FontParse(msg) => write!(f, "failed to parse font: {msg}"),
SubsetError::Subset(msg) => write!(f, "failed to subset font: {msg}"),
SubsetError::Woff2(msg) => write!(f, "failed to compress to WOFF2: {msg}"),
SubsetError::WoffDecompress(msg) => write!(f, "failed to decompress WOFF: {msg}"),
}
}
}
impl std::error::Error for SubsetError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FontFormat {
Ttf,
Otf,
Woff,
Woff2,
Unknown,
}
impl FontFormat {
pub fn detect(data: &[u8]) -> Self {
if data.len() < 4 {
return FontFormat::Unknown;
}
match &data[0..4] {
[0x77, 0x4F, 0x46, 0x32] => FontFormat::Woff2,
[0x77, 0x4F, 0x46, 0x46] => FontFormat::Woff,
[0x00, 0x01, 0x00, 0x00] => FontFormat::Ttf,
[0x4F, 0x54, 0x54, 0x4F] => FontFormat::Otf,
[0x74, 0x74, 0x63, 0x66] => FontFormat::Ttf,
[0x74, 0x72, 0x75, 0x65] => FontFormat::Ttf,
_ => FontFormat::Unknown,
}
}
pub fn is_woff2(&self) -> bool {
matches!(self, FontFormat::Woff2)
}
}
#[cfg(feature = "woff2")]
pub fn decompress_font(font_data: &[u8]) -> Result<Vec<u8>, SubsetError> {
match FontFormat::detect(font_data) {
FontFormat::Woff2 => woofwoof::decompress(font_data)
.ok_or_else(|| SubsetError::WoffDecompress("WOFF2 decompression failed".to_string())),
FontFormat::Woff => Err(SubsetError::WoffDecompress(
"WOFF1 decompression not supported, please convert to WOFF2 or TTF first".to_string(),
)),
_ => Ok(font_data.to_vec()),
}
}
#[cfg(feature = "woff2")]
pub fn compress_to_woff2(font_data: &[u8]) -> Result<Vec<u8>, SubsetError> {
woofwoof::compress(font_data, "", 11, true)
.ok_or_else(|| SubsetError::Woff2("WOFF2 compression failed".to_string()))
}
fn layout_features(extra_features: &[OpenTypeFeatureTag]) -> IntSet<Tag> {
use fontcull_klippa::DEFAULT_LAYOUT_FEATURES;
let extra_feature_tags = extra_features.iter().map(|ft| Tag::new(ft));
IntSet::from_iter(
DEFAULT_LAYOUT_FEATURES
.iter()
.copied()
.chain(extra_feature_tags),
)
}
pub fn subset_font_data(
font_data: &[u8],
chars: &HashSet<char>,
opentype_features: &[OpenTypeFeatureTag],
) -> Result<Vec<u8>, SubsetError> {
use fontcull_klippa::{Plan, SubsetFlags, subset_font};
use fontcull_skrifa::{FontRef, GlyphId};
use fontcull_write_fonts::types::NameId;
let font = FontRef::new(font_data).map_err(|e| SubsetError::FontParse(format!("{e:?}")))?;
let mut unicodes: IntSet<u32> = IntSet::empty();
for c in chars {
unicodes.insert(*c as u32);
}
let empty_gids: IntSet<GlyphId> = IntSet::empty();
let empty_tags: IntSet<Tag> = IntSet::empty();
let empty_name_ids: IntSet<NameId> = IntSet::empty();
let empty_langs: IntSet<u16> = IntSet::empty();
let layout_scripts: IntSet<Tag> = IntSet::all();
let layout_features: IntSet<Tag> = layout_features(opentype_features);
let plan = Plan::new(
&empty_gids, &unicodes, &font,
SubsetFlags::default(),
&empty_tags, &layout_scripts, &layout_features, &empty_name_ids, &empty_langs, );
let subsetted = subset_font(&font, &plan).map_err(|e| SubsetError::Subset(format!("{e:?}")))?;
Ok(subsetted)
}
#[cfg(feature = "woff2")]
pub fn subset_font_to_woff2(
font_data: &[u8],
chars: &HashSet<char>,
opentype_features: &[OpenTypeFeatureTag],
) -> Result<Vec<u8>, SubsetError> {
let subsetted = subset_font_data(font_data, chars, opentype_features)?;
let woff2 = woofwoof::compress(&subsetted, "", 11, true)
.ok_or_else(|| SubsetError::Woff2("WOFF2 compression failed".to_string()))?;
Ok(woff2)
}
pub fn subset_font_data_unicode(
font_data: &[u8],
unicodes: &[u32],
opentype_features: &[OpenTypeFeatureTag],
) -> Result<Vec<u8>, SubsetError> {
use fontcull_klippa::{Plan, SubsetFlags, subset_font};
use fontcull_read_fonts::collections::IntSet;
use fontcull_skrifa::{FontRef, GlyphId, Tag};
use fontcull_write_fonts::types::NameId;
let font = FontRef::new(font_data).map_err(|e| SubsetError::FontParse(format!("{e:?}")))?;
let mut unicode_set: IntSet<u32> = IntSet::empty();
for &u in unicodes {
unicode_set.insert(u);
}
let empty_gids: IntSet<GlyphId> = IntSet::empty();
let empty_tags: IntSet<Tag> = IntSet::empty();
let empty_name_ids: IntSet<NameId> = IntSet::empty();
let empty_langs: IntSet<u16> = IntSet::empty();
let layout_scripts: IntSet<Tag> = IntSet::all();
let layout_features: IntSet<Tag> = layout_features(opentype_features);
let plan = Plan::new(
&empty_gids,
&unicode_set,
&font,
SubsetFlags::default(),
&empty_tags,
&layout_scripts,
&layout_features,
&empty_name_ids,
&empty_langs,
);
let subsetted = subset_font(&font, &plan).map_err(|e| SubsetError::Subset(format!("{e:?}")))?;
Ok(subsetted)
}
#[cfg(feature = "woff2")]
pub fn subset_font_to_woff2_unicode(
font_data: &[u8],
unicodes: &[u32],
opentype_features: &[OpenTypeFeatureTag],
) -> Result<Vec<u8>, SubsetError> {
let subsetted = subset_font_data_unicode(font_data, unicodes, opentype_features)?;
let woff2 = woofwoof::compress(&subsetted, "", 11, true)
.ok_or_else(|| SubsetError::Woff2("WOFF2 compression failed".to_string()))?;
Ok(woff2)
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[test]
fn test_subset_error_display() {
let err = SubsetError::FontParse("invalid header".to_string());
assert_eq!(format!("{}", err), "failed to parse font: invalid header");
}
#[test]
fn test_font_format_detection() {
assert_eq!(
FontFormat::detect(&[0x77, 0x4F, 0x46, 0x32]),
FontFormat::Woff2
);
assert_eq!(
FontFormat::detect(&[0x77, 0x4F, 0x46, 0x46]),
FontFormat::Woff
);
assert_eq!(
FontFormat::detect(&[0x00, 0x01, 0x00, 0x00]),
FontFormat::Ttf
);
assert_eq!(
FontFormat::detect(&[0x4F, 0x54, 0x54, 0x4F]),
FontFormat::Otf
);
assert_eq!(FontFormat::detect(&[0x00, 0x01]), FontFormat::Unknown);
assert_eq!(
FontFormat::detect(&[0xDE, 0xAD, 0xBE, 0xEF]),
FontFormat::Unknown
);
}
#[test]
fn test_font_format_is_woff2() {
assert!(FontFormat::Woff2.is_woff2());
assert!(!FontFormat::Woff.is_woff2());
assert!(!FontFormat::Ttf.is_woff2());
assert!(!FontFormat::Otf.is_woff2());
assert!(!FontFormat::Unknown.is_woff2());
}
#[test]
#[cfg(feature = "woff2")]
fn test_decompress_ttf_passthrough() {
let ttf_data = [0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
let result = decompress_font(&ttf_data).unwrap();
assert_eq!(result, ttf_data);
}
#[test]
#[cfg(feature = "woff2")]
fn test_decompress_woff2_fixture() {
let woff2_data =
std::fs::read("test_data/simple_glyf.woff2").expect("failed to read WOFF2 fixture");
assert_eq!(FontFormat::detect(&woff2_data), FontFormat::Woff2);
let decompressed = decompress_font(&woff2_data).expect("failed to decompress WOFF2");
assert_eq!(FontFormat::detect(&decompressed), FontFormat::Ttf);
let chars: HashSet<char> = ['a', 'b', 'c'].into_iter().collect();
let _subsetted = subset_font_data(&decompressed, &chars, &[]).expect("failed to subset");
}
#[test]
#[cfg(feature = "woff2")]
fn test_decompress_woff1_not_supported() {
let woff1_data =
std::fs::read("test_data/simple_glyf.woff").expect("failed to read WOFF1 fixture");
assert_eq!(FontFormat::detect(&woff1_data), FontFormat::Woff);
let result = decompress_font(&woff1_data);
assert!(result.is_err());
}
#[test]
#[cfg(feature = "woff2")]
fn test_subset_woff2_input() {
let woff2_input =
std::fs::read("test_data/simple_glyf.woff2").expect("failed to read WOFF2 fixture");
let decompressed = decompress_font(&woff2_input).expect("failed to decompress");
let chars: HashSet<char> = ['a', 'b', 'c'].into_iter().collect();
let subsetted = subset_font_data(&decompressed, &chars, &[]).expect("failed to subset");
let woff2_output = compress_to_woff2(&subsetted).expect("failed to compress output");
assert_eq!(FontFormat::detect(&woff2_output), FontFormat::Woff2);
}
}