use crate::parser::{read_u16, read_u8};
use crate::Error;
pub const LTSH_TABLE_TAG: u32 = 0x4C54_5348;
pub const LTSH_ALWAYS_LINEAR: u8 = 1;
pub const LTSH_VERSION_0: u16 = 0;
#[derive(Debug, Clone)]
pub struct LtshTable {
version: u16,
y_pels: Vec<u8>,
}
impl LtshTable {
pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
Self::parse_inner(bytes, None)
}
pub fn parse_with_glyph_count(bytes: &[u8], expected_num_glyphs: u16) -> Result<Self, Error> {
Self::parse_inner(bytes, Some(expected_num_glyphs))
}
fn parse_inner(bytes: &[u8], expected: Option<u16>) -> Result<Self, Error> {
if bytes.len() < 4 {
return Err(Error::UnexpectedEof);
}
let version = read_u16(bytes, 0)?;
if version != LTSH_VERSION_0 {
return Err(Error::BadStructure("LTSH: unrecognised version"));
}
let num_glyphs = read_u16(bytes, 2)?;
if let Some(exp) = expected {
if num_glyphs != exp {
return Err(Error::BadStructure(
"LTSH: numGlyphs disagrees with maxp.numGlyphs",
));
}
}
let body_off = 4usize;
let body_end = body_off
.checked_add(num_glyphs as usize)
.ok_or(Error::BadStructure("LTSH: numGlyphs overflow"))?;
if bytes.len() < body_end {
return Err(Error::UnexpectedEof);
}
let mut y_pels = Vec::with_capacity(num_glyphs as usize);
for i in 0..num_glyphs as usize {
y_pels.push(read_u8(bytes, body_off + i)?);
}
Ok(Self { version, y_pels })
}
pub fn version_raw(&self) -> u16 {
self.version
}
pub fn num_glyphs(&self) -> u16 {
self.y_pels.len() as u16
}
pub fn y_pels(&self) -> &[u8] {
&self.y_pels
}
pub fn linear_threshold(&self, glyph_id: u16) -> Option<u8> {
self.y_pels.get(glyph_id as usize).copied()
}
pub fn is_always_linear(&self, glyph_id: u16) -> bool {
self.linear_threshold(glyph_id) == Some(LTSH_ALWAYS_LINEAR)
}
pub fn linearly_scales_at_ppem(&self, glyph_id: u16, ppem: u16) -> bool {
match self.linear_threshold(glyph_id) {
Some(threshold) => ppem >= threshold as u16,
None => false,
}
}
pub fn all_always_linear(&self) -> bool {
!self.y_pels.is_empty() && self.y_pels.iter().all(|&y| y == LTSH_ALWAYS_LINEAR)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_ltsh(version: u16, y_pels: &[u8]) -> Vec<u8> {
let mut b = Vec::with_capacity(4 + y_pels.len());
b.extend_from_slice(&version.to_be_bytes());
b.extend_from_slice(&(y_pels.len() as u16).to_be_bytes());
b.extend_from_slice(y_pels);
b
}
#[test]
fn parses_minimal_three_glyph_table() {
let bytes = make_ltsh(LTSH_VERSION_0, &[1, 1, 12]);
let t = LtshTable::parse(&bytes).expect("parse");
assert_eq!(t.version_raw(), 0);
assert_eq!(t.num_glyphs(), 3);
assert_eq!(t.y_pels(), &[1, 1, 12]);
assert_eq!(t.linear_threshold(0), Some(1));
assert_eq!(t.linear_threshold(2), Some(12));
assert_eq!(t.linear_threshold(3), None);
}
#[test]
fn rejects_short_header() {
let bytes = vec![0u8; 3];
assert!(matches!(
LtshTable::parse(&bytes),
Err(Error::UnexpectedEof)
));
}
#[test]
fn rejects_truncated_body() {
let mut bytes = Vec::with_capacity(6);
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&5u16.to_be_bytes());
bytes.extend_from_slice(&[1u8, 1u8]);
assert!(matches!(
LtshTable::parse(&bytes),
Err(Error::UnexpectedEof)
));
}
#[test]
fn rejects_unknown_version() {
let bytes = make_ltsh(2, &[1, 1, 1]);
assert!(matches!(
LtshTable::parse(&bytes),
Err(Error::BadStructure(_))
));
}
#[test]
fn rejects_maxp_glyph_count_mismatch() {
let bytes = make_ltsh(LTSH_VERSION_0, &[1, 1, 1]);
assert!(matches!(
LtshTable::parse_with_glyph_count(&bytes, 4),
Err(Error::BadStructure(_))
));
let t = LtshTable::parse_with_glyph_count(&bytes, 3).expect("parse");
assert_eq!(t.num_glyphs(), 3);
}
#[test]
fn always_linear_sentinel_detected() {
let bytes = make_ltsh(LTSH_VERSION_0, &[1, 1, 1, 1]);
let t = LtshTable::parse(&bytes).expect("parse");
assert!(t.all_always_linear());
for gid in 0..4 {
assert!(t.is_always_linear(gid));
}
assert!(!t.is_always_linear(4));
}
#[test]
fn mixed_threshold_table_not_all_linear() {
let bytes = make_ltsh(LTSH_VERSION_0, &[1, 1, 8, 1]);
let t = LtshTable::parse(&bytes).expect("parse");
assert!(!t.all_always_linear());
assert!(t.is_always_linear(0));
assert!(!t.is_always_linear(2));
assert_eq!(t.linear_threshold(2), Some(8));
}
#[test]
fn linearly_scales_at_ppem_threshold() {
let bytes = make_ltsh(LTSH_VERSION_0, &[1, 1, 24, 50]);
let t = LtshTable::parse(&bytes).expect("parse");
assert!(t.linearly_scales_at_ppem(0, 8));
assert!(!t.linearly_scales_at_ppem(2, 23));
assert!(t.linearly_scales_at_ppem(2, 24));
assert!(t.linearly_scales_at_ppem(2, 25));
assert!(!t.linearly_scales_at_ppem(3, 49));
assert!(t.linearly_scales_at_ppem(3, 50));
assert!(!t.linearly_scales_at_ppem(4, 100));
}
#[test]
fn empty_table_round_trips_through_parser() {
let bytes = make_ltsh(LTSH_VERSION_0, &[]);
let t = LtshTable::parse(&bytes).expect("parse");
assert_eq!(t.num_glyphs(), 0);
assert!(t.y_pels().is_empty());
assert!(!t.all_always_linear());
}
#[test]
fn parser_accepts_trailing_pad_bytes() {
let mut bytes = make_ltsh(LTSH_VERSION_0, &[1, 1, 8]);
bytes.push(0u8);
let t = LtshTable::parse(&bytes).expect("parse");
assert_eq!(t.num_glyphs(), 3);
assert_eq!(t.y_pels(), &[1, 1, 8]);
}
#[test]
fn tag_bytes_match_constant() {
assert_eq!(
u32::from_be_bytes(*b"LTSH"),
LTSH_TABLE_TAG,
"LTSH_TABLE_TAG = 0x{:08X}",
LTSH_TABLE_TAG
);
}
}