#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![warn(clippy::indexing_slicing)]
#![allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
mod blend;
mod charstring;
mod container;
mod eexec;
mod encoding;
mod error;
mod postscript;
mod program;
pub use blend::{AxisKind, MmAxis};
pub use charstring::Glyph;
pub use container::{Container, FontFile, font_file};
pub use eexec::{CHARSTRING_SEED, DEFAULT_LEN_IV, EEXEC_SEED, EEXEC_SKIP, decrypt, encrypt};
pub use encoding::{Encoding, standard_encoding_name, unicode_from_glyph_name};
pub use error::Error;
use blend::Blend;
use pdfrum_common::kurbo::{Affine, BezPath, Rect, Shape};
use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Gid(pub u16);
#[derive(Debug, Clone)]
pub struct Type1Font {
container: Container,
font_name: Option<Box<str>>,
full_name: Option<Box<str>>,
family_name: Option<Box<str>>,
italic_angle: f32,
is_fixed_pitch: bool,
font_matrix: Affine,
font_bbox: Rect,
encoding: Encoding,
subrs: Vec<Vec<u8>>,
charstrings: Vec<Vec<u8>>,
glyph_names: Vec<Box<str>>,
by_name: HashMap<Box<str>, u16>,
unicode_map: HashMap<char, u16>,
blend: Option<Blend>,
}
const DEFAULT_MATRIX: Affine = Affine::new([0.001, 0.0, 0.0, 0.001, 0.0, 0.0]);
impl Type1Font {
pub fn parse(bytes: &[u8], limits: &Limits, diags: &mut Diagnostics) -> Result<Self, Error> {
let split = container::split(bytes, diags)?;
let plain = eexec::decrypt(&split.cipher, eexec::EEXEC_SEED, eexec::EEXEC_SKIP);
if !looks_like_postscript(&plain) {
return Err(Error::EexecGarbage);
}
let header = program::read_header(&split.clear);
let private = program::read_private(&plain);
if private.charstrings.is_empty() {
return Err(Error::NoCharStrings);
}
let cap = limits.max_array_len.min(u16::MAX as usize);
let (glyph_names, charstrings): (Vec<_>, Vec<_>) =
private.charstrings.into_iter().take(cap).unzip();
let by_name: HashMap<Box<str>, u16> = glyph_names
.iter()
.enumerate()
.filter_map(|(i, n)| Some((n.clone(), u16::try_from(i).ok()?)))
.collect();
let mut unicode_map: HashMap<char, u16> = HashMap::new();
for (i, name) in glyph_names.iter().enumerate() {
if let (Some(ch), Ok(gid)) = (encoding::unicode_from_glyph_name(name), u16::try_from(i))
{
unicode_map.entry(ch).or_insert(gid);
}
}
let blend = program::build_blend(&header, diags);
let encoding = header.encoding.unwrap_or(Encoding::Standard);
note_missing_encoding_glyphs(&encoding, &by_name, diags);
let font_matrix = header.font_matrix.unwrap_or(DEFAULT_MATRIX);
Ok(Self {
container: split.container,
font_name: header.font_name,
full_name: header.full_name,
family_name: header.family_name,
italic_angle: header.italic_angle,
is_fixed_pitch: header.is_fixed_pitch,
font_matrix,
font_bbox: header.font_bbox.unwrap_or(Rect::ZERO),
encoding,
subrs: private.subrs,
charstrings,
glyph_names,
by_name,
unicode_map,
blend,
})
}
#[must_use]
pub fn container(&self) -> Container {
self.container
}
#[must_use]
pub fn units_per_em(&self) -> u16 {
let sx = self.font_matrix.as_coeffs().first().copied().unwrap_or(0.0);
if sx.abs() < 1e-12 {
return 1000;
}
let upem = (1.0 / sx).abs().round();
if upem.is_finite() && (1.0..=f64::from(u16::MAX)).contains(&upem) {
#[allow(clippy::cast_sign_loss)]
{
upem as u16
}
} else {
1000
}
}
#[must_use]
pub fn font_matrix(&self) -> Affine {
self.font_matrix
}
#[must_use]
pub fn bbox(&self) -> Rect {
self.font_bbox
}
#[must_use]
pub fn num_glyphs(&self) -> u32 {
self.charstrings.len() as u32
}
#[must_use]
pub fn is_fixed_pitch(&self) -> bool {
self.is_fixed_pitch
}
#[must_use]
pub fn italic_angle(&self) -> f32 {
self.italic_angle
}
#[must_use]
pub fn postscript_name(&self) -> Option<&str> {
self.font_name.as_deref()
}
#[must_use]
pub fn full_name(&self) -> Option<&str> {
self.full_name.as_deref()
}
#[must_use]
pub fn family_name(&self) -> Option<&str> {
self.family_name.as_deref()
}
#[must_use]
pub fn encoding(&self) -> &Encoding {
&self.encoding
}
#[must_use]
pub fn code_to_gid(&self, code: u8) -> Option<Gid> {
self.name_to_gid(self.encoding.glyph_name(code)?)
}
#[must_use]
pub fn unicode_to_gid(&self, ch: char) -> Option<Gid> {
self.unicode_map.get(&ch).copied().map(Gid)
}
pub fn unicode_pairs(&self) -> impl Iterator<Item = (char, Gid)> + '_ {
self.unicode_map.iter().map(|(&ch, &gid)| (ch, Gid(gid)))
}
#[must_use]
pub fn name_to_gid(&self, name: &str) -> Option<Gid> {
self.by_name.get(name).copied().map(Gid)
}
#[must_use]
pub fn glyph_name(&self, gid: Gid) -> Option<&str> {
self.glyph_names.get(gid.0 as usize).map(AsRef::as_ref)
}
#[must_use]
pub fn has_glyph_names(&self) -> bool {
true
}
pub fn glyph_names(&self) -> impl Iterator<Item = (Gid, &str)> {
self.glyph_names
.iter()
.enumerate()
.filter_map(|(i, n)| Some((Gid(u16::try_from(i).ok()?), n.as_ref())))
}
#[must_use]
pub fn outline(&self, gid: Gid) -> Option<(BezPath, f32)> {
let weights = self.default_weights();
let g = self.interpret(gid, &weights, &mut Diagnostics::with_limit(0))?;
Some((g.path, g.advance))
}
#[must_use]
pub fn outline_with_diagnostics(
&self,
gid: Gid,
diags: &mut Diagnostics,
) -> Option<(BezPath, f32)> {
let weights = self.default_weights();
let g = self.interpret(gid, &weights, diags)?;
Some((g.path, g.advance))
}
#[must_use]
pub fn glyph_bounds(&self, gid: Gid) -> Option<Rect> {
let (path, _) = self.outline(gid)?;
(!path.is_empty()).then(|| path.bounding_box())
}
#[must_use]
pub fn mm_axes(&self) -> Option<&[MmAxis]> {
self.blend.as_ref().map(|b| b.axes.as_slice())
}
#[must_use]
pub fn instantiate(&self, coords: &[f32]) -> Option<Type1Instance<'_>> {
let blend = self.blend.as_ref()?;
Some(Type1Instance {
font: self,
weights: blend.weights_for(coords),
})
}
#[must_use]
pub fn default_weight_vector(&self) -> &[f32] {
self.blend.as_ref().map_or(&[], |b| &b.default_weights)
}
fn default_weights(&self) -> Vec<f32> {
self.blend
.as_ref()
.map(|b| b.default_weights.clone())
.unwrap_or_default()
}
fn interpret(&self, gid: Gid, weights: &[f32], diags: &mut Diagnostics) -> Option<Glyph> {
let code = self.charstrings.get(gid.0 as usize)?;
let lookup = |name: &str| self.by_name.get(name).map(|g| *g as usize);
let (glyph, abort) = charstring::interpret(
code,
charstring::Env {
subrs: &self.subrs,
charstrings: &self.charstrings,
name_lookup: &lookup,
weights,
blend: self.blend.as_ref(),
},
);
if abort.is_some() {
diags.record(
Severity::Suspicious,
DiagKind::Type1CharstringAborted,
Some(u64::from(gid.0)),
);
}
Some(glyph)
}
}
#[derive(Debug, Clone)]
pub struct Type1Instance<'a> {
font: &'a Type1Font,
weights: Vec<f32>,
}
impl Type1Instance<'_> {
#[must_use]
pub fn outline(&self, gid: Gid) -> Option<(BezPath, f32)> {
let g = self
.font
.interpret(gid, &self.weights, &mut Diagnostics::with_limit(0))?;
Some((g.path, g.advance))
}
#[must_use]
pub fn advance(&self, gid: Gid) -> Option<f32> {
self.outline(gid).map(|(_, a)| a)
}
#[must_use]
pub fn weight_vector(&self) -> &[f32] {
&self.weights
}
#[must_use]
pub fn font(&self) -> &Type1Font {
self.font
}
}
fn looks_like_postscript(plain: &[u8]) -> bool {
let head = plain.get(..64).unwrap_or(plain);
if head.is_empty() {
return false;
}
let printable = head
.iter()
.filter(|b| b.is_ascii_graphic() || b.is_ascii_whitespace())
.count();
printable * 4 >= head.len() * 3
}
fn note_missing_encoding_glyphs(
enc: &Encoding,
by_name: &HashMap<Box<str>, u16>,
diags: &mut Diagnostics,
) {
if let Encoding::Custom(table) = enc {
for (code, slot) in table.iter().enumerate() {
if let Some(name) = slot
&& !by_name.contains_key(name.as_ref())
{
diags.record(
Severity::Suspicious,
DiagKind::Type1EncodingGlyphMissing,
Some(code as u64),
);
}
}
}
}
#[cfg(test)]
mod tests;