use crate::cff::dict::{Dict, Operand};
use crate::Error;
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u16)]
pub enum Cff2Op {
CharStringIndexOffset = 17,
VariationStoreOffset = 24,
FontDictIndexOffset = 0x0C24,
FontDictSelectOffset = 0x0C25,
FontMatrix = 0x0C07,
}
impl Cff2Op {
fn code(self) -> u16 {
self as u16
}
}
#[derive(Debug, Clone)]
pub struct Cff2TopDict {
pub charstring_index_offset: u32,
pub font_dict_index_offset: u32,
pub font_dict_select_offset: Option<u32>,
pub variation_store_offset: Option<u32>,
pub font_matrix: [f64; 6],
pub has_font_matrix: bool,
}
impl Default for Cff2TopDict {
fn default() -> Self {
Self {
charstring_index_offset: 0,
font_dict_index_offset: 0,
font_dict_select_offset: None,
variation_store_offset: None,
font_matrix: DEFAULT_FONT_MATRIX,
has_font_matrix: false,
}
}
}
pub const DEFAULT_FONT_MATRIX: [f64; 6] = [0.001, 0.0, 0.0, 0.001, 0.0, 0.0];
impl Cff2TopDict {
pub(crate) fn parse(bytes: &[u8]) -> Result<Self, Error> {
let dict = Dict::parse(bytes)?;
let mut out = Self::default();
let mut have_charstring = false;
let mut have_font_dict_index = false;
for (op, operands) in dict.iter() {
match *op {
code if code == Cff2Op::CharStringIndexOffset.code() => {
out.charstring_index_offset = take_offset(operands)?;
have_charstring = true;
}
code if code == Cff2Op::FontDictIndexOffset.code() => {
out.font_dict_index_offset = take_offset(operands)?;
have_font_dict_index = true;
}
code if code == Cff2Op::FontDictSelectOffset.code() => {
out.font_dict_select_offset = Some(take_offset(operands)?);
}
code if code == Cff2Op::VariationStoreOffset.code() => {
out.variation_store_offset = Some(take_offset(operands)?);
}
code if code == Cff2Op::FontMatrix.code() => {
out.font_matrix = take_font_matrix(operands)?;
out.has_font_matrix = true;
}
_ => {}
}
}
if !have_charstring {
return Err(Error::Cff(
"CFF2 Top DICT missing CharStringINDEXOffset (op 17)",
));
}
if !have_font_dict_index {
return Err(Error::Cff(
"CFF2 Top DICT missing FontDICTINDEXOffset (op 12 36)",
));
}
Ok(out)
}
pub fn is_variable(&self) -> bool {
self.variation_store_offset.is_some()
}
}
fn take_offset(operands: &[Operand]) -> Result<u32, Error> {
let last = operands
.last()
.ok_or(Error::Cff("CFF2 Top DICT: offset operator with no operand"))?;
let v = last
.as_int()
.ok_or(Error::Cff("CFF2 Top DICT: non-integer offset operand"))?;
if v < 0 {
return Err(Error::Cff("CFF2 Top DICT: negative offset"));
}
Ok(v as u32)
}
fn take_font_matrix(operands: &[Operand]) -> Result<[f64; 6], Error> {
if operands.len() < 6 {
return Err(Error::Cff("CFF2 Top DICT FontMatrix < 6 operands"));
}
let m = [
operands[0].as_f64(),
operands[1].as_f64(),
operands[2].as_f64(),
operands[3].as_f64(),
operands[4].as_f64(),
operands[5].as_f64(),
];
let off_diagonal_or_translation_zero = m[1] == 0.0 && m[2] == 0.0 && m[4] == 0.0 && m[5] == 0.0;
if !off_diagonal_or_translation_zero {
return Err(Error::Cff(
"CFF2 Top DICT FontMatrix: off-diagonal/translation non-zero",
));
}
if (m[0] - m[3]).abs() > f64::EPSILON {
return Err(Error::Cff("CFF2 Top DICT FontMatrix: scale[0] != scale[3]"));
}
Ok(m)
}
#[cfg(test)]
mod tests {
use super::*;
fn op_i16(v: i16) -> Vec<u8> {
let b = v.to_be_bytes();
vec![28, b[0], b[1]]
}
fn op_i32(v: i32) -> Vec<u8> {
let b = v.to_be_bytes();
vec![29, b[0], b[1], b[2], b[3]]
}
fn op_bcd_5em4() -> Vec<u8> {
vec![30, 0x0a, 0x00, 0x05, 0xff]
}
fn op_zero() -> u8 {
139
}
#[test]
fn parses_minimal_top_dict() {
let mut bytes = Vec::new();
bytes.extend(op_i16(200));
bytes.push(17); bytes.extend(op_i16(100));
bytes.extend_from_slice(&[12, 36]); let top = Cff2TopDict::parse(&bytes).expect("parse");
assert_eq!(top.charstring_index_offset, 200);
assert_eq!(top.font_dict_index_offset, 100);
assert!(!top.has_font_matrix);
assert!(top.font_dict_select_offset.is_none());
assert!(top.variation_store_offset.is_none());
assert!(!top.is_variable());
assert_eq!(top.font_matrix, DEFAULT_FONT_MATRIX);
}
#[test]
fn parses_variable_font_top_dict() {
let mut bytes = Vec::new();
bytes.extend(op_i16(200));
bytes.push(17);
bytes.extend(op_i16(100));
bytes.extend_from_slice(&[12, 36]);
bytes.extend(op_i16(50));
bytes.push(24); let top = Cff2TopDict::parse(&bytes).expect("parse");
assert_eq!(top.variation_store_offset, Some(50));
assert!(top.is_variable());
}
#[test]
fn parses_multi_fd_top_dict() {
let mut bytes = Vec::new();
bytes.extend(op_i16(200));
bytes.push(17);
bytes.extend(op_i16(100));
bytes.extend_from_slice(&[12, 36]);
bytes.extend(op_i16(150));
bytes.extend_from_slice(&[12, 37]); let top = Cff2TopDict::parse(&bytes).expect("parse");
assert_eq!(top.font_dict_select_offset, Some(150));
}
#[test]
fn parses_font_matrix() {
let mut bytes = Vec::new();
bytes.extend(op_bcd_5em4()); bytes.push(op_zero()); bytes.push(op_zero()); bytes.extend(op_bcd_5em4()); bytes.push(op_zero()); bytes.push(op_zero()); bytes.extend_from_slice(&[12, 7]); bytes.extend(op_i16(200));
bytes.push(17);
bytes.extend(op_i16(100));
bytes.extend_from_slice(&[12, 36]);
let top = Cff2TopDict::parse(&bytes).expect("parse");
assert!(top.has_font_matrix);
assert!((top.font_matrix[0] - 0.0005).abs() < 1e-12);
assert!((top.font_matrix[3] - 0.0005).abs() < 1e-12);
assert_eq!(top.font_matrix[1], 0.0);
assert_eq!(top.font_matrix[5], 0.0);
}
#[test]
fn rejects_missing_charstring_offset() {
let mut bytes = Vec::new();
bytes.extend(op_i16(100));
bytes.extend_from_slice(&[12, 36]);
let err = Cff2TopDict::parse(&bytes).unwrap_err();
match err {
Error::Cff(s) => assert!(s.contains("CharStringINDEXOffset")),
_ => panic!("unexpected: {err:?}"),
}
}
#[test]
fn rejects_missing_font_dict_offset() {
let mut bytes = Vec::new();
bytes.extend(op_i16(200));
bytes.push(17);
let err = Cff2TopDict::parse(&bytes).unwrap_err();
match err {
Error::Cff(s) => assert!(s.contains("FontDICTINDEXOffset")),
_ => panic!("unexpected: {err:?}"),
}
}
#[test]
fn rejects_non_uniform_font_matrix() {
let mut bytes = Vec::new();
bytes.extend(op_bcd_5em4());
bytes.push(op_zero());
bytes.push(op_zero());
bytes.extend_from_slice(&[30, 0x0a, 0x00, 0x1f]);
bytes.push(op_zero());
bytes.push(op_zero());
bytes.extend_from_slice(&[12, 7]);
bytes.extend(op_i16(200));
bytes.push(17);
bytes.extend(op_i16(100));
bytes.extend_from_slice(&[12, 36]);
let err = Cff2TopDict::parse(&bytes).unwrap_err();
match err {
Error::Cff(s) => assert!(s.contains("scale[0] != scale[3]")),
_ => panic!("unexpected: {err:?}"),
}
}
#[test]
fn rejects_font_matrix_with_translation() {
let mut bytes = Vec::new();
bytes.extend(op_bcd_5em4());
bytes.push(op_zero());
bytes.push(op_zero());
bytes.extend(op_bcd_5em4());
bytes.push(239);
bytes.push(op_zero());
bytes.extend_from_slice(&[12, 7]);
bytes.extend(op_i16(200));
bytes.push(17);
bytes.extend(op_i16(100));
bytes.extend_from_slice(&[12, 36]);
let err = Cff2TopDict::parse(&bytes).unwrap_err();
match err {
Error::Cff(s) => assert!(s.contains("off-diagonal/translation non-zero")),
_ => panic!("unexpected: {err:?}"),
}
}
#[test]
fn rejects_negative_offset() {
let mut bytes = Vec::new();
bytes.extend(op_i32(-1));
bytes.push(17);
bytes.extend(op_i16(100));
bytes.extend_from_slice(&[12, 36]);
let err = Cff2TopDict::parse(&bytes).unwrap_err();
match err {
Error::Cff(s) => assert!(s.contains("negative offset")),
_ => panic!("unexpected: {err:?}"),
}
}
#[test]
fn skips_unknown_operators() {
let mut bytes = Vec::new();
bytes.extend(op_i16(200));
bytes.push(17);
bytes.extend(op_i16(100));
bytes.extend_from_slice(&[12, 36]);
bytes.extend(op_i16(0));
bytes.extend(op_i16(0));
bytes.extend(op_i16(0));
bytes.extend(op_i16(0));
bytes.push(5);
let top = Cff2TopDict::parse(&bytes).expect("parse");
assert_eq!(top.charstring_index_offset, 200);
assert_eq!(top.font_dict_index_offset, 100);
}
}