use crate::blend::{AxisKind, Blend, DesignMap};
use crate::eexec;
use crate::encoding::{self, Encoding};
use crate::postscript::{Lexer, Token};
use pdfrum_common::kurbo::{Affine, Rect};
#[derive(Debug, Clone, Default)]
pub(crate) struct Header {
pub font_name: Option<Box<str>>,
pub full_name: Option<Box<str>>,
pub family_name: Option<Box<str>>,
pub weight_name: Option<Box<str>>,
pub italic_angle: f32,
pub is_fixed_pitch: bool,
pub font_matrix: Option<Affine>,
pub font_bbox: Option<Rect>,
pub encoding: Option<Encoding>,
pub axis_kinds: Vec<AxisKind>,
pub design_positions: Vec<Vec<f32>>,
pub design_maps: Vec<DesignMap>,
pub weight_vector: Vec<f32>,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct Private {
pub subrs: Vec<Vec<u8>>,
pub charstrings: Vec<(Box<str>, Vec<u8>)>,
}
pub(crate) fn read_header(bytes: &[u8]) -> Header {
let mut h = Header::default();
let mut lx = Lexer::new(bytes);
let mut seen_italic = false;
let mut seen_pitch = false;
while let Some(tok) = lx.next() {
let Token::Literal(key) = tok else { continue };
match key {
b"FontName" => take_first(&mut h.font_name, next_name(&mut lx)),
b"FullName" => take_first(&mut h.full_name, next_string(&mut lx)),
b"FamilyName" => take_first(&mut h.family_name, next_string(&mut lx)),
b"Weight" => {
take_first(&mut h.weight_name, next_string(&mut lx));
}
b"ItalicAngle" => {
if let (false, Some(v)) = (seen_italic, next_number(&mut lx)) {
h.italic_angle = v as f32;
seen_italic = true;
}
}
b"isFixedPitch" => {
if let (false, Some(v)) = (seen_pitch, next_bool(&mut lx)) {
h.is_fixed_pitch = v;
seen_pitch = true;
}
}
b"FontMatrix" => take_first(&mut h.font_matrix, read_matrix(&mut lx)),
b"FontBBox" => take_first(&mut h.font_bbox, read_bbox(&mut lx)),
b"Encoding" => take_first(&mut h.encoding, read_encoding(&mut lx)),
b"BlendAxisTypes" => take_first_vec(&mut h.axis_kinds, read_axis_types(&mut lx)),
b"BlendDesignPositions" => {
take_first_vec(&mut h.design_positions, read_number_matrix(&mut lx));
}
b"BlendDesignMap" => take_first_vec(&mut h.design_maps, read_design_maps(&mut lx)),
b"WeightVector" => take_first_vec(&mut h.weight_vector, read_number_array(&mut lx)),
_ => {}
}
}
h
}
fn take_first<T>(slot: &mut Option<T>, value: Option<T>) {
if slot.is_none() && value.is_some() {
*slot = value;
}
}
fn take_first_vec<T>(slot: &mut Vec<T>, value: Vec<T>) {
if slot.is_empty() && !value.is_empty() {
*slot = value;
}
}
pub(crate) fn read_private(bytes: &[u8]) -> Private {
let len_iv = scan_len_iv(bytes).unwrap_or(eexec::DEFAULT_LEN_IV);
let skip = eexec::len_iv_skip(len_iv);
let mut p = Private::default();
let mut lx = Lexer::new(bytes);
while let Some(tok) = lx.next() {
match tok {
Token::Literal(b"Subrs") => take_first_vec(&mut p.subrs, read_subrs(&mut lx, skip)),
Token::Literal(b"CharStrings") => {
take_first_vec(&mut p.charstrings, read_charstrings(&mut lx, skip));
}
_ => {}
}
}
p
}
fn scan_len_iv(bytes: &[u8]) -> Option<i32> {
let mut lx = Lexer::new(bytes);
while let Some(tok) = lx.next() {
if tok == Token::Literal(b"lenIV")
&& let Some(Token::Number(v)) = lx.next()
{
return Some(v as i32);
}
}
None
}
fn declared_count(n: f64) -> usize {
payload_len(n).map_or(0, |n| n.min(65_536))
}
fn payload_len(n: f64) -> Option<usize> {
if !n.is_finite() || !(0.0..=4_294_967_295.0).contains(&n) {
return None;
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
Some(n as usize)
}
fn read_subrs(lx: &mut Lexer<'_>, skip: usize) -> Vec<Vec<u8>> {
let count = match lx.next() {
Some(Token::Number(n)) => declared_count(n),
_ => return Vec::new(),
};
let mut subrs = vec![Vec::new(); count];
let mut seen = 0usize;
let mut cursor = lx.clone();
while seen < count {
let Some(tok) = cursor.next() else { break };
match tok {
Token::Keyword(b"dup") => {}
Token::Literal(b"CharStrings") => break,
_ => continue,
}
let (Some(Token::Number(index)), Some(Token::Number(length))) =
(cursor.next(), cursor.next())
else {
continue;
};
let Some(Token::Keyword(_)) = cursor.next() else {
continue;
};
let Some(payload) = payload_len(length).and_then(|n| cursor.take_binary(n)) else {
break;
};
if let Some(slot) = payload_len(index).and_then(|i| subrs.get_mut(i)) {
*slot = eexec::decrypt(payload, eexec::CHARSTRING_SEED, skip);
}
seen = seen.saturating_add(1);
}
*lx = cursor;
subrs
}
fn read_charstrings(lx: &mut Lexer<'_>, skip: usize) -> Vec<(Box<str>, Vec<u8>)> {
let capacity = match lx.next() {
Some(Token::Number(n)) => declared_count(n),
_ => 0,
};
let mut out = Vec::with_capacity(capacity);
while let Some(tok) = lx.next() {
match tok {
Token::Keyword(b"end") => break,
Token::Literal(name) => {
let Some(Token::Number(length)) = lx.next() else {
continue;
};
let Some(Token::Keyword(_)) = lx.next() else {
continue;
};
let Some(payload) = payload_len(length).and_then(|n| lx.take_binary(n)) else {
break;
};
if let Ok(name) = core::str::from_utf8(name) {
out.push((
Box::from(name),
eexec::decrypt(payload, eexec::CHARSTRING_SEED, skip),
));
}
}
_ => {}
}
}
out
}
fn read_encoding(lx: &mut Lexer<'_>) -> Option<Encoding> {
let mut cursor = lx.clone();
match cursor.next()? {
Token::Keyword(b"StandardEncoding") => {
*lx = cursor;
Some(Encoding::Standard)
}
Token::Keyword(b"ExpertEncoding") => {
*lx = cursor;
Some(Encoding::Expert)
}
Token::Keyword(b"ISOLatin1Encoding") => {
*lx = cursor;
Some(Encoding::IsoLatin1)
}
Token::Number(_) => {
let mut pairs: Vec<(u8, &[u8])> = Vec::new();
let mut ended = false;
while let Some(tok) = cursor.next() {
match tok {
Token::Keyword(b"dup") => {
let (Some(Token::Number(code)), Some(Token::Literal(name))) =
(cursor.next(), cursor.next())
else {
continue;
};
if let Ok(code) = u8::try_from(code as i64) {
pairs.push((code, name));
}
}
Token::Keyword(b"def") => {
ended = true;
break;
}
Token::Keyword(b"eexec") | Token::Literal(b"Private") => break,
_ => {}
}
}
if ended {
*lx = cursor;
}
Some(encoding::custom_from_pairs(pairs))
}
_ => None,
}
}
fn read_matrix(lx: &mut Lexer<'_>) -> Option<Affine> {
let v = read_number_array(lx);
let c = |i: usize| -> Option<f64> { Some(f64::from(v.get(i).copied()?)) };
Some(Affine::new([c(0)?, c(1)?, c(2)?, c(3)?, c(4)?, c(5)?]))
}
fn read_bbox(lx: &mut Lexer<'_>) -> Option<Rect> {
let v = read_number_array(lx);
let c = |i: usize| -> Option<f64> { Some(f64::from(v.get(i).copied()?)) };
Some(Rect::new(c(0)?, c(1)?, c(2)?, c(3)?))
}
fn read_number_array(lx: &mut Lexer<'_>) -> Vec<f32> {
let mut cursor = lx.clone();
let mut out = Vec::new();
let mut opened = false;
for tok in cursor.by_ref() {
match tok {
Token::Delim(b'[' | b'{') if !opened => opened = true,
Token::Number(n) => out.push(n as f32),
_ => break,
}
}
*lx = cursor;
out
}
fn read_number_matrix(lx: &mut Lexer<'_>) -> Vec<Vec<f32>> {
let mut cursor = lx.clone();
let mut rows = Vec::new();
let mut row: Option<Vec<f32>> = None;
let mut depth = 0u32;
for tok in cursor.by_ref() {
match tok {
Token::Delim(b'[') => {
depth = depth.saturating_add(1);
if depth == 2 {
row = Some(Vec::new());
}
}
Token::Delim(b']') => {
if depth == 2
&& let Some(r) = row.take()
{
rows.push(r);
}
depth = depth.saturating_sub(1);
if depth == 0 {
break;
}
}
Token::Number(n) => {
if let Some(r) = row.as_mut() {
r.push(n as f32);
}
}
_ => break,
}
}
*lx = cursor;
rows
}
fn read_design_maps(lx: &mut Lexer<'_>) -> Vec<DesignMap> {
let mut cursor = lx.clone();
let mut maps = Vec::new();
let mut knots: Vec<(f32, f32)> = Vec::new();
let mut pair: Vec<f32> = Vec::new();
let mut depth = 0u32;
for tok in cursor.by_ref() {
match tok {
Token::Delim(b'[') => {
depth = depth.saturating_add(1);
match depth {
2 => knots.clear(),
3 => pair.clear(),
_ => {}
}
}
Token::Delim(b']') => {
match depth {
3 => {
if let (Some(&d), Some(&n)) = (pair.first(), pair.get(1)) {
knots.push((d, n));
}
}
2 => maps.push(DesignMap {
knots: core::mem::take(&mut knots),
}),
_ => {}
}
depth = depth.saturating_sub(1);
if depth == 0 {
break;
}
}
Token::Number(n) => {
if depth == 3 {
pair.push(n as f32);
}
}
_ => break,
}
}
*lx = cursor;
maps
}
fn read_axis_types(lx: &mut Lexer<'_>) -> Vec<AxisKind> {
let mut cursor = lx.clone();
let mut out = Vec::new();
let mut opened = false;
for tok in cursor.by_ref() {
match tok {
Token::Delim(b'[') if !opened => opened = true,
Token::Literal(name) => out.push(match name {
b"Weight" => AxisKind::Weight,
b"Width" => AxisKind::Width,
other => AxisKind::Other(core::str::from_utf8(other).unwrap_or("?").into()),
}),
_ => break,
}
}
*lx = cursor;
out
}
fn next_name(lx: &mut Lexer<'_>) -> Option<Box<str>> {
match lx.next()? {
Token::Literal(n) => core::str::from_utf8(n).ok().map(Box::from),
_ => None,
}
}
fn next_string(lx: &mut Lexer<'_>) -> Option<Box<str>> {
match lx.next()? {
Token::Str(s) => Some(String::from_utf8_lossy(s).trim().into()),
_ => None,
}
}
fn next_number(lx: &mut Lexer<'_>) -> Option<f64> {
match lx.next()? {
Token::Number(n) => Some(n),
_ => None,
}
}
fn next_bool(lx: &mut Lexer<'_>) -> Option<bool> {
match lx.next()? {
Token::Keyword(b"true") => Some(true),
Token::Keyword(b"false") => Some(false),
_ => None,
}
}
pub(crate) fn build_blend(h: &Header, diags: &mut pdfrum_common::Diagnostics) -> Option<Blend> {
if h.weight_vector.is_empty() && h.axis_kinds.is_empty() {
return None; }
Blend::new(
h.axis_kinds.clone(),
h.design_positions.clone(),
h.design_maps.clone(),
h.weight_vector.clone(),
diags,
)
}
#[cfg(test)]
#[allow(
clippy::indexing_slicing,
clippy::float_cmp,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::similar_names
)]
mod tests {
use super::{Header, read_header, read_private};
use crate::blend::AxisKind;
use crate::eexec;
use crate::encoding::Encoding;
#[test]
fn header_identity_and_geometry() {
let h = read_header(
b"%!PS-AdobeFont-1.0: Demo\n\
/FontInfo 4 dict dup begin\n\
/FullName (Demo Regular) readonly def\n\
/FamilyName (Demo) readonly def\n\
/Weight (Medium) readonly def\n\
/ItalicAngle -12.5 def\n\
/isFixedPitch true def\n\
end readonly def\n\
/FontName /DemoRegular def\n\
/FontMatrix [0.001 0 0 0.001 0 0] readonly def\n\
/FontBBox {-100 -200 900 800} readonly def\n",
);
assert_eq!(h.font_name.as_deref(), Some("DemoRegular"));
assert_eq!(h.full_name.as_deref(), Some("Demo Regular"));
assert_eq!(h.family_name.as_deref(), Some("Demo"));
assert_eq!(h.weight_name.as_deref(), Some("Medium"));
assert!((h.italic_angle - -12.5).abs() < f32::EPSILON);
assert!(h.is_fixed_pitch);
let m = h.font_matrix.expect("matrix").as_coeffs();
assert!((m[0] - 0.001).abs() < 1e-9);
let b = h.font_bbox.expect("bbox");
assert_eq!((b.x0, b.y0, b.x1, b.y1), (-100.0, -200.0, 900.0, 800.0));
}
#[test]
fn a_built_encoding_vector() {
let h = read_header(
b"/Encoding 256 array\n\
0 1 255 {1 index exch /.notdef put} for\n\
dup 32 /space put\n\
dup 65 /A put\n\
dup 66 /B put\n\
readonly def\n\
/FontName /X def\n",
);
let enc = h.encoding.expect("encoding");
assert_eq!(enc.glyph_name(32), Some("space"));
assert_eq!(enc.glyph_name(65), Some("A"));
assert_eq!(enc.glyph_name(67), None);
assert!(enc.predefined().is_none());
assert_eq!(h.font_name.as_deref(), Some("X"));
}
#[test]
fn the_predefined_encoding_form() {
let h = read_header(b"/Encoding StandardEncoding def\n/FontName /Y def\n");
assert_eq!(h.encoding, Some(Encoding::Standard));
assert_eq!(h.font_name.as_deref(), Some("Y"));
}
#[test]
fn the_multiple_master_declarations() {
let h: Header = read_header(
b"/BlendDesignPositions [[0 0 ][1 0 ][0 1 ][1 1 ]] def\n\
/BlendDesignMap [[[110 0 ][790 1 ]][[100 0 ][900 1 ]]] def\n\
/BlendAxisTypes [/Weight /Width ] def\n\
/WeightVector [0.2702 0.1048 0.4504 0.1746 ] def\n",
);
assert_eq!(h.axis_kinds, vec![AxisKind::Weight, AxisKind::Width]);
assert_eq!(
h.design_positions,
vec![
vec![0.0, 0.0],
vec![1.0, 0.0],
vec![0.0, 1.0],
vec![1.0, 1.0]
]
);
assert_eq!(h.design_maps.len(), 2);
assert_eq!(h.design_maps[0].knots, vec![(110.0, 0.0), (790.0, 1.0)]);
assert_eq!(h.design_maps[1].knots, vec![(100.0, 0.0), (900.0, 1.0)]);
assert_eq!(h.weight_vector.len(), 4);
assert!((h.weight_vector[0] - 0.2702).abs() < 1e-6);
}
fn private_source(len_iv: Option<i32>) -> Vec<u8> {
let enc = |plain: &[u8], skip: usize| {
let mut v = vec![0x5A; skip];
v.extend_from_slice(plain);
eexec::encrypt(&v, eexec::CHARSTRING_SEED)
};
let skip = eexec::len_iv_skip(len_iv.unwrap_or(eexec::DEFAULT_LEN_IV));
let mut out = Vec::new();
out.extend_from_slice(b"dup /Private 8 dict dup begin\n");
if let Some(n) = len_iv {
out.extend_from_slice(format!("/lenIV {n} def\n").as_bytes());
}
out.extend_from_slice(b"/Subrs 2 array\n");
for (i, body) in [b"sub-zero".as_slice(), b"sub-one"].iter().enumerate() {
let c = enc(body, skip);
out.extend_from_slice(format!("dup {i} {} RD ", c.len()).as_bytes());
out.extend_from_slice(&c);
out.extend_from_slice(b" NP\n");
}
out.extend_from_slice(b"ND\n/CharStrings 2 dict dup begin\n");
for (name, body) in [(".notdef", b"nd-body".as_slice()), ("A", b"A-body")] {
let c = enc(body, skip);
out.extend_from_slice(format!("/{name} {} RD ", c.len()).as_bytes());
out.extend_from_slice(&c);
out.extend_from_slice(b" ND\n");
}
out.extend_from_slice(b"end\nend\n");
out
}
#[test]
fn private_dictionary_with_the_default_len_iv() {
let p = read_private(&private_source(None));
assert_eq!(p.subrs.len(), 2);
assert_eq!(p.subrs[0], b"sub-zero");
assert_eq!(p.subrs[1], b"sub-one");
assert_eq!(p.charstrings.len(), 2);
assert_eq!(&*p.charstrings[0].0, ".notdef");
assert_eq!(p.charstrings[1].1, b"A-body");
}
#[test]
fn len_iv_of_zero_and_eight_are_honoured() {
for n in [0i32, 8] {
let p = read_private(&private_source(Some(n)));
assert_eq!(p.charstrings.len(), 2, "lenIV {n}");
assert_eq!(p.charstrings[1].1, b"A-body", "lenIV {n}");
assert_eq!(p.subrs[1], b"sub-one", "lenIV {n}");
}
}
#[test]
fn a_binary_payload_containing_postscript_syntax_is_not_tokenized() {
let body = b"end /CharStrings 9 dict";
let mut v = vec![0u8; 4];
v.extend_from_slice(body);
let c = eexec::encrypt(&v, eexec::CHARSTRING_SEED);
let mut src = Vec::from(b"/CharStrings 1 dict dup begin\n/A ".as_slice());
src.extend_from_slice(format!("{} RD ", c.len()).as_bytes());
src.extend_from_slice(&c);
src.extend_from_slice(b" ND\nend\n");
let p = read_private(&src);
assert_eq!(p.charstrings.len(), 1);
assert_eq!(p.charstrings[0].1, body);
}
#[test]
fn a_truncated_charstrings_dict_keeps_what_it_read() {
let mut src = private_source(None);
src.truncate(src.len() - 40);
let p = read_private(&src);
assert!(p.charstrings.len() <= 2);
assert_eq!(p.subrs.len(), 2);
}
#[test]
fn absent_keys_leave_defaults() {
let h = read_header(b"nothing to see here");
assert!(h.font_name.is_none());
assert!(h.font_matrix.is_none());
assert!(h.encoding.is_none());
assert!(h.weight_vector.is_empty());
let p = read_private(b"");
assert!(p.subrs.is_empty());
assert!(p.charstrings.is_empty());
}
}