use crate::parser::{read_i16, read_i32, read_u16, read_u8};
use crate::Error;
pub const HDMX_TABLE_TAG: u32 = 0x6864_6D78;
pub const HDMX_VERSION_0: u16 = 0;
pub const HDMX_HEADER_LEN: usize = 8;
pub const HDMX_RECORD_HEADER_LEN: usize = 2;
#[derive(Debug, Clone)]
pub struct HdmxRecord {
pixel_size: u8,
max_width: u8,
widths: Vec<u8>,
}
impl HdmxRecord {
pub fn pixel_size(&self) -> u8 {
self.pixel_size
}
pub fn max_width(&self) -> u8 {
self.max_width
}
pub fn widths(&self) -> &[u8] {
&self.widths
}
pub fn advance_pixels(&self, glyph_id: u16) -> Option<u8> {
self.widths.get(glyph_id as usize).copied()
}
}
#[derive(Debug, Clone)]
pub struct HdmxTable {
version: u16,
size_device_record: i32,
records: Vec<HdmxRecord>,
}
impl HdmxTable {
pub fn parse(bytes: &[u8], expected_num_glyphs: u16) -> Result<Self, Error> {
if bytes.len() < HDMX_HEADER_LEN {
return Err(Error::UnexpectedEof);
}
let version = read_u16(bytes, 0)?;
if version != HDMX_VERSION_0 {
return Err(Error::BadStructure("hdmx: unrecognised version"));
}
let num_records = read_i16(bytes, 2)?;
if num_records < 0 {
return Err(Error::BadStructure("hdmx: negative numRecords"));
}
let size_device_record = read_i32(bytes, 4)?;
let num_glyphs = expected_num_glyphs as usize;
let min_stride = HDMX_RECORD_HEADER_LEN
.checked_add(num_glyphs)
.ok_or(Error::BadStructure("hdmx: numGlyphs overflow"))?;
if size_device_record < 0 || (size_device_record as usize) < min_stride {
return Err(Error::BadStructure("hdmx: sizeDeviceRecord too small"));
}
let stride = size_device_record as usize;
let total = stride
.checked_mul(num_records as usize)
.and_then(|n| n.checked_add(HDMX_HEADER_LEN))
.ok_or(Error::BadStructure("hdmx: record array overflow"))?;
if bytes.len() < total {
return Err(Error::UnexpectedEof);
}
let mut records = Vec::with_capacity(num_records as usize);
let mut prev_ppem: Option<u8> = None;
for i in 0..num_records as usize {
let off = HDMX_HEADER_LEN + i * stride;
let pixel_size = read_u8(bytes, off)?;
let max_width = read_u8(bytes, off + 1)?;
if let Some(prev) = prev_ppem {
if pixel_size <= prev {
return Err(Error::BadStructure(
"hdmx: pixelSize not strictly increasing",
));
}
}
prev_ppem = Some(pixel_size);
let widths_off = off + HDMX_RECORD_HEADER_LEN;
let widths = bytes[widths_off..widths_off + num_glyphs].to_vec();
records.push(HdmxRecord {
pixel_size,
max_width,
widths,
});
}
Ok(Self {
version,
size_device_record,
records,
})
}
pub fn version_raw(&self) -> u16 {
self.version
}
pub fn num_records(&self) -> u16 {
self.records.len() as u16
}
pub fn size_device_record(&self) -> i32 {
self.size_device_record
}
pub fn records(&self) -> &[HdmxRecord] {
&self.records
}
pub fn record_for_ppem(&self, ppem: u8) -> Option<&HdmxRecord> {
match self
.records
.binary_search_by_key(&ppem, HdmxRecord::pixel_size)
{
Ok(i) => self.records.get(i),
Err(_) => None,
}
}
pub fn advance_pixels(&self, glyph_id: u16, ppem: u8) -> Option<u8> {
self.record_for_ppem(ppem)?.advance_pixels(glyph_id)
}
pub fn recorded_ppem_sizes(&self) -> Vec<u8> {
self.records.iter().map(HdmxRecord::pixel_size).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_hdmx(version: u16, num_glyphs: usize, recs: &[(u8, u8, &[u8])]) -> Vec<u8> {
let min_stride = HDMX_RECORD_HEADER_LEN + num_glyphs;
let stride = (min_stride + 3) & !3;
let mut out = Vec::with_capacity(HDMX_HEADER_LEN + stride * recs.len());
out.extend_from_slice(&version.to_be_bytes());
out.extend_from_slice(&(recs.len() as i16).to_be_bytes());
out.extend_from_slice(&(stride as i32).to_be_bytes());
for &(ppem, max_w, widths) in recs {
assert_eq!(widths.len(), num_glyphs);
out.push(ppem);
out.push(max_w);
out.extend_from_slice(widths);
let pad = stride - HDMX_RECORD_HEADER_LEN - num_glyphs;
out.resize(out.len() + pad, 0);
}
out
}
#[test]
fn parses_two_record_table() {
let bytes = make_hdmx(
HDMX_VERSION_0,
3,
&[(12, 7, &[0, 6, 7]), (16, 9, &[0, 8, 9])],
);
let t = HdmxTable::parse(&bytes, 3).expect("parse");
assert_eq!(t.version_raw(), 0);
assert_eq!(t.num_records(), 2);
assert_eq!(t.size_device_record(), 8);
assert_eq!(t.recorded_ppem_sizes(), vec![12, 16]);
assert_eq!(t.advance_pixels(0, 12), Some(0));
assert_eq!(t.advance_pixels(2, 16), Some(9));
assert_eq!(t.advance_pixels(2, 14), None); assert_eq!(t.advance_pixels(3, 12), None);
let r12 = t.record_for_ppem(12).expect("ppem 12 record");
assert_eq!(r12.max_width(), 7);
assert_eq!(r12.widths(), &[0, 6, 7]);
assert_eq!(r12.advance_pixels(1), Some(6));
}
#[test]
fn rejects_short_header() {
let bytes = vec![0u8; 7];
assert!(matches!(
HdmxTable::parse(&bytes, 0),
Err(Error::UnexpectedEof)
));
}
#[test]
fn rejects_unknown_version() {
let mut bytes = vec![0u8; HDMX_HEADER_LEN];
bytes[0..2].copy_from_slice(&1u16.to_be_bytes());
assert!(matches!(
HdmxTable::parse(&bytes, 0),
Err(Error::BadStructure(_))
));
}
#[test]
fn rejects_negative_num_records() {
let mut bytes = vec![0u8; HDMX_HEADER_LEN];
bytes[2..4].copy_from_slice(&(-1i16).to_be_bytes());
assert!(matches!(
HdmxTable::parse(&bytes, 0),
Err(Error::BadStructure(_))
));
}
#[test]
fn rejects_too_small_stride() {
let mut bytes = vec![0u8; HDMX_HEADER_LEN];
bytes[2..4].copy_from_slice(&1i16.to_be_bytes());
bytes[4..8].copy_from_slice(&5i32.to_be_bytes());
assert!(matches!(
HdmxTable::parse(&bytes, 4),
Err(Error::BadStructure(_))
));
}
#[test]
fn rejects_truncated_body() {
let bytes = make_hdmx(HDMX_VERSION_0, 3, &[(12, 7, &[0, 6, 7])]);
let mut bytes2 = bytes.clone();
bytes2[2..4].copy_from_slice(&2i16.to_be_bytes());
assert!(matches!(
HdmxTable::parse(&bytes2, 3),
Err(Error::UnexpectedEof)
));
}
#[test]
fn rejects_non_monotonic_pixel_size() {
let bytes = make_hdmx(HDMX_VERSION_0, 2, &[(12, 5, &[0, 5]), (12, 6, &[0, 6])]);
assert!(matches!(
HdmxTable::parse(&bytes, 2),
Err(Error::BadStructure(_))
));
let bytes2 = make_hdmx(HDMX_VERSION_0, 2, &[(16, 6, &[0, 6]), (12, 5, &[0, 5])]);
assert!(matches!(
HdmxTable::parse(&bytes2, 2),
Err(Error::BadStructure(_))
));
}
#[test]
fn binary_search_picks_exact_ppem_only() {
let bytes = make_hdmx(
HDMX_VERSION_0,
2,
&[(10, 4, &[0, 4]), (14, 6, &[0, 6]), (20, 9, &[0, 9])],
);
let t = HdmxTable::parse(&bytes, 2).expect("parse");
assert!(t.record_for_ppem(10).is_some());
assert!(t.record_for_ppem(14).is_some());
assert!(t.record_for_ppem(20).is_some());
assert!(t.record_for_ppem(11).is_none());
assert!(t.record_for_ppem(16).is_none());
assert!(t.record_for_ppem(255).is_none());
}
#[test]
fn tolerates_extra_padding_in_stride() {
let num_glyphs = 4usize;
let stride = 12usize;
let mut bytes = Vec::with_capacity(HDMX_HEADER_LEN + stride * 2);
bytes.extend_from_slice(&HDMX_VERSION_0.to_be_bytes());
bytes.extend_from_slice(&2i16.to_be_bytes());
bytes.extend_from_slice(&(stride as i32).to_be_bytes());
bytes.push(12);
bytes.push(4);
bytes.extend_from_slice(&[1, 2, 3, 4]);
bytes.extend_from_slice(&[0u8; 6]);
bytes.push(16);
bytes.push(5);
bytes.extend_from_slice(&[2, 3, 4, 5]);
bytes.extend_from_slice(&[0u8; 6]);
let t = HdmxTable::parse(&bytes, num_glyphs as u16).expect("parse");
assert_eq!(t.size_device_record(), 12);
assert_eq!(t.advance_pixels(0, 12), Some(1));
assert_eq!(t.advance_pixels(3, 16), Some(5));
}
#[test]
fn empty_table_round_trips() {
let mut bytes = Vec::with_capacity(HDMX_HEADER_LEN);
bytes.extend_from_slice(&HDMX_VERSION_0.to_be_bytes());
bytes.extend_from_slice(&0i16.to_be_bytes());
bytes.extend_from_slice(&8i32.to_be_bytes());
let t = HdmxTable::parse(&bytes, 5).expect("parse");
assert_eq!(t.num_records(), 0);
assert!(t.records().is_empty());
assert!(t.record_for_ppem(12).is_none());
assert!(t.advance_pixels(0, 12).is_none());
}
}