use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as FmtWrite;
use allsorts::binary::read::ReadScope;
use allsorts::font_data::FontData;
use allsorts::subset::{self, CmapTarget, SubsetProfile};
use skrifa::instance::Size;
use skrifa::metrics::{GlyphMetrics, Metrics};
use skrifa::{FontRef, MetadataProvider};
use crate::core::filters::encode_flate;
use crate::core::objects::{Dictionary, Object, PdfName, PdfStream};
use crate::error::{PdfError, PdfResult};
pub(crate) struct ParsedFontMetrics {
pub ps_name: String,
pub units_per_em: u16,
pub ascent: f32,
pub descent: f32,
pub cap_height: f32,
pub bbox: [f32; 4],
}
pub(crate) fn parse_font_metrics(data: &[u8]) -> PdfResult<ParsedFontMetrics> {
parse_font_metrics_with_axes(data, &[])
}
pub(crate) fn parse_font_metrics_with_axes(
data: &[u8],
axes: &[(&str, f32)],
) -> PdfResult<ParsedFontMetrics> {
let font_ref = FontRef::new(data)
.map_err(|e| PdfError::InvalidFont(format!("Failed to parse font: {}", e)))?;
let location = build_location(&font_ref, axes);
let metrics = Metrics::new(&font_ref, Size::unscaled(), &location);
let ps_name = font_ref
.localized_strings(skrifa::string::StringId::POSTSCRIPT_NAME)
.english_or_first()
.map(|s| s.chars().collect::<String>())
.unwrap_or_else(|| "UnknownFont".to_string());
let bbox = metrics
.bounds
.map(|b| [b.x_min, b.y_min, b.x_max, b.y_max])
.unwrap_or([0.0, 0.0, 1000.0, 1000.0]);
Ok(ParsedFontMetrics {
ps_name,
units_per_em: metrics.units_per_em,
ascent: metrics.ascent,
descent: metrics.descent,
cap_height: metrics.cap_height.unwrap_or(metrics.ascent * 0.7),
bbox,
})
}
pub(crate) struct SubsetResult {
pub data: Vec<u8>,
pub char_to_gid: BTreeMap<char, u16>,
pub old_to_new: BTreeMap<u16, u16>,
pub widths: BTreeMap<u16, u16>,
}
pub(crate) fn build_location_pub(
font_ref: &FontRef<'_>,
axes: &[(&str, f32)],
) -> skrifa::instance::Location {
build_location(font_ref, axes)
}
fn build_location(font_ref: &FontRef<'_>, axes: &[(&str, f32)]) -> skrifa::instance::Location {
if axes.is_empty() {
return skrifa::instance::Location::default();
}
let axis_settings: Vec<_> = axes
.iter()
.filter_map(|(tag, val)| {
let tag_bytes = tag.as_bytes();
if tag_bytes.len() == 4 {
let arr: [u8; 4] = [tag_bytes[0], tag_bytes[1], tag_bytes[2], tag_bytes[3]];
let t = skrifa::Tag::new(&arr);
Some((t, *val))
} else {
None
}
})
.collect();
let axis_collection = font_ref.axes();
axis_collection.location(axis_settings)
}
pub(crate) fn collect_glyphs_and_subset(
font_data: &[u8],
chars: &[char],
) -> PdfResult<SubsetResult> {
collect_glyphs_and_subset_with_axes(font_data, chars, &[])
}
pub(crate) fn collect_glyphs_and_subset_with_axes(
font_data: &[u8],
chars: &[char],
axes: &[(&str, f32)],
) -> PdfResult<SubsetResult> {
let font_ref = FontRef::new(font_data)
.map_err(|e| PdfError::InvalidFont(format!("Font parse error: {}", e)))?;
let charmap = font_ref.charmap();
let location = build_location(&font_ref, axes);
let glyph_metrics = GlyphMetrics::new(&font_ref, Size::unscaled(), &location);
let mut glyph_set: BTreeSet<u16> = BTreeSet::new();
glyph_set.insert(0); let mut char_to_gid: BTreeMap<char, u16> = BTreeMap::new();
let mut widths: BTreeMap<u16, u16> = BTreeMap::new();
widths.insert(
0,
glyph_metrics
.advance_width(skrifa::GlyphId::new(0))
.unwrap_or(0.0) as u16,
);
for &ch in chars {
if let Some(gid) = charmap.map(ch) {
let gid_u16 = gid.to_u32() as u16;
if let std::collections::btree_map::Entry::Vacant(e) = char_to_gid.entry(ch) {
e.insert(gid_u16);
glyph_set.insert(gid_u16);
let width = glyph_metrics.advance_width(gid).unwrap_or(0.0) as u16;
widths.insert(gid_u16, width);
}
}
}
let glyph_ids: Vec<u16> = glyph_set.into_iter().collect();
let scope = ReadScope::new(font_data);
let font_data_parsed = scope
.read::<FontData<'_>>()
.map_err(|e| PdfError::InvalidFont(format!("Allsorts parse error: {}", e)))?;
let provider = font_data_parsed
.table_provider(0)
.map_err(|e| PdfError::InvalidFont(format!("Allsorts table provider: {}", e)))?;
let subset_data = subset::subset(
&provider,
&glyph_ids,
&SubsetProfile::Minimal,
CmapTarget::Unrestricted,
)
.map_err(|e| PdfError::InvalidFont(format!("Subsetting failed: {:?}", e)))?;
let mut old_to_new: BTreeMap<u16, u16> = BTreeMap::new();
for (new_idx, &old_gid) in glyph_ids.iter().enumerate() {
old_to_new.insert(old_gid, new_idx as u16);
}
Ok(SubsetResult {
data: subset_data,
char_to_gid,
old_to_new,
widths,
})
}
pub(crate) struct FontDescriptorParams<'a> {
pub ps_name: &'a str,
pub units_per_em: u16,
pub ascent: f32,
pub descent: f32,
pub cap_height: f32,
pub bbox: [f32; 4],
pub flags: i64,
pub font_file_key: &'a str,
pub font_file_ref: Object,
}
pub(crate) fn build_font_descriptor(params: FontDescriptorParams<'_>) -> Dictionary {
let mut dict = Dictionary::new();
dict.insert(
PdfName::new("Type"),
Object::Name(PdfName::new("FontDescriptor")),
);
dict.insert(
PdfName::new("FontName"),
Object::Name(PdfName::new(params.ps_name)),
);
dict.insert(PdfName::new("Flags"), Object::Integer(params.flags));
let scale = 1000.0 / params.units_per_em as f64;
dict.insert(
PdfName::new("Ascent"),
Object::Integer((params.ascent as f64 * scale).round() as i64),
);
dict.insert(
PdfName::new("Descent"),
Object::Integer((params.descent as f64 * scale).round() as i64),
);
dict.insert(
PdfName::new("CapHeight"),
Object::Integer((params.cap_height as f64 * scale).round() as i64),
);
dict.insert(
PdfName::new("FontBBox"),
Object::Array(
params
.bbox
.iter()
.map(|&v| Object::Integer((v as f64 * scale).round() as i64))
.collect(),
),
);
dict.insert(PdfName::new("ItalicAngle"), Object::Integer(0));
dict.insert(PdfName::new("StemV"), Object::Integer(80));
dict.insert(PdfName::new(params.font_file_key), params.font_file_ref);
dict
}
pub(crate) fn build_font_stream(data: &[u8]) -> PdfResult<PdfStream> {
let mut dict = Dictionary::new();
let (compressed, filter) = encode_flate(data)?;
dict.insert(PdfName::new("Length1"), Object::Integer(data.len() as i64));
if let Some(f) = filter {
dict.insert(PdfName::new("Filter"), Object::Name(PdfName::new(f)));
}
Ok(PdfStream::new(dict, compressed))
}
pub(crate) fn build_to_unicode_cmap(
char_to_gid: &BTreeMap<char, u16>,
old_to_new: &BTreeMap<u16, u16>,
byte_width: usize,
) -> PdfResult<PdfStream> {
let mut cmap = String::with_capacity(512 + char_to_gid.len() * 30);
cmap.push_str("/CIDInit /ProcSet findresource begin\n");
cmap.push_str("12 dict begin\n");
cmap.push_str("begincmap\n");
cmap.push_str("/CIDSystemInfo\n");
cmap.push_str("<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n");
cmap.push_str("/CMapName /Adobe-Identity-UCS def\n");
cmap.push_str("/CMapType 2 def\n");
cmap.push_str("1 begincodespacerange\n");
if byte_width == 1 {
cmap.push_str("<00> <FF>\n");
} else {
cmap.push_str("<0000> <FFFF>\n");
}
cmap.push_str("endcodespacerange\n");
let hex_width = byte_width * 2;
let mut mappings: Vec<(u16, char)> = Vec::new();
for (&ch, &old_gid) in char_to_gid {
if let Some(&new_gid) = old_to_new.get(&old_gid) {
mappings.push((new_gid, ch));
}
}
mappings.sort_by_key(|&(gid, _)| gid);
if !mappings.is_empty() {
for chunk in mappings.chunks(100) {
let _ = writeln!(cmap, "{} beginbfchar", chunk.len());
for &(gid, ch) in chunk {
let unicode = ch as u32;
if unicode > 0xFFFF {
let u = unicode - 0x10000;
let high = 0xD800 + (u >> 10);
let low = 0xDC00 + (u & 0x3FF);
let _ = writeln!(
cmap,
"<{:0>width$X}> <{:04X}{:04X}>",
gid,
high,
low,
width = hex_width
);
} else {
let _ = writeln!(
cmap,
"<{:0>width$X}> <{:04X}>",
gid,
unicode,
width = hex_width
);
}
}
cmap.push_str("endbfchar\n");
}
}
cmap.push_str("endcmap\n");
cmap.push_str("CMapName currentdict /CMap defineresource pop\n");
cmap.push_str("end\n");
cmap.push_str("end\n");
let data = cmap.into_bytes();
let (compressed, filter) = encode_flate(&data)?;
let mut dict = Dictionary::new();
if let Some(f) = filter {
dict.insert(PdfName::new("Filter"), Object::Name(PdfName::new(f)));
}
Ok(PdfStream::new(dict, compressed))
}