use crate::parser::{read_i16, read_u16, read_u32};
use crate::Error;
#[derive(Debug, Clone, Copy)]
pub struct PostTable<'a> {
raw_version: u32,
italic_angle_fixed: i32,
underline_position: i16,
underline_thickness: i16,
is_fixed_pitch_raw: u32,
min_mem_type42: u32,
max_mem_type42: u32,
min_mem_type1: u32,
max_mem_type1: u32,
num_glyphs: u16,
name_index_bytes: &'a [u8],
string_data: &'a [u8],
offset_bytes: &'a [u8],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PostFormat {
V1_0,
V2_0,
V2_5,
V3_0,
Other(u32),
}
impl<'a> PostTable<'a> {
pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
if bytes.len() < 32 {
return Err(Error::UnexpectedEof);
}
let raw_version = read_u32(bytes, 0)?;
let italic_angle_fixed = read_u32(bytes, 4)? as i32;
let underline_position = read_i16(bytes, 8)?;
let underline_thickness = read_i16(bytes, 10)?;
let is_fixed_pitch_raw = read_u32(bytes, 12)?;
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 (num_glyphs, name_index_bytes, string_data, offset_bytes) = match raw_version {
0x0002_0000 => {
if bytes.len() < 34 {
return Err(Error::BadStructure("post 2.0 truncated before numGlyphs"));
}
let n = read_u16(bytes, 32)?;
let idx_start = 34usize;
let idx_end = idx_start
.checked_add((n as usize).checked_mul(2).ok_or(Error::BadStructure(
"post 2.0 glyphNameIndex length overflow",
))?)
.ok_or(Error::BadStructure(
"post 2.0 glyphNameIndex length overflow",
))?;
if bytes.len() < idx_end {
return Err(Error::BadStructure(
"post 2.0 truncated inside glyphNameIndex",
));
}
let name_index = &bytes[idx_start..idx_end];
let strings = &bytes[idx_end..];
(n, name_index, strings, &[][..])
}
0x0002_5000 => {
if bytes.len() < 34 {
return Err(Error::BadStructure("post 2.5 truncated before numGlyphs"));
}
let n = read_u16(bytes, 32)?;
let off_start = 34usize;
let off_end = off_start
.checked_add(n as usize)
.ok_or(Error::BadStructure("post 2.5 offset length overflow"))?;
if bytes.len() < off_end {
return Err(Error::BadStructure("post 2.5 truncated inside offsets"));
}
let offsets = &bytes[off_start..off_end];
(n, &[][..], &[][..], offsets)
}
_ => (0u16, &[][..], &[][..], &[][..]),
};
Ok(Self {
raw_version,
italic_angle_fixed,
underline_position,
underline_thickness,
is_fixed_pitch_raw,
min_mem_type42,
max_mem_type42,
min_mem_type1,
max_mem_type1,
num_glyphs,
name_index_bytes,
string_data,
offset_bytes,
})
}
pub fn raw_version(&self) -> u32 {
self.raw_version
}
pub fn format(&self) -> PostFormat {
match self.raw_version {
0x0001_0000 => PostFormat::V1_0,
0x0002_0000 => PostFormat::V2_0,
0x0002_5000 => PostFormat::V2_5,
0x0003_0000 => PostFormat::V3_0,
other => PostFormat::Other(other),
}
}
pub fn italic_angle(&self) -> f64 {
self.italic_angle_fixed as f64 / 65536.0
}
pub fn italic_angle_fixed(&self) -> i32 {
self.italic_angle_fixed
}
pub fn underline_position(&self) -> i16 {
self.underline_position
}
pub fn underline_thickness(&self) -> i16 {
self.underline_thickness
}
pub fn is_fixed_pitch(&self) -> bool {
self.is_fixed_pitch_raw != 0
}
pub fn is_fixed_pitch_raw(&self) -> u32 {
self.is_fixed_pitch_raw
}
pub fn min_mem_type42(&self) -> u32 {
self.min_mem_type42
}
pub fn max_mem_type42(&self) -> u32 {
self.max_mem_type42
}
pub fn min_mem_type1(&self) -> u32 {
self.min_mem_type1
}
pub fn max_mem_type1(&self) -> u32 {
self.max_mem_type1
}
pub fn num_glyphs(&self) -> u16 {
self.num_glyphs
}
pub fn name_index(&self, glyph_id: u16) -> Option<u16> {
if self.raw_version != 0x0002_0000 || glyph_id >= self.num_glyphs {
return None;
}
let off = (glyph_id as usize) * 2;
let s = self.name_index_bytes.get(off..off + 2)?;
Some(u16::from_be_bytes([s[0], s[1]]))
}
pub fn name_string(&self, pascal_index: u16) -> Option<&'a [u8]> {
if self.raw_version != 0x0002_0000 {
return None;
}
let mut cursor = 0usize;
for current in 0..=pascal_index as usize {
let len_byte = *self.string_data.get(cursor)?;
let start = cursor + 1;
let end = start.checked_add(len_byte as usize)?;
if end > self.string_data.len() {
return None;
}
if current == pascal_index as usize {
return Some(&self.string_data[start..end]);
}
cursor = end;
}
None
}
pub fn standard_offset(&self, glyph_id: u16) -> Option<i8> {
if self.raw_version != 0x0002_5000 || glyph_id >= self.num_glyphs {
return None;
}
self.offset_bytes
.get(glyph_id as usize)
.copied()
.map(|b| b as i8)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_header(version: u32) -> Vec<u8> {
let mut b = vec![0u8; 32];
b[0..4].copy_from_slice(&version.to_be_bytes());
b[4..8].copy_from_slice(&(-786_432i32 as u32).to_be_bytes());
b[8..10].copy_from_slice(&(-75i16).to_be_bytes());
b[10..12].copy_from_slice(&(50i16).to_be_bytes());
b[12..16].copy_from_slice(&0u32.to_be_bytes());
b
}
#[test]
fn parses_v3_header() {
let bytes = build_header(0x0003_0000);
let p = PostTable::parse(&bytes).unwrap();
assert_eq!(p.format(), PostFormat::V3_0);
assert_eq!(p.raw_version(), 0x0003_0000);
assert!((p.italic_angle() - -12.0).abs() < 1e-12);
assert_eq!(p.italic_angle_fixed(), -786_432);
assert_eq!(p.underline_position(), -75);
assert_eq!(p.underline_thickness(), 50);
assert!(!p.is_fixed_pitch());
assert_eq!(p.num_glyphs(), 0);
}
#[test]
fn parses_v1_header() {
let bytes = build_header(0x0001_0000);
let p = PostTable::parse(&bytes).unwrap();
assert_eq!(p.format(), PostFormat::V1_0);
assert_eq!(p.num_glyphs(), 0);
assert_eq!(p.name_index(0), None);
assert_eq!(p.standard_offset(0), None);
assert_eq!(p.name_string(0), None);
}
#[test]
fn fixed_pitch_decodes_nonzero() {
let mut bytes = build_header(0x0003_0000);
bytes[12..16].copy_from_slice(&1u32.to_be_bytes());
let p = PostTable::parse(&bytes).unwrap();
assert!(p.is_fixed_pitch());
assert_eq!(p.is_fixed_pitch_raw(), 1);
}
#[test]
fn fixed_pitch_decodes_high_bit() {
let mut bytes = build_header(0x0003_0000);
bytes[12..16].copy_from_slice(&0x8000_0000u32.to_be_bytes());
let p = PostTable::parse(&bytes).unwrap();
assert!(p.is_fixed_pitch());
assert_eq!(p.is_fixed_pitch_raw(), 0x8000_0000);
}
#[test]
fn italic_angle_decodes_fractional() {
let mut bytes = build_header(0x0003_0000);
bytes[4..8].copy_from_slice(&(-622_592i32 as u32).to_be_bytes());
let p = PostTable::parse(&bytes).unwrap();
assert!((p.italic_angle() - -9.5).abs() < 1e-12);
}
#[test]
fn mem_fields_decode() {
let mut bytes = build_header(0x0003_0000);
bytes[16..20].copy_from_slice(&100u32.to_be_bytes());
bytes[20..24].copy_from_slice(&200u32.to_be_bytes());
bytes[24..28].copy_from_slice(&300u32.to_be_bytes());
bytes[28..32].copy_from_slice(&400u32.to_be_bytes());
let p = PostTable::parse(&bytes).unwrap();
assert_eq!(p.min_mem_type42(), 100);
assert_eq!(p.max_mem_type42(), 200);
assert_eq!(p.min_mem_type1(), 300);
assert_eq!(p.max_mem_type1(), 400);
}
#[test]
fn rejects_short_buffer() {
let bytes = vec![0u8; 16];
assert!(matches!(
PostTable::parse(&bytes),
Err(Error::UnexpectedEof)
));
}
#[test]
fn other_version_decodes_header_only() {
let bytes = build_header(0x0004_0000);
let p = PostTable::parse(&bytes).unwrap();
assert_eq!(p.format(), PostFormat::Other(0x0004_0000));
assert_eq!(p.num_glyphs(), 0);
assert_eq!(p.name_index(0), None);
}
fn build_v2(g0_idx: u16, g1_idx: u16) -> Vec<u8> {
let mut b = build_header(0x0002_0000);
b.extend_from_slice(&2u16.to_be_bytes());
b.extend_from_slice(&g0_idx.to_be_bytes());
b.extend_from_slice(&g1_idx.to_be_bytes());
b.push(7);
b.extend_from_slice(b"MyGlyph");
b
}
#[test]
fn v2_parses_header_and_indices() {
let bytes = build_v2(0, 258);
let p = PostTable::parse(&bytes).unwrap();
assert_eq!(p.format(), PostFormat::V2_0);
assert_eq!(p.num_glyphs(), 2);
assert_eq!(p.name_index(0), Some(0));
assert_eq!(p.name_index(1), Some(258));
assert_eq!(p.name_index(2), None);
}
#[test]
fn v2_resolves_pascal_string() {
let bytes = build_v2(0, 258);
let p = PostTable::parse(&bytes).unwrap();
let name = p.name_string(0).unwrap();
assert_eq!(name, b"MyGlyph");
assert!(p.name_string(1).is_none());
}
#[test]
fn v2_resolves_multiple_pascal_strings() {
let mut b = build_header(0x0002_0000);
b.extend_from_slice(&3u16.to_be_bytes());
b.extend_from_slice(&258u16.to_be_bytes());
b.extend_from_slice(&259u16.to_be_bytes());
b.extend_from_slice(&260u16.to_be_bytes());
b.push(1);
b.extend_from_slice(b"A");
b.push(2);
b.extend_from_slice(b"BB");
b.push(3);
b.extend_from_slice(b"CCC");
let p = PostTable::parse(&b).unwrap();
assert_eq!(p.name_string(0).unwrap(), b"A");
assert_eq!(p.name_string(1).unwrap(), b"BB");
assert_eq!(p.name_string(2).unwrap(), b"CCC");
assert!(p.name_string(3).is_none());
}
#[test]
fn v2_rejects_truncated_name_index() {
let mut b = build_header(0x0002_0000);
b.extend_from_slice(&4u16.to_be_bytes());
b.extend_from_slice(&0u16.to_be_bytes());
b.extend_from_slice(&0u16.to_be_bytes());
assert!(matches!(PostTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn v2_rejects_pascal_length_past_end() {
let mut b = build_header(0x0002_0000);
b.extend_from_slice(&1u16.to_be_bytes());
b.extend_from_slice(&258u16.to_be_bytes());
b.push(50);
b.push(b'x');
let p = PostTable::parse(&b).unwrap();
assert!(p.name_string(0).is_none());
}
#[test]
fn v25_decodes_offset_array() {
let mut b = build_header(0x0002_5000);
b.extend_from_slice(&3u16.to_be_bytes());
b.push(36);
b.push(36);
b.push(36);
let p = PostTable::parse(&b).unwrap();
assert_eq!(p.format(), PostFormat::V2_5);
assert_eq!(p.num_glyphs(), 3);
assert_eq!(p.standard_offset(0), Some(36));
assert_eq!(p.standard_offset(1), Some(36));
assert_eq!(p.standard_offset(2), Some(36));
assert_eq!(p.standard_offset(3), None);
}
#[test]
fn v25_decodes_negative_offset() {
let mut b = build_header(0x0002_5000);
b.extend_from_slice(&1u16.to_be_bytes());
b.push(0xFEu8); let p = PostTable::parse(&b).unwrap();
assert_eq!(p.standard_offset(0), Some(-2));
}
#[test]
fn v25_rejects_truncated_offset_array() {
let mut b = build_header(0x0002_5000);
b.extend_from_slice(&5u16.to_be_bytes());
b.push(0);
b.push(0);
assert!(matches!(PostTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn v1_v3_have_no_format_specific_data() {
for v in [0x0001_0000u32, 0x0003_0000u32] {
let bytes = build_header(v);
let p = PostTable::parse(&bytes).unwrap();
assert_eq!(p.num_glyphs(), 0);
assert!(p.name_index(0).is_none());
assert!(p.name_string(0).is_none());
assert!(p.standard_offset(0).is_none());
}
}
}