pub mod charset;
pub mod charstring;
pub mod dict;
pub mod encoding;
pub mod fdselect;
pub mod header;
pub mod index;
pub mod private;
pub mod strings;
pub mod subrs;
use crate::outline::CubicOutline;
use crate::Error;
use self::charset::Charset;
use self::charstring::Interpreter;
use self::dict::{Dict, Operand, Operator};
use self::encoding::Encoding;
use self::fdselect::FdSelect;
use self::header::CffHeader;
use self::index::Index;
use self::private::PrivateDict;
pub use self::private::PrivateHints;
use self::strings::Strings;
#[derive(Debug, Clone, Default)]
pub struct TopMetadata {
pub font_bbox: [f32; 4],
pub italic_angle: f64,
pub underline_position: f64,
pub underline_thickness: f64,
pub is_fixed_pitch: bool,
pub font_matrix: [f64; 6],
pub paint_type: i32,
pub charstring_type: i32,
pub stroke_width: f64,
pub notice_sid: Option<u16>,
pub copyright_sid: Option<u16>,
pub version_sid: Option<u16>,
pub full_name_sid: Option<u16>,
pub family_name_sid: Option<u16>,
pub weight_sid: Option<u16>,
pub postscript_sid: Option<u16>,
pub base_font_name_sid: Option<u16>,
pub unique_id: Option<i32>,
pub xuid: Vec<i32>,
pub synthetic_base: Option<i32>,
pub base_font_blend: Vec<f64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegistryOrdering {
pub registry_sid: u16,
pub ordering_sid: u16,
pub supplement: i32,
}
#[derive(Debug, Clone)]
enum FontKind<'a> {
NonCid(Box<PrivateDict<'a>>),
Cid {
fd_array: Vec<PrivateDict<'a>>,
fd_select: FdSelect<'a>,
ros: RegistryOrdering,
},
}
#[derive(Debug, Clone)]
pub struct Cff<'a> {
bytes: &'a [u8],
name: &'a [u8],
strings: Strings<'a>,
global_subrs: Index<'a>,
charstrings: Index<'a>,
charset: Charset<'a>,
encoding: Encoding<'a>,
kind: FontKind<'a>,
top: TopMetadata,
}
impl<'a> Cff<'a> {
pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
let header = CffHeader::parse(bytes)?;
let mut cursor = header.size as usize;
let name_index = Index::parse(bytes, cursor)?;
cursor = name_index.end;
if name_index.count == 0 {
return Err(Error::Cff("empty Name INDEX"));
}
let name = name_index.entry(0)?;
let top_index = Index::parse(bytes, cursor)?;
cursor = top_index.end;
if top_index.count != name_index.count {
return Err(Error::Cff("Top DICT INDEX count mismatch"));
}
let top_bytes = top_index.entry(0)?;
let top_dict = Dict::parse(top_bytes)?;
let string_index = Index::parse(bytes, cursor)?;
cursor = string_index.end;
let strings = Strings::new(string_index);
let global_subrs = Index::parse(bytes, cursor)?;
let cs_off = top_dict
.get_int(Operator::CharStrings)
.ok_or(Error::Cff("Top DICT missing CharStrings offset"))?;
if cs_off < 0 {
return Err(Error::Cff("negative CharStrings offset"));
}
let charstrings = Index::parse(bytes, cs_off as usize)?;
let charset_off = top_dict.get_int(Operator::Charset).unwrap_or(0);
let charset = Charset::parse(bytes, charset_off, charstrings.count)?;
let encoding_off = top_dict.get_int(Operator::Encoding).unwrap_or(0);
let encoding = Encoding::parse(bytes, encoding_off)?;
let kind = if let Some(ros) = parse_ros(&top_dict)? {
let fd_array_off = top_dict
.get_int(Operator::FdArray)
.ok_or(Error::Cff("CIDFont missing FDArray"))?;
if fd_array_off < 0 {
return Err(Error::Cff("negative FDArray offset"));
}
let fd_index = Index::parse(bytes, fd_array_off as usize)?;
let mut fd_array = Vec::with_capacity(fd_index.count as usize);
for i in 0..fd_index.count {
let fd_bytes = fd_index.entry(i)?;
let fd_dict = Dict::parse(fd_bytes)?;
let priv_dict = parse_private_from_dict(bytes, &fd_dict)?
.ok_or(Error::Cff("CIDFont Font DICT missing Private"))?;
fd_array.push(priv_dict);
}
if fd_array.is_empty() {
return Err(Error::Cff("CIDFont FDArray is empty"));
}
let fd_select_off = top_dict
.get_int(Operator::FdSelect)
.ok_or(Error::Cff("CIDFont missing FDSelect"))?;
if fd_select_off < 0 {
return Err(Error::Cff("negative FDSelect offset"));
}
let fd_select = FdSelect::parse(bytes, fd_select_off as usize, charstrings.count)?;
FontKind::Cid {
fd_array,
fd_select,
ros,
}
} else {
let private = parse_private_from_dict(bytes, &top_dict)?
.ok_or(Error::Cff("Top DICT missing Private"))?;
FontKind::NonCid(Box::new(private))
};
let top = extract_top_metadata(&top_dict);
Ok(Self {
bytes,
name,
strings,
global_subrs,
charstrings,
charset,
encoding,
kind,
top,
})
}
pub fn top_metadata(&self) -> &TopMetadata {
&self.top
}
pub(crate) fn resolve_sid(&self, sid: u16) -> Option<&str> {
self.strings.get(sid)
}
pub fn glyph_count(&self) -> u16 {
self.charstrings.count.min(u16::MAX as u32) as u16
}
pub fn ps_name(&self) -> &'a [u8] {
self.name
}
pub fn encoding_lookup(&self, codepoint: u8) -> Option<u16> {
self.encoding
.lookup(codepoint, &self.charset, &self.strings)
}
fn private_for(&self, gid: u16) -> Result<&PrivateDict<'a>, Error> {
match &self.kind {
FontKind::NonCid(p) => Ok(p),
FontKind::Cid {
fd_array,
fd_select,
..
} => {
let fd = fd_select
.fd_index(gid)
.ok_or(Error::Cff("FDSelect has no entry for glyph"))?;
fd_array
.get(fd as usize)
.ok_or(Error::Cff("FDSelect FD index out of FDArray range"))
}
}
}
pub fn glyph_outline(&self, gid: u16) -> Result<CubicOutline, Error> {
let gid_u = gid as u32;
if gid_u >= self.charstrings.count {
return Err(Error::GlyphOutOfRange(gid));
}
let private = self.private_for(gid)?;
let cs = self.charstrings.entry(gid_u)?;
let mut interp = Interpreter::new(
&self.global_subrs,
private.local_subrs.as_ref(),
private.nominal_width_x,
private.default_width_x,
)
.with_seac_resolver(&self.charstrings, &self.charset);
interp.run(cs)?;
let mut outline = interp.into_outline();
outline.recompute_bounds();
Ok(outline)
}
pub fn is_cid(&self) -> bool {
matches!(self.kind, FontKind::Cid { .. })
}
pub fn registry_ordering(&self) -> Option<&RegistryOrdering> {
match &self.kind {
FontKind::Cid { ros, .. } => Some(ros),
FontKind::NonCid(_) => None,
}
}
pub fn fd_count(&self) -> usize {
match &self.kind {
FontKind::Cid { fd_array, .. } => fd_array.len(),
FontKind::NonCid(_) => 0,
}
}
pub fn private_hints(&self) -> &PrivateHints {
match &self.kind {
FontKind::NonCid(p) => &p.hints,
FontKind::Cid { fd_array, .. } => &fd_array[0].hints,
}
}
pub fn private_hints_fd(&self, fd_index: usize) -> Option<&PrivateHints> {
match &self.kind {
FontKind::Cid { fd_array, .. } => fd_array.get(fd_index).map(|p| &p.hints),
FontKind::NonCid(_) => None,
}
}
pub fn private_hints_for_glyph(&self, gid: u16) -> Option<&PrivateHints> {
self.private_for(gid).ok().map(|p| &p.hints)
}
pub fn bytes(&self) -> &'a [u8] {
self.bytes
}
pub(crate) fn strings(&self) -> &Strings<'a> {
&self.strings
}
pub(crate) fn charset(&self) -> &Charset<'a> {
&self.charset
}
}
fn parse_ros(top: &Dict) -> Result<Option<RegistryOrdering>, Error> {
let Some(operands) = top.get_array(Operator::Ros) else {
return Ok(None);
};
if operands.len() != 3 {
return Err(Error::Cff("ROS operator must have 3 operands"));
}
let registry_sid = operands[0]
.as_int()
.and_then(|v| u16::try_from(v).ok())
.ok_or(Error::Cff("ROS registry SID"))?;
let ordering_sid = operands[1]
.as_int()
.and_then(|v| u16::try_from(v).ok())
.ok_or(Error::Cff("ROS ordering SID"))?;
let supplement = match operands[2] {
Operand::Int(n) => n,
Operand::Real(r) => r as i32,
};
Ok(Some(RegistryOrdering {
registry_sid,
ordering_sid,
supplement,
}))
}
fn parse_private_from_dict<'a>(
bytes: &'a [u8],
dict: &Dict,
) -> Result<Option<PrivateDict<'a>>, Error> {
let Some(private_arr) = dict.get_array(Operator::Private) else {
return Ok(None);
};
if private_arr.len() != 2 {
return Err(Error::Cff("Private operand must be [size, offset]"));
}
let priv_size = private_arr[0].as_int().ok_or(Error::Cff("Private size"))?;
let priv_off = private_arr[1]
.as_int()
.ok_or(Error::Cff("Private offset"))?;
if priv_size < 0 || priv_off < 0 {
return Err(Error::Cff("negative Private size/offset"));
}
Ok(Some(PrivateDict::parse(
bytes,
priv_off as usize,
priv_size as usize,
)?))
}
fn extract_top_metadata(dict: &Dict) -> TopMetadata {
let font_bbox = dict
.get_array(Operator::FontBBox)
.map(|operands| {
let mut out = [0.0f32; 4];
for (i, o) in operands.iter().take(4).enumerate() {
out[i] = o.as_f64() as f32;
}
out
})
.unwrap_or([0.0; 4]);
let italic_angle = dict.get_number(Operator::ItalicAngle).unwrap_or(0.0);
let underline_position = dict
.get_number(Operator::UnderlinePosition)
.unwrap_or(-100.0);
let underline_thickness = dict
.get_number(Operator::UnderlineThickness)
.unwrap_or(50.0);
let is_fixed_pitch = dict.get_int(Operator::IsFixedPitch).unwrap_or(0) != 0;
let font_matrix = dict
.get_array(Operator::FontMatrix)
.map(|operands| {
let mut out = [0.0f64; 6];
for (i, o) in operands.iter().take(6).enumerate() {
out[i] = o.as_f64();
}
out
})
.unwrap_or([0.001, 0.0, 0.0, 0.001, 0.0, 0.0]);
let paint_type = dict.get_int(Operator::PaintType).unwrap_or(0);
let charstring_type = dict.get_int(Operator::CharstringType).unwrap_or(2);
let stroke_width = dict.get_number(Operator::StrokeWidth).unwrap_or(0.0);
let to_sid = |op| dict.get_int(op).and_then(|i| u16::try_from(i).ok());
let notice_sid = to_sid(Operator::Notice);
let copyright_sid = to_sid(Operator::Copyright);
let version_sid = to_sid(Operator::Version);
let full_name_sid = to_sid(Operator::FullName);
let family_name_sid = to_sid(Operator::FamilyName);
let weight_sid = to_sid(Operator::Weight);
let postscript_sid = to_sid(Operator::PostScript);
let base_font_name_sid = to_sid(Operator::BaseFontName);
let unique_id = dict.get_int(Operator::UniqueID);
let synthetic_base = dict.get_int(Operator::SyntheticBase);
let xuid = dict
.get_array(Operator::XUID)
.map(|ops| ops.iter().filter_map(|o| o.as_int()).collect::<Vec<_>>())
.unwrap_or_default();
let base_font_blend = dict
.get_array(Operator::BaseFontBlend)
.map(|ops| {
let mut acc = 0.0f64;
let mut out = Vec::with_capacity(ops.len());
for o in ops {
acc += o.as_f64();
out.push(acc);
}
out
})
.unwrap_or_default();
TopMetadata {
font_bbox,
italic_angle,
underline_position,
underline_thickness,
is_fixed_pitch,
font_matrix,
paint_type,
charstring_type,
stroke_width,
notice_sid,
copyright_sid,
version_sid,
full_name_sid,
family_name_sid,
weight_sid,
postscript_sid,
base_font_name_sid,
unique_id,
xuid,
synthetic_base,
base_font_blend,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn top_metadata_defaults_when_dict_empty() {
let dict = Dict::parse(&[]).unwrap();
let m = extract_top_metadata(&dict);
assert_eq!(m.font_bbox, [0.0, 0.0, 0.0, 0.0]);
assert_eq!(m.italic_angle, 0.0);
assert_eq!(m.underline_position, -100.0);
assert_eq!(m.underline_thickness, 50.0);
assert!(!m.is_fixed_pitch);
assert_eq!(m.font_matrix, [0.001, 0.0, 0.0, 0.001, 0.0, 0.0]);
assert_eq!(m.paint_type, 0);
assert_eq!(m.charstring_type, 2);
assert_eq!(m.stroke_width, 0.0);
assert_eq!(m.notice_sid, None);
assert_eq!(m.copyright_sid, None);
assert_eq!(m.version_sid, None);
assert_eq!(m.family_name_sid, None);
assert_eq!(m.unique_id, None);
assert!(m.xuid.is_empty());
assert_eq!(m.synthetic_base, None);
assert!(m.base_font_blend.is_empty());
assert_eq!(m.postscript_sid, None);
assert_eq!(m.base_font_name_sid, None);
}
#[test]
fn top_metadata_picks_up_font_matrix_paint_stroke() {
let bcd_5em4: [u8; 5] = [30, 0x0a, 0x00, 0x05, 0xff];
let mut buf = Vec::new();
buf.extend_from_slice(&bcd_5em4);
buf.push(139);
buf.push(139);
buf.extend_from_slice(&bcd_5em4);
buf.push(239);
buf.push(28);
buf.extend_from_slice(&200i16.to_be_bytes());
buf.push(12);
buf.push(7);
buf.push(141);
buf.extend_from_slice(&[12, 5]);
buf.push(141);
buf.extend_from_slice(&[12, 6]);
buf.push(149);
buf.extend_from_slice(&[12, 8]);
let dict = Dict::parse(&buf).unwrap();
let m = extract_top_metadata(&dict);
assert_eq!(m.font_matrix[0], 0.0005);
assert_eq!(m.font_matrix[1], 0.0);
assert_eq!(m.font_matrix[2], 0.0);
assert_eq!(m.font_matrix[3], 0.0005);
assert_eq!(m.font_matrix[4], 100.0);
assert_eq!(m.font_matrix[5], 200.0);
assert_eq!(m.paint_type, 2);
assert_eq!(m.charstring_type, 2);
assert_eq!(m.stroke_width, 10.0);
}
#[test]
fn top_metadata_short_font_matrix_zero_filled() {
let buf = vec![140, 141, 142, 12, 7];
let dict = Dict::parse(&buf).unwrap();
let m = extract_top_metadata(&dict);
assert_eq!(m.font_matrix, [1.0, 2.0, 3.0, 0.0, 0.0, 0.0]);
}
#[test]
fn top_metadata_picks_up_fontbbox_and_italic() {
let mut buf = Vec::new();
for v in [-100i16, -200, 1000, 800] {
buf.push(28);
buf.extend_from_slice(&v.to_be_bytes());
}
buf.push(5);
buf.push(127);
buf.push(12);
buf.push(2);
buf.push(140);
buf.push(12);
buf.push(1);
let dict = Dict::parse(&buf).unwrap();
let m = extract_top_metadata(&dict);
assert_eq!(m.font_bbox, [-100.0, -200.0, 1000.0, 800.0]);
assert_eq!(m.italic_angle, -12.0);
assert!(m.is_fixed_pitch);
assert_eq!(m.underline_position, -100.0);
assert_eq!(m.underline_thickness, 50.0);
}
fn op_int32(v: i32) -> [u8; 5] {
let b = v.to_be_bytes();
[29, b[0], b[1], b[2], b[3]]
}
fn op_int1(v: i32) -> u8 {
(v + 139) as u8
}
fn index_one(payload: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&1u16.to_be_bytes()); out.push(1); out.push(1); out.push((payload.len() + 1) as u8); out.extend_from_slice(payload);
out
}
fn string_index(strings: &[&str]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&(strings.len() as u16).to_be_bytes());
if strings.is_empty() {
return out;
}
out.push(2); let mut off = 1u16;
out.extend_from_slice(&off.to_be_bytes());
for s in strings {
off += s.len() as u16;
out.extend_from_slice(&off.to_be_bytes());
}
for s in strings {
out.extend_from_slice(s.as_bytes());
}
out
}
fn build_cid_cff() -> Vec<u8> {
let charstring: Vec<u8> = vec![
op_int1(0),
op_int1(0),
21, op_int1(100),
op_int1(0),
op_int1(0),
op_int1(100),
op_int1(-100),
op_int1(0),
5, 14, ];
let mut charstrings = Vec::new();
charstrings.extend_from_slice(&3u16.to_be_bytes());
charstrings.push(1); let len = charstring.len() as u8;
charstrings.push(1);
charstrings.push(1 + len);
charstrings.push(1 + 2 * len);
charstrings.push(1 + 3 * len);
for _ in 0..3 {
charstrings.extend_from_slice(&charstring);
}
let charset = vec![0u8, 0x00, 0x01, 0x00, 0x02];
let fdselect = vec![
3u8, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x03, ];
let priv0: Vec<u8> = {
let mut v = Vec::new();
v.extend_from_slice(&op_int32(500));
v.push(20);
v
};
let priv1: Vec<u8> = {
let mut v = Vec::new();
v.extend_from_slice(&op_int32(700));
v.push(20);
v
};
let header = vec![1u8, 0, 4, 1];
let name_index = index_one(b"CIDFontTest");
let strings = string_index(&["Adobe", "Identity"]);
let gsubr = vec![0u8, 0];
let build_top = |cs_off: i32, charset_off: i32, fdarray_off: i32, fdselect_off: i32| {
let mut t = Vec::new();
t.extend_from_slice(&op_int32(391));
t.extend_from_slice(&op_int32(392));
t.push(op_int1(0));
t.extend_from_slice(&[12, 30]); t.extend_from_slice(&op_int32(cs_off));
t.push(17);
t.extend_from_slice(&op_int32(charset_off));
t.push(15);
t.extend_from_slice(&op_int32(fdarray_off));
t.extend_from_slice(&[12, 36]);
t.extend_from_slice(&op_int32(fdselect_off));
t.extend_from_slice(&[12, 37]);
t
};
let top_body_len = build_top(0, 0, 0, 0).len();
let top_index_len = {
let mut tmp = Vec::new();
tmp.extend_from_slice(&1u16.to_be_bytes()); tmp.push(2); tmp.extend_from_slice(&1u16.to_be_bytes());
tmp.extend_from_slice(&((top_body_len + 1) as u16).to_be_bytes());
tmp.len() + top_body_len
};
let front_len =
header.len() + name_index.len() + top_index_len + strings.len() + gsubr.len();
let cs_off = front_len;
let charset_off = cs_off + charstrings.len();
let fdselect_off = charset_off + charset.len();
let priv0_off = fdselect_off + fdselect.len();
let priv1_off = priv0_off + priv0.len();
let fdarray_off = priv1_off + priv1.len();
let fd0_body = {
let mut v = Vec::new();
v.extend_from_slice(&op_int32(priv0.len() as i32)); v.extend_from_slice(&op_int32(priv0_off as i32)); v.push(18); v
};
let fd1_body = {
let mut v = Vec::new();
v.extend_from_slice(&op_int32(priv1.len() as i32));
v.extend_from_slice(&op_int32(priv1_off as i32));
v.push(18);
v
};
let fdarray = {
let mut v = Vec::new();
v.extend_from_slice(&2u16.to_be_bytes()); v.push(1); let mut off = 1u8;
v.push(off);
off += fd0_body.len() as u8;
v.push(off);
off += fd1_body.len() as u8;
v.push(off);
v.extend_from_slice(&fd0_body);
v.extend_from_slice(&fd1_body);
v
};
let top_body = build_top(
cs_off as i32,
charset_off as i32,
fdarray_off as i32,
fdselect_off as i32,
);
assert_eq!(top_body.len(), top_body_len, "Top DICT must be fixed-size");
let top_index = {
let mut v = Vec::new();
v.extend_from_slice(&1u16.to_be_bytes());
v.push(2);
v.extend_from_slice(&1u16.to_be_bytes());
v.extend_from_slice(&((top_body.len() + 1) as u16).to_be_bytes());
v.extend_from_slice(&top_body);
v
};
assert_eq!(top_index.len(), top_index_len);
let mut out = Vec::new();
out.extend_from_slice(&header);
out.extend_from_slice(&name_index);
out.extend_from_slice(&top_index);
out.extend_from_slice(&strings);
out.extend_from_slice(&gsubr);
assert_eq!(out.len(), front_len);
out.extend_from_slice(&charstrings);
out.extend_from_slice(&charset);
out.extend_from_slice(&fdselect);
out.extend_from_slice(&priv0);
out.extend_from_slice(&priv1);
out.extend_from_slice(&fdarray);
out
}
#[test]
fn parses_cid_keyed_font() {
let buf = build_cid_cff();
let cff = Cff::parse(&buf).expect("CID CFF parse");
assert!(cff.is_cid(), "should detect CID-keyed font via ROS");
assert_eq!(cff.glyph_count(), 3);
assert_eq!(cff.fd_count(), 2);
let ros = cff.registry_ordering().expect("ROS present");
assert_eq!(ros.registry_sid, 391);
assert_eq!(ros.ordering_sid, 392);
assert_eq!(ros.supplement, 0);
assert_eq!(cff.resolve_sid(ros.registry_sid), Some("Adobe"));
assert_eq!(cff.resolve_sid(ros.ordering_sid), Some("Identity"));
}
#[test]
fn cid_fdselect_routes_glyphs_to_correct_fd() {
let buf = build_cid_cff();
let cff = Cff::parse(&buf).expect("CID CFF parse");
let p0 = cff.private_for(0).unwrap();
let p1 = cff.private_for(1).unwrap();
let p2 = cff.private_for(2).unwrap();
assert_eq!(p0.default_width_x, 500.0);
assert_eq!(p1.default_width_x, 500.0);
assert_eq!(p2.default_width_x, 700.0);
}
#[test]
fn cid_glyph_outlines_decode() {
let buf = build_cid_cff();
let cff = Cff::parse(&buf).expect("CID CFF parse");
for gid in 0u16..3 {
let outline = cff.glyph_outline(gid).expect("decode");
assert!(!outline.is_empty(), "gid {gid} outline empty");
assert!((outline.bounds.width() - 100.0).abs() < 1e-3);
assert!((outline.bounds.height() - 100.0).abs() < 1e-3);
}
}
#[test]
fn non_cid_font_reports_no_cid_metadata() {
let buf = build_cid_cff();
let cff = Cff::parse(&buf).unwrap();
assert!(cff.is_cid());
assert!(cff.registry_ordering().is_some());
assert!(cff.fd_count() > 0);
}
#[test]
fn top_metadata_picks_up_unique_id() {
let mut buf = Vec::new();
buf.push(28);
buf.extend_from_slice(&28416i16.to_be_bytes());
buf.push(13);
let dict = Dict::parse(&buf).unwrap();
let m = extract_top_metadata(&dict);
assert_eq!(m.unique_id, Some(28416));
}
#[test]
fn top_metadata_picks_up_xuid_array() {
let mut buf = vec![140, 150, 228];
buf.push(28);
buf.extend_from_slice(&12345i16.to_be_bytes());
buf.push(14);
let dict = Dict::parse(&buf).unwrap();
let m = extract_top_metadata(&dict);
assert_eq!(m.xuid, vec![1, 11, 89, 12345]);
}
#[test]
fn top_metadata_picks_up_synthetic_and_postscript_sids() {
let mut buf = vec![181, 12, 20]; buf.extend_from_slice(&[248, 27, 12, 21]); buf.extend_from_slice(&[248, 36, 12, 22]); let dict = Dict::parse(&buf).unwrap();
let m = extract_top_metadata(&dict);
assert_eq!(m.synthetic_base, Some(42));
assert_eq!(m.postscript_sid, Some(391));
assert_eq!(m.base_font_name_sid, Some(400));
}
#[test]
fn top_metadata_undeltifies_base_font_blend() {
let mut buf = Vec::new();
for v in [10i16, 5, -3, 2] {
buf.push(28);
buf.extend_from_slice(&v.to_be_bytes());
}
buf.extend_from_slice(&[12, 23]);
let dict = Dict::parse(&buf).unwrap();
let m = extract_top_metadata(&dict);
assert_eq!(m.base_font_blend, vec![10.0, 15.0, 12.0, 14.0]);
}
#[test]
fn top_metadata_empty_xuid_and_blend_default_to_empty_vec() {
let dict = Dict::parse(&[]).unwrap();
let m = extract_top_metadata(&dict);
assert!(m.xuid.is_empty());
assert!(m.base_font_blend.is_empty());
}
}