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";
#[doc(hidden)]
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;
pub static STANDARD_MAC_GLYPH_NAMES: [&str; STANDARD_MAC_GLYPH_COUNT as usize] = [
".notdef",
".null",
"nonmarkingreturn",
"space",
"exclam",
"quotedbl",
"numbersign",
"dollar",
"percent",
"ampersand",
"quotesingle",
"parenleft",
"parenright",
"asterisk",
"plus",
"comma",
"hyphen",
"period",
"slash",
"zero",
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"colon",
"semicolon",
"less",
"equal",
"greater",
"question",
"at",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"bracketleft",
"backslash",
"bracketright",
"asciicircum",
"underscore",
"grave",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"braceleft",
"bar",
"braceright",
"asciitilde",
"Adieresis",
"Aring",
"Ccedilla",
"Eacute",
"Ntilde",
"Odieresis",
"Udieresis",
"aacute",
"agrave",
"acircumflex",
"adieresis",
"atilde",
"aring",
"ccedilla",
"eacute",
"egrave",
"ecircumflex",
"edieresis",
"iacute",
"igrave",
"icircumflex",
"idieresis",
"ntilde",
"oacute",
"ograve",
"ocircumflex",
"odieresis",
"otilde",
"uacute",
"ugrave",
"ucircumflex",
"udieresis",
"dagger",
"degree",
"cent",
"sterling",
"section",
"bullet",
"paragraph",
"germandbls",
"registered",
"copyright",
"trademark",
"acute",
"dieresis",
"notequal",
"AE",
"Oslash",
"infinity",
"plusminus",
"lessequal",
"greaterequal",
"yen",
"mu",
"partialdiff",
"summation",
"product",
"pi",
"integral",
"ordfeminine",
"ordmasculine",
"Omega",
"ae",
"oslash",
"questiondown",
"exclamdown",
"logicalnot",
"radical",
"florin",
"approxequal",
"Delta",
"guillemotleft",
"guillemotright",
"ellipsis",
"nonbreakingspace",
"Agrave",
"Atilde",
"Otilde",
"OE",
"oe",
"endash",
"emdash",
"quotedblleft",
"quotedblright",
"quoteleft",
"quoteright",
"divide",
"lozenge",
"ydieresis",
"Ydieresis",
"fraction",
"currency",
"guilsinglleft",
"guilsinglright",
"fi",
"fl",
"daggerdbl",
"periodcentered",
"quotesinglbase",
"quotedblbase",
"perthousand",
"Acircumflex",
"Ecircumflex",
"Aacute",
"Edieresis",
"Egrave",
"Iacute",
"Icircumflex",
"Idieresis",
"Igrave",
"Oacute",
"Ocircumflex",
"apple",
"Ograve",
"Uacute",
"Ucircumflex",
"Ugrave",
"dotlessi",
"circumflex",
"tilde",
"macron",
"breve",
"dotaccent",
"ring",
"cedilla",
"hungarumlaut",
"ogonek",
"caron",
"Lslash",
"lslash",
"Scaron",
"scaron",
"Zcaron",
"zcaron",
"brokenbar",
"Eth",
"eth",
"Yacute",
"yacute",
"Thorn",
"thorn",
"minus",
"multiply",
"onesuperior",
"twosuperior",
"threesuperior",
"onehalf",
"onequarter",
"threequarters",
"franc",
"Gbreve",
"gbreve",
"Idotaccent",
"Scedilla",
"scedilla",
"Cacute",
"cacute",
"Ccaron",
"ccaron",
"dcroat",
];
pub fn standard_mac_glyph_name(index: u16) -> Option<&'static str> {
STANDARD_MAC_GLYPH_NAMES.get(index as usize).copied()
}
#[derive(Debug, Clone)]
#[doc(hidden)]
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,
}
}
pub fn resolved_glyph_name(&self, gid: u16) -> Option<&str> {
match self.glyph_name_ref(gid)? {
GlyphNameRef::Custom(s) => Some(s),
GlyphNameRef::StandardMac { index } => standard_mac_glyph_name(index),
}
}
pub fn named_glyph_count(&self) -> u16 {
match &self.format {
PostFormat::Version10 => STANDARD_MAC_GLYPH_COUNT,
PostFormat::Version20(v) => v.num_glyphs,
PostFormat::Version25(v) => v.num_glyphs,
PostFormat::Version30 => 0,
}
}
pub fn gid_for_name(&self, name: &str) -> Option<u16> {
if matches!(self.format, PostFormat::Version30) {
return None;
}
let count = self.named_glyph_count();
let std_target = STANDARD_MAC_GLYPH_NAMES
.iter()
.position(|n| *n == name)
.map(|i| i as u16);
for gid in 0..count {
match self.glyph_name_ref(gid) {
Some(GlyphNameRef::StandardMac { index }) if Some(index) == std_target => {
return Some(gid);
}
Some(GlyphNameRef::Custom(s)) if s == name => {
return Some(gid);
}
_ => {}
}
}
None
}
pub fn iter_glyph_names(&self) -> impl Iterator<Item = (u16, &str)> + '_ {
let count = self.named_glyph_count();
(0..count).filter_map(move |gid| self.resolved_glyph_name(gid).map(|n| (gid, n)))
}
}
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());
assert_eq!(p.resolved_glyph_name(0), Some(".notdef"));
assert_eq!(p.resolved_glyph_name(217), Some("tilde"));
assert!(p.resolved_glyph_name(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());
assert_eq!(p.resolved_glyph_name(302), Some("tilde"));
assert_eq!(p.resolved_glyph_name(408), Some("weird"));
}
#[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 })
);
assert_eq!(p.resolved_glyph_name(0), Some("A"));
assert_eq!(p.resolved_glyph_name(1), Some("B"));
assert_eq!(p.resolved_glyph_name(2), Some("C"));
}
#[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 standard_mac_glyph_names_table_is_well_formed() {
assert_eq!(STANDARD_MAC_GLYPH_NAMES.len(), 258);
assert_eq!(
STANDARD_MAC_GLYPH_NAMES.len(),
STANDARD_MAC_GLYPH_COUNT as usize
);
let mut sorted: Vec<&str> = STANDARD_MAC_GLYPH_NAMES.to_vec();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), 258, "all 258 names must be distinct");
}
#[test]
fn standard_mac_glyph_name_spot_checks() {
assert_eq!(standard_mac_glyph_name(0), Some(".notdef"));
assert_eq!(standard_mac_glyph_name(1), Some(".null"));
assert_eq!(standard_mac_glyph_name(2), Some("nonmarkingreturn"));
assert_eq!(standard_mac_glyph_name(3), Some("space"));
assert_eq!(standard_mac_glyph_name(36), Some("A"));
assert_eq!(standard_mac_glyph_name(192), Some("fi"));
assert_eq!(standard_mac_glyph_name(193), Some("fl"));
assert_eq!(standard_mac_glyph_name(217), Some("tilde"));
assert_eq!(standard_mac_glyph_name(257), Some("dcroat"));
assert_eq!(standard_mac_glyph_name(258), None);
assert_eq!(standard_mac_glyph_name(u16::MAX), 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 v10_reverse_lookup_spans_full_standard_set() {
let b = header(POST_VERSION_10);
let p = PostTable::parse(&b).unwrap();
assert_eq!(p.named_glyph_count(), 258);
assert_eq!(p.gid_for_name(".notdef"), Some(0));
assert_eq!(p.gid_for_name("A"), Some(36));
assert_eq!(p.gid_for_name("tilde"), Some(217));
assert_eq!(p.gid_for_name("dcroat"), Some(257));
assert_eq!(p.gid_for_name("Alpha"), None);
assert_eq!(p.gid_for_name(""), None);
}
#[test]
fn v20_reverse_lookup_covers_custom_and_standard() {
let num_glyphs: u16 = 4;
let mut tail = Vec::new();
tail.extend_from_slice(&num_glyphs.to_be_bytes());
for idx in [0u16, 36, 258, 259] {
tail.extend_from_slice(&idx.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.named_glyph_count(), 4);
assert_eq!(p.gid_for_name(".notdef"), Some(0));
assert_eq!(p.gid_for_name("A"), Some(1));
assert_eq!(p.gid_for_name("Alpha"), Some(2));
assert_eq!(p.gid_for_name("Beta"), Some(3));
assert_eq!(p.gid_for_name("missing"), None);
}
#[test]
fn reverse_lookup_returns_lowest_gid_on_duplicate() {
let num_glyphs: u16 = 3;
let mut tail = Vec::new();
tail.extend_from_slice(&num_glyphs.to_be_bytes());
for idx in [0u16, 36, 36] {
tail.extend_from_slice(&idx.to_be_bytes());
}
let mut bytes = header(POST_VERSION_20);
bytes.extend_from_slice(&tail);
let p = PostTable::parse(&bytes).unwrap();
assert_eq!(p.gid_for_name("A"), Some(1));
}
#[test]
fn v25_reverse_lookup_inverts_offset() {
let num_glyphs: u16 = 3;
let mut tail = Vec::new();
tail.extend_from_slice(&num_glyphs.to_be_bytes());
for _ in 0..3 {
tail.push(36i8 as u8);
}
let mut bytes = header(POST_VERSION_25);
bytes.extend_from_slice(&tail);
let p = PostTable::parse(&bytes).unwrap();
assert_eq!(p.named_glyph_count(), 3);
assert_eq!(p.gid_for_name("A"), Some(0));
assert_eq!(p.gid_for_name("B"), Some(1));
assert_eq!(p.gid_for_name("C"), Some(2));
assert_eq!(p.gid_for_name("D"), None);
}
#[test]
fn v30_reverse_lookup_and_iter_are_empty() {
let b = header(POST_VERSION_30);
let p = PostTable::parse(&b).unwrap();
assert_eq!(p.named_glyph_count(), 0);
assert_eq!(p.gid_for_name(".notdef"), None);
assert_eq!(p.iter_glyph_names().count(), 0);
}
#[test]
fn iter_glyph_names_round_trips_through_reverse_lookup() {
let num_glyphs: u16 = 4;
let mut tail = Vec::new();
tail.extend_from_slice(&num_glyphs.to_be_bytes());
for idx in [0u16, 36, 258, 259] {
tail.extend_from_slice(&idx.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();
let pairs: Vec<(u16, &str)> = p.iter_glyph_names().collect();
assert_eq!(
pairs,
vec![(0, ".notdef"), (1, "A"), (2, "Alpha"), (3, "Beta")]
);
for (gid, name) in pairs {
assert_eq!(p.gid_for_name(name), Some(gid));
}
}
#[test]
fn v20_iter_skips_unsatisfiable_pascal_reference() {
let num_glyphs: u16 = 2;
let mut tail = Vec::new();
tail.extend_from_slice(&num_glyphs.to_be_bytes());
tail.extend_from_slice(&0u16.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();
let pairs: Vec<(u16, &str)> = p.iter_glyph_names().collect();
assert_eq!(pairs, vec![(0, ".notdef")]);
}
#[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);
}
}