use crate::parser::{read_i16, read_i32, read_u16, read_u32, read_u8};
use crate::Error;
pub const POST_TABLE_TAG: [u8; 4] = *b"post";
pub const POST_HEADER_LEN: usize = 32;
pub const POST_VERSION_10: u32 = 0x0001_0000;
pub const POST_VERSION_20: u32 = 0x0002_0000;
pub const POST_VERSION_25: u32 = 0x0002_5000;
pub const POST_VERSION_30: u32 = 0x0003_0000;
pub const STANDARD_MAC_GLYPH_COUNT: u16 = 258;
pub const RECOMMENDED_GLYPH_NAME_MAX_LEN: usize = 63;
#[derive(Debug, Clone)]
pub struct PostTable {
pub version_raw: u32,
pub italic_angle: f32,
pub underline_position: i16,
pub underline_thickness: i16,
pub is_fixed_pitch: bool,
pub min_mem_type42: u32,
pub max_mem_type42: u32,
pub min_mem_type1: u32,
pub max_mem_type1: u32,
pub format: PostFormat,
}
#[derive(Debug, Clone)]
pub enum PostFormat {
Version10,
Version20(PostV20),
Version25(PostV25),
Version30,
}
#[derive(Debug, Clone)]
pub struct PostV20 {
pub num_glyphs: u16,
pub glyph_name_indices: Vec<u16>,
pub pascal_strings: Vec<String>,
pub has_oversize_glyph_name: bool,
pub has_non_conformant_glyph_name: bool,
}
#[derive(Debug, Clone)]
pub struct PostV25 {
pub num_glyphs: u16,
pub offsets: Vec<i8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GlyphNameRef<'a> {
StandardMac { index: u16 },
Custom(&'a str),
}
impl PostTable {
pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < POST_HEADER_LEN {
return Err(Error::UnexpectedEof);
}
let version_raw = read_u32(bytes, 0)?;
let italic_raw = read_i32(bytes, 4)?;
let italic_angle = italic_raw as f32 / 65536.0;
let underline_position = read_i16(bytes, 8)?;
let underline_thickness = read_i16(bytes, 10)?;
let is_fixed_pitch = read_u32(bytes, 12)? != 0;
let min_mem_type42 = read_u32(bytes, 16)?;
let max_mem_type42 = read_u32(bytes, 20)?;
let min_mem_type1 = read_u32(bytes, 24)?;
let max_mem_type1 = read_u32(bytes, 28)?;
let tail = &bytes[POST_HEADER_LEN..];
let format = match version_raw {
POST_VERSION_10 => PostFormat::Version10,
POST_VERSION_20 => PostFormat::Version20(parse_v20(tail)?),
POST_VERSION_25 => PostFormat::Version25(parse_v25(tail)?),
POST_VERSION_30 => PostFormat::Version30,
_ => return Err(Error::BadStructure("post: unsupported version")),
};
Ok(Self {
version_raw,
italic_angle,
underline_position,
underline_thickness,
is_fixed_pitch,
min_mem_type42,
max_mem_type42,
min_mem_type1,
max_mem_type1,
format,
})
}
pub fn has_glyph_names(&self) -> bool {
!matches!(self.format, PostFormat::Version30)
}
pub fn has_oversize_glyph_name(&self) -> bool {
match &self.format {
PostFormat::Version20(v) => v.has_oversize_glyph_name,
_ => false,
}
}
pub fn has_non_conformant_glyph_name(&self) -> bool {
match &self.format {
PostFormat::Version20(v) => v.has_non_conformant_glyph_name,
_ => false,
}
}
pub fn pascal_string_count(&self) -> usize {
match &self.format {
PostFormat::Version20(v) => v.pascal_strings.len(),
_ => 0,
}
}
pub fn glyph_name_ref(&self, gid: u16) -> Option<GlyphNameRef<'_>> {
match &self.format {
PostFormat::Version10 => {
if gid < STANDARD_MAC_GLYPH_COUNT {
Some(GlyphNameRef::StandardMac { index: gid })
} else {
None
}
}
PostFormat::Version20(v) => {
let idx = *v.glyph_name_indices.get(gid as usize)?;
if idx < STANDARD_MAC_GLYPH_COUNT {
Some(GlyphNameRef::StandardMac { index: idx })
} else {
let pi = (idx - STANDARD_MAC_GLYPH_COUNT) as usize;
v.pascal_strings
.get(pi)
.map(|s| GlyphNameRef::Custom(s.as_str()))
}
}
PostFormat::Version25(v) => {
let off = *v.offsets.get(gid as usize)?;
let std = i32::from(gid) + i32::from(off);
if (0..i32::from(STANDARD_MAC_GLYPH_COUNT)).contains(&std) {
Some(GlyphNameRef::StandardMac { index: std as u16 })
} else {
None
}
}
PostFormat::Version30 => None,
}
}
pub fn custom_glyph_name(&self, gid: u16) -> Option<&str> {
match self.glyph_name_ref(gid)? {
GlyphNameRef::Custom(s) => Some(s),
GlyphNameRef::StandardMac { .. } => None,
}
}
}
fn parse_v20(tail: &[u8]) -> Result<PostV20, Error> {
if tail.len() < 2 {
return Err(Error::UnexpectedEof);
}
let num_glyphs = read_u16(tail, 0)?;
let idx_bytes_len = 2usize
.checked_mul(num_glyphs as usize)
.ok_or(Error::BadStructure("post v2.0: numGlyphs overflow"))?;
let idx_end = 2usize
.checked_add(idx_bytes_len)
.ok_or(Error::BadStructure("post v2.0: numGlyphs overflow"))?;
if tail.len() < idx_end {
return Err(Error::UnexpectedEof);
}
let mut glyph_name_indices = Vec::with_capacity(num_glyphs as usize);
let mut max_pascal_referenced: i32 = -1;
for i in 0..num_glyphs as usize {
let v = read_u16(tail, 2 + i * 2)?;
if v >= STANDARD_MAC_GLYPH_COUNT {
let pi = (v - STANDARD_MAC_GLYPH_COUNT) as i32;
if pi > max_pascal_referenced {
max_pascal_referenced = pi;
}
}
glyph_name_indices.push(v);
}
let pool = &tail[idx_end..];
let mut pascal_strings: Vec<String> = Vec::new();
let mut has_oversize_glyph_name = false;
let mut has_non_conformant_glyph_name = false;
let mut p = 0usize;
while p < pool.len() {
let len = read_u8(pool, p)? as usize;
p += 1;
if p + len > pool.len() {
return Err(Error::UnexpectedEof);
}
let raw = &pool[p..p + len];
if len > RECOMMENDED_GLYPH_NAME_MAX_LEN {
has_oversize_glyph_name = true;
}
if !raw.iter().all(|b| is_conformant_glyph_name_byte(*b)) {
has_non_conformant_glyph_name = true;
}
let s = match std::str::from_utf8(raw) {
Ok(s) => s.to_string(),
Err(_) => String::from_utf8_lossy(raw).into_owned(),
};
pascal_strings.push(s);
p += len;
}
let _ = max_pascal_referenced;
Ok(PostV20 {
num_glyphs,
glyph_name_indices,
pascal_strings,
has_oversize_glyph_name,
has_non_conformant_glyph_name,
})
}
fn parse_v25(tail: &[u8]) -> Result<PostV25, Error> {
if tail.len() < 2 {
return Err(Error::UnexpectedEof);
}
let num_glyphs = read_u16(tail, 0)?;
let needed = 2usize
.checked_add(num_glyphs as usize)
.ok_or(Error::BadStructure("post v2.5: numGlyphs overflow"))?;
if tail.len() < needed {
return Err(Error::UnexpectedEof);
}
let mut offsets = Vec::with_capacity(num_glyphs as usize);
for i in 0..num_glyphs as usize {
offsets.push(tail[2 + i] as i8);
}
Ok(PostV25 {
num_glyphs,
offsets,
})
}
fn is_conformant_glyph_name_byte(b: u8) -> bool {
b.is_ascii_uppercase() || b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'.' || b == b'_'
}
#[cfg(test)]
mod tests {
use super::*;
fn header(version: u32) -> Vec<u8> {
let mut b = vec![0u8; POST_HEADER_LEN];
b[0..4].copy_from_slice(&version.to_be_bytes());
b[4..8].copy_from_slice(&((-10i32) << 16).to_be_bytes());
b[8..10].copy_from_slice(&(-100i16).to_be_bytes());
b[10..12].copy_from_slice(&50i16.to_be_bytes());
b[12..16].copy_from_slice(&1u32.to_be_bytes());
b[16..20].copy_from_slice(&0u32.to_be_bytes());
b[20..24].copy_from_slice(&0u32.to_be_bytes());
b[24..28].copy_from_slice(&0u32.to_be_bytes());
b[28..32].copy_from_slice(&0u32.to_be_bytes());
b
}
#[test]
fn parses_minimal_v3_header() {
let b = header(POST_VERSION_30);
let p = PostTable::parse(&b).unwrap();
assert_eq!(p.version_raw, POST_VERSION_30);
assert!((p.italic_angle - (-10.0)).abs() < 0.001);
assert_eq!(p.underline_position, -100);
assert_eq!(p.underline_thickness, 50);
assert!(p.is_fixed_pitch);
assert!(matches!(p.format, PostFormat::Version30));
assert!(!p.has_glyph_names());
assert!(p.glyph_name_ref(0).is_none());
}
#[test]
fn parses_v10_returns_standard_mac_indices() {
let b = header(POST_VERSION_10);
let p = PostTable::parse(&b).unwrap();
assert!(matches!(p.format, PostFormat::Version10));
assert!(p.has_glyph_names());
assert_eq!(
p.glyph_name_ref(0),
Some(GlyphNameRef::StandardMac { index: 0 })
);
assert_eq!(
p.glyph_name_ref(217),
Some(GlyphNameRef::StandardMac { index: 217 })
);
assert!(p.glyph_name_ref(258).is_none());
}
#[test]
fn v20_resolves_spec_worked_example() {
let num_glyphs: u16 = 409;
let mut tail = Vec::new();
tail.extend_from_slice(&num_glyphs.to_be_bytes());
for gid in 0..num_glyphs {
let idx: u16 = match gid {
302 => 217,
408 => 262, _ => 0, };
tail.extend_from_slice(&idx.to_be_bytes());
}
for name in ["one", "two", "three", "four", "weird"] {
tail.push(name.len() as u8);
tail.extend_from_slice(name.as_bytes());
}
let mut bytes = header(POST_VERSION_20);
bytes.extend_from_slice(&tail);
let p = PostTable::parse(&bytes).unwrap();
assert!(p.has_glyph_names());
assert_eq!(p.pascal_string_count(), 5);
assert_eq!(
p.glyph_name_ref(302),
Some(GlyphNameRef::StandardMac { index: 217 })
);
assert_eq!(p.glyph_name_ref(408), Some(GlyphNameRef::Custom("weird")));
assert_eq!(p.custom_glyph_name(408), Some("weird"));
assert!(p.custom_glyph_name(302).is_none());
}
#[test]
fn v20_pascal_pool_indices_are_zero_based() {
let num_glyphs: u16 = 2;
let mut tail = Vec::new();
tail.extend_from_slice(&num_glyphs.to_be_bytes());
tail.extend_from_slice(&258u16.to_be_bytes()); tail.extend_from_slice(&259u16.to_be_bytes()); for name in ["Alpha", "Beta"] {
tail.push(name.len() as u8);
tail.extend_from_slice(name.as_bytes());
}
let mut bytes = header(POST_VERSION_20);
bytes.extend_from_slice(&tail);
let p = PostTable::parse(&bytes).unwrap();
assert_eq!(p.glyph_name_ref(0), Some(GlyphNameRef::Custom("Alpha")));
assert_eq!(p.glyph_name_ref(1), Some(GlyphNameRef::Custom("Beta")));
}
#[test]
fn v20_rejects_truncated_pascal_string() {
let num_glyphs: u16 = 1;
let mut tail = Vec::new();
tail.extend_from_slice(&num_glyphs.to_be_bytes());
tail.extend_from_slice(&258u16.to_be_bytes());
tail.push(5); tail.extend_from_slice(b"abc");
let mut bytes = header(POST_VERSION_20);
bytes.extend_from_slice(&tail);
assert!(matches!(
PostTable::parse(&bytes),
Err(Error::UnexpectedEof)
));
}
#[test]
fn v20_flags_oversize_and_non_conformant_names() {
let num_glyphs: u16 = 2;
let mut tail = Vec::new();
tail.extend_from_slice(&num_glyphs.to_be_bytes());
tail.extend_from_slice(&258u16.to_be_bytes()); tail.extend_from_slice(&259u16.to_be_bytes()); let oversize = "a".repeat(64);
tail.push(oversize.len() as u8);
tail.extend_from_slice(oversize.as_bytes());
let bad = "weird/name";
tail.push(bad.len() as u8);
tail.extend_from_slice(bad.as_bytes());
let mut bytes = header(POST_VERSION_20);
bytes.extend_from_slice(&tail);
let p = PostTable::parse(&bytes).unwrap();
assert!(p.has_oversize_glyph_name());
assert!(p.has_non_conformant_glyph_name());
}
#[test]
fn v25_resolves_signed_offset_into_standard_set() {
let num_glyphs: u16 = 3;
let mut tail = Vec::new();
tail.extend_from_slice(&num_glyphs.to_be_bytes());
tail.push(36i8 as u8);
tail.push(36i8 as u8);
tail.push(36i8 as u8);
let mut bytes = header(POST_VERSION_25);
bytes.extend_from_slice(&tail);
let p = PostTable::parse(&bytes).unwrap();
assert!(p.has_glyph_names());
assert_eq!(
p.glyph_name_ref(0),
Some(GlyphNameRef::StandardMac { index: 36 })
);
assert_eq!(
p.glyph_name_ref(1),
Some(GlyphNameRef::StandardMac { index: 37 })
);
assert_eq!(
p.glyph_name_ref(2),
Some(GlyphNameRef::StandardMac { index: 38 })
);
}
#[test]
fn v25_negative_offset_below_zero_yields_none() {
let num_glyphs: u16 = 1;
let mut tail = Vec::new();
tail.extend_from_slice(&num_glyphs.to_be_bytes());
tail.push((-1i8) as u8);
let mut bytes = header(POST_VERSION_25);
bytes.extend_from_slice(&tail);
let p = PostTable::parse(&bytes).unwrap();
assert!(p.glyph_name_ref(0).is_none());
}
#[test]
fn v25_offset_past_standard_set_yields_none() {
let num_glyphs: u16 = 251;
let mut tail = Vec::new();
tail.extend_from_slice(&num_glyphs.to_be_bytes());
for _ in 0..num_glyphs {
tail.push(127i8 as u8);
}
let mut bytes = header(POST_VERSION_25);
bytes.extend_from_slice(&tail);
let p = PostTable::parse(&bytes).unwrap();
assert_eq!(
p.glyph_name_ref(0),
Some(GlyphNameRef::StandardMac { index: 127 })
);
assert!(p.glyph_name_ref(250).is_none());
}
#[test]
fn rejects_unknown_version() {
let b = header(0x0004_0000);
assert!(matches!(PostTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn rejects_short_header() {
let b = vec![0u8; 31];
assert!(matches!(PostTable::parse(&b), Err(Error::UnexpectedEof)));
}
#[test]
fn v20_truncated_index_array_rejected() {
let mut tail = Vec::new();
tail.extend_from_slice(&2u16.to_be_bytes());
tail.extend_from_slice(&0u16.to_be_bytes()); let mut bytes = header(POST_VERSION_20);
bytes.extend_from_slice(&tail);
assert!(matches!(
PostTable::parse(&bytes),
Err(Error::UnexpectedEof)
));
}
#[test]
fn v20_pascal_index_out_of_pool_decodes_glyph_as_none() {
let num_glyphs: u16 = 1;
let mut tail = Vec::new();
tail.extend_from_slice(&num_glyphs.to_be_bytes());
tail.extend_from_slice(&258u16.to_be_bytes());
let mut bytes = header(POST_VERSION_20);
bytes.extend_from_slice(&tail);
let p = PostTable::parse(&bytes).unwrap();
assert!(p.glyph_name_ref(0).is_none());
}
#[test]
fn version_constants_sanity() {
assert_eq!(POST_VERSION_10, 0x0001_0000);
assert_eq!(POST_VERSION_20, 0x0002_0000);
assert_eq!(POST_VERSION_25, 0x0002_5000);
assert_eq!(POST_VERSION_30, 0x0003_0000);
assert_eq!(STANDARD_MAC_GLYPH_COUNT, 258);
assert_eq!(POST_HEADER_LEN, 32);
}
}