use crate::parser::{read_i8, read_u16, read_u32, read_u8};
use crate::Error;
pub const PCLT_TABLE_TAG: u32 = 0x5043_4C54;
pub const PCLT_TABLE_LEN: usize = 54;
pub const PCLT_MAJOR_VERSION: u16 = 1;
pub const PCLT_STROKE_WEIGHT_RANGE: core::ops::RangeInclusive<i8> = -7..=7;
pub const PCLT_WIDTH_TYPE_RANGE: core::ops::RangeInclusive<i8> = -5..=5;
#[derive(Debug, Clone)]
pub struct PcltTable {
major_version: u16,
minor_version: u16,
font_number: u32,
pitch: u16,
x_height: u16,
style: u16,
type_family: u16,
cap_height: u16,
symbol_set: u16,
typeface: [u8; 16],
character_complement: [u8; 8],
file_name: [u8; 6],
stroke_weight: i8,
width_type: i8,
serif_style: u8,
reserved: u8,
}
impl PcltTable {
pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < PCLT_TABLE_LEN {
return Err(Error::UnexpectedEof);
}
let major_version = read_u16(bytes, 0)?;
if major_version != PCLT_MAJOR_VERSION {
return Err(Error::BadStructure("PCLT: unrecognised majorVersion"));
}
let minor_version = read_u16(bytes, 2)?;
let font_number = read_u32(bytes, 4)?;
let pitch = read_u16(bytes, 8)?;
let x_height = read_u16(bytes, 10)?;
let style = read_u16(bytes, 12)?;
let type_family = read_u16(bytes, 14)?;
let cap_height = read_u16(bytes, 16)?;
let symbol_set = read_u16(bytes, 18)?;
let mut typeface = [0u8; 16];
typeface.copy_from_slice(&bytes[20..36]);
let mut character_complement = [0u8; 8];
character_complement.copy_from_slice(&bytes[36..44]);
let mut file_name = [0u8; 6];
file_name.copy_from_slice(&bytes[44..50]);
let stroke_weight = read_i8(bytes, 50)?;
let width_type = read_i8(bytes, 51)?;
let serif_style = read_u8(bytes, 52)?;
let reserved = read_u8(bytes, 53)?;
Ok(Self {
major_version,
minor_version,
font_number,
pitch,
x_height,
style,
type_family,
cap_height,
symbol_set,
typeface,
character_complement,
file_name,
stroke_weight,
width_type,
serif_style,
reserved,
})
}
pub fn major_version(&self) -> u16 {
self.major_version
}
pub fn minor_version(&self) -> u16 {
self.minor_version
}
pub fn font_number(&self) -> u32 {
self.font_number
}
pub fn font_number_is_native(&self) -> bool {
self.font_number & 0x8000_0000 == 0
}
pub fn font_number_vendor_code(&self) -> u8 {
((self.font_number >> 24) & 0x7F) as u8
}
pub fn font_number_vendor_assigned(&self) -> u32 {
self.font_number & 0x00FF_FFFF
}
pub fn pitch(&self) -> u16 {
self.pitch
}
pub fn x_height(&self) -> u16 {
self.x_height
}
pub fn cap_height(&self) -> u16 {
self.cap_height
}
pub fn style(&self) -> u16 {
self.style
}
pub fn style_posture(&self) -> u8 {
(self.style & 0x0003) as u8
}
pub fn style_width(&self) -> u8 {
((self.style >> 2) & 0x0007) as u8
}
pub fn style_structure(&self) -> u8 {
((self.style >> 5) & 0x001F) as u8
}
pub fn style_reserved_bits(&self) -> u8 {
(self.style >> 10) as u8
}
pub fn type_family(&self) -> u16 {
self.type_family
}
pub fn type_family_vendor_code(&self) -> u8 {
(self.type_family >> 12) as u8
}
pub fn type_family_code(&self) -> u16 {
self.type_family & 0x0FFF
}
pub fn symbol_set(&self) -> u16 {
self.symbol_set
}
pub fn symbol_set_number(&self) -> u16 {
self.symbol_set >> 5
}
pub fn symbol_set_id(&self) -> u8 {
(self.symbol_set & 0x001F) as u8 + 64
}
pub fn typeface_raw(&self) -> &[u8; 16] {
&self.typeface
}
pub fn typeface(&self) -> Option<&str> {
trim_ascii_field(&self.typeface)
}
pub fn character_complement_raw(&self) -> &[u8; 8] {
&self.character_complement
}
pub fn character_complement(&self) -> u64 {
u64::from_be_bytes(self.character_complement)
}
pub fn provides_collection(&self, bit: u8) -> bool {
bit < 64 && self.character_complement() & (1u64 << bit) == 0
}
pub fn is_unicode_indexed(&self) -> bool {
self.character_complement() & 1 == 0
}
pub fn file_name_raw(&self) -> &[u8; 6] {
&self.file_name
}
pub fn file_name(&self) -> Option<&str> {
trim_ascii_field(&self.file_name)
}
pub fn file_name_treatment(&self) -> u8 {
self.file_name[3]
}
pub fn stroke_weight(&self) -> i8 {
self.stroke_weight
}
pub fn stroke_weight_is_valid(&self) -> bool {
PCLT_STROKE_WEIGHT_RANGE.contains(&self.stroke_weight)
}
pub fn width_type(&self) -> i8 {
self.width_type
}
pub fn width_type_is_valid(&self) -> bool {
PCLT_WIDTH_TYPE_RANGE.contains(&self.width_type)
}
pub fn serif_style(&self) -> u8 {
self.serif_style
}
pub fn serif_style_value(&self) -> u8 {
self.serif_style & 0x3F
}
pub fn serif_style_class(&self) -> u8 {
self.serif_style >> 6
}
pub fn reserved(&self) -> u8 {
self.reserved
}
}
fn trim_ascii_field(field: &[u8]) -> Option<&str> {
let mut end = field.len();
while end > 0 && (field[end - 1] == 0 || field[end - 1] == b' ') {
end -= 1;
}
let trimmed = &field[..end];
if trimmed.iter().all(|&b| (0x20..0x7F).contains(&b)) {
core::str::from_utf8(trimmed).ok()
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
struct PcltBuilder {
major: u16,
minor: u16,
font_number: u32,
pitch: u16,
x_height: u16,
style: u16,
type_family: u16,
cap_height: u16,
symbol_set: u16,
typeface: [u8; 16],
complement: [u8; 8],
file_name: [u8; 6],
stroke_weight: i8,
width_type: i8,
serif_style: u8,
reserved: u8,
}
impl PcltBuilder {
fn new() -> Self {
let mut typeface = [b' '; 16];
typeface[..9].copy_from_slice(b"Times New");
Self {
major: PCLT_MAJOR_VERSION,
minor: 0,
font_number: (u32::from(b'B') << 24) | 5,
pitch: 569,
x_height: 1062,
style: 0,
type_family: (2 << 12) | 517,
cap_height: 1466,
symbol_set: 629,
typeface,
complement: 0xFFFF_FFFF_37FF_FFFEu64.to_be_bytes(),
file_name: *b"TNRR00",
stroke_weight: 0,
width_type: 0,
serif_style: (2 << 6) | 6,
reserved: 0,
}
}
fn build(&self) -> Vec<u8> {
let mut b = Vec::with_capacity(PCLT_TABLE_LEN);
b.extend_from_slice(&self.major.to_be_bytes());
b.extend_from_slice(&self.minor.to_be_bytes());
b.extend_from_slice(&self.font_number.to_be_bytes());
b.extend_from_slice(&self.pitch.to_be_bytes());
b.extend_from_slice(&self.x_height.to_be_bytes());
b.extend_from_slice(&self.style.to_be_bytes());
b.extend_from_slice(&self.type_family.to_be_bytes());
b.extend_from_slice(&self.cap_height.to_be_bytes());
b.extend_from_slice(&self.symbol_set.to_be_bytes());
b.extend_from_slice(&self.typeface);
b.extend_from_slice(&self.complement);
b.extend_from_slice(&self.file_name);
b.push(self.stroke_weight as u8);
b.push(self.width_type as u8);
b.push(self.serif_style);
b.push(self.reserved);
assert_eq!(b.len(), PCLT_TABLE_LEN);
b
}
}
#[test]
fn parses_baseline_table() {
let bytes = PcltBuilder::new().build();
let t = PcltTable::parse(&bytes).expect("parse");
assert_eq!(t.major_version(), 1);
assert_eq!(t.minor_version(), 0);
assert_eq!(t.pitch(), 569);
assert_eq!(t.x_height(), 1062);
assert_eq!(t.cap_height(), 1466);
assert_eq!(t.reserved(), 0);
}
#[test]
fn font_number_segments_decode() {
let mut b = PcltBuilder::new();
b.font_number = (u32::from(b'M') << 24) | 0x00AB_CDEF;
let t = PcltTable::parse(&b.build()).expect("parse");
assert!(t.font_number_is_native());
assert_eq!(t.font_number_vendor_code(), b'M');
assert_eq!(t.font_number_vendor_assigned(), 0x00AB_CDEF);
b.font_number |= 0x8000_0000;
let t = PcltTable::parse(&b.build()).expect("parse");
assert!(!t.font_number_is_native());
assert_eq!(t.font_number_vendor_code(), b'M' & 0x7F);
}
#[test]
fn style_word_bitfields_decode() {
let mut b = PcltBuilder::new();
b.style = (4 << 5) | (1 << 2) | 2;
let t = PcltTable::parse(&b.build()).expect("parse");
assert_eq!(t.style_structure(), 4);
assert_eq!(t.style_width(), 1);
assert_eq!(t.style_posture(), 2);
assert_eq!(t.style_reserved_bits(), 0);
b.style |= 0b101010 << 10;
let t = PcltTable::parse(&b.build()).expect("parse");
assert_eq!(t.style_reserved_bits(), 0b101010);
assert_eq!(t.style_structure(), 4);
}
#[test]
fn type_family_vendor_and_code_decode() {
let mut b = PcltBuilder::new();
b.type_family = (5 << 12) | 0x123;
let t = PcltTable::parse(&b.build()).expect("parse");
assert_eq!(t.type_family_vendor_code(), 5);
assert_eq!(t.type_family_code(), 0x123);
}
#[test]
fn symbol_set_examples_from_spec_decode() {
let cases: &[(u16, u16, u8)] = &[
(629, 19, b'U'), (309, 9, b'U'), (621, 19, b'M'), (394, 12, b'J'), (362, 11, b'J'), (330, 10, b'J'), (298, 9, b'J'), (234, 7, b'J'), ];
for &(decimal, number, id) in cases {
let mut b = PcltBuilder::new();
b.symbol_set = decimal;
let t = PcltTable::parse(&b.build()).expect("parse");
assert_eq!(t.symbol_set_number(), number, "decimal {decimal}");
assert_eq!(t.symbol_set_id(), id, "decimal {decimal}");
}
let mut b = PcltBuilder::new();
b.symbol_set = 0;
let t = PcltTable::parse(&b.build()).expect("parse");
assert_eq!(t.symbol_set(), 0);
}
#[test]
fn typeface_string_trims_trailing_pad() {
let bytes = PcltBuilder::new().build();
let t = PcltTable::parse(&bytes).expect("parse");
assert_eq!(t.typeface(), Some("Times New"));
assert_eq!(t.typeface_raw().len(), 16);
let mut b = PcltBuilder::new();
b.typeface = *b"Times New Bd ";
let t = PcltTable::parse(&b.build()).expect("parse");
assert_eq!(t.typeface(), Some("Times New Bd"));
let mut b = PcltBuilder::new();
b.typeface[0] = 0xC0;
let t = PcltTable::parse(&b.build()).expect("parse");
assert_eq!(t.typeface(), None);
assert_eq!(t.typeface_raw()[0], 0xC0);
}
#[test]
fn character_complement_examples_from_spec() {
let t = PcltTable::parse(&PcltBuilder::new().build()).expect("parse");
assert_eq!(t.character_complement(), 0xFFFF_FFFF_37FF_FFFE);
assert!(t.provides_collection(31));
assert!(t.provides_collection(30));
assert!(!t.provides_collection(29)); assert!(!t.provides_collection(28)); assert!(t.provides_collection(27));
assert!(!t.provides_collection(26));
assert!(t.is_unicode_indexed());
assert!(!t.provides_collection(64));
let mut b = PcltBuilder::new();
b.complement = 0xFFFF_FFFF_FFFF_FFFEu64.to_be_bytes();
let t = PcltTable::parse(&b.build()).expect("parse");
assert!(t.is_unicode_indexed());
for bit in 22..=31 {
assert!(!t.provides_collection(bit));
}
let mut b = PcltBuilder::new();
b.complement = 0xFFFF_FFFF_3BFF_FFFEu64.to_be_bytes();
let t = PcltTable::parse(&b.build()).expect("parse");
assert!(t.provides_collection(31));
assert!(t.provides_collection(30));
assert!(t.provides_collection(26));
assert!(!t.provides_collection(27));
}
#[test]
fn file_name_parts_decode() {
let t = PcltTable::parse(&PcltBuilder::new().build()).expect("parse");
assert_eq!(t.file_name(), Some("TNRR00"));
assert_eq!(t.file_name_treatment(), b'R');
let mut b = PcltBuilder::new();
b.file_name = *b"TNRJ00";
let t = PcltTable::parse(&b.build()).expect("parse");
assert_eq!(t.file_name_treatment(), b'J');
}
#[test]
fn stroke_weight_and_width_type_ranges() {
let mut b = PcltBuilder::new();
b.stroke_weight = 3; b.width_type = -2; let t = PcltTable::parse(&b.build()).expect("parse");
assert_eq!(t.stroke_weight(), 3);
assert!(t.stroke_weight_is_valid());
assert_eq!(t.width_type(), -2);
assert!(t.width_type_is_valid());
b.stroke_weight = -8;
b.width_type = 6;
let t = PcltTable::parse(&b.build()).expect("parse");
assert!(!t.stroke_weight_is_valid());
assert!(!t.width_type_is_valid());
}
#[test]
fn serif_style_bitfields_decode() {
let t = PcltTable::parse(&PcltBuilder::new().build()).expect("parse");
assert_eq!(t.serif_style_class(), 2);
assert_eq!(t.serif_style_value(), 6);
let mut b = PcltBuilder::new();
b.serif_style = (1 << 6) | 1;
let t = PcltTable::parse(&b.build()).expect("parse");
assert_eq!(t.serif_style_class(), 1);
assert_eq!(t.serif_style_value(), 1);
}
#[test]
fn rejects_unknown_major_version() {
let mut b = PcltBuilder::new();
b.major = 2;
assert!(matches!(
PcltTable::parse(&b.build()),
Err(Error::BadStructure(_))
));
}
#[test]
fn minor_version_is_surfaced_not_rejected() {
let mut b = PcltBuilder::new();
b.minor = 1;
let t = PcltTable::parse(&b.build()).expect("parse");
assert_eq!(t.minor_version(), 1);
}
#[test]
fn rejects_short_slice() {
let bytes = PcltBuilder::new().build();
for len in [0usize, 4, 20, PCLT_TABLE_LEN - 1] {
assert!(
matches!(PcltTable::parse(&bytes[..len]), Err(Error::UnexpectedEof)),
"len {len}"
);
}
}
#[test]
fn accepts_trailing_pad_bytes() {
let mut bytes = PcltBuilder::new().build();
bytes.extend_from_slice(&[0u8, 0u8]);
let t = PcltTable::parse(&bytes).expect("parse");
assert_eq!(t.pitch(), 569);
}
#[test]
fn reserved_pad_byte_is_surfaced() {
let mut b = PcltBuilder::new();
b.reserved = 0x7E;
let t = PcltTable::parse(&b.build()).expect("parse");
assert_eq!(t.reserved(), 0x7E);
}
#[test]
fn tag_bytes_match_constant() {
assert_eq!(
u32::from_be_bytes(*b"PCLT"),
PCLT_TABLE_TAG,
"PCLT_TABLE_TAG = 0x{:08X}",
PCLT_TABLE_TAG
);
}
}