use crate::parser::{read_i8, read_u16, read_u32, read_u8};
use crate::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CblcEntry {
pub image_format: u16,
pub image_data_offset: u32,
pub data_len: u32,
pub ppem_x: u8,
pub ppem_y: u8,
pub bit_depth: u8,
pub fixed_metrics: Option<BigGlyphMetrics>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BigGlyphMetrics {
pub height: u8,
pub width: u8,
pub hori_bearing_x: i8,
pub hori_bearing_y: i8,
pub hori_advance: u8,
pub vert_bearing_x: i8,
pub vert_bearing_y: i8,
pub vert_advance: u8,
}
impl BigGlyphMetrics {
pub(crate) fn parse(bytes: &[u8], off: usize) -> Result<Self, Error> {
Ok(Self {
height: read_u8(bytes, off)?,
width: read_u8(bytes, off + 1)?,
hori_bearing_x: read_i8(bytes, off + 2)?,
hori_bearing_y: read_i8(bytes, off + 3)?,
hori_advance: read_u8(bytes, off + 4)?,
vert_bearing_x: read_i8(bytes, off + 5)?,
vert_bearing_y: read_i8(bytes, off + 6)?,
vert_advance: read_u8(bytes, off + 7)?,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SmallGlyphMetrics {
pub height: u8,
pub width: u8,
pub bearing_x: i8,
pub bearing_y: i8,
pub advance: u8,
}
impl SmallGlyphMetrics {
pub fn parse(bytes: &[u8], off: usize) -> Result<Self, Error> {
Ok(Self {
height: read_u8(bytes, off)?,
width: read_u8(bytes, off + 1)?,
bearing_x: read_i8(bytes, off + 2)?,
bearing_y: read_i8(bytes, off + 3)?,
advance: read_u8(bytes, off + 4)?,
})
}
}
#[derive(Debug, Clone, Copy)]
struct StrikeRecord {
index_subtable_list_offset: u32,
#[allow(dead_code)]
index_subtable_list_size: u32,
number_of_index_subtables: u32,
start_glyph_index: u16,
end_glyph_index: u16,
ppem_x: u8,
ppem_y: u8,
bit_depth: u8,
}
const BITMAP_SIZE_RECORD_LEN: usize = 4 + 4 + 4 + 4 + 12 + 12 + 2 + 2 + 1 + 1 + 1 + 1; const SBIT_LINE_METRICS_LEN: usize = 12;
#[derive(Debug, Clone)]
pub struct CblcTable<'a> {
bytes: &'a [u8],
strikes: Vec<StrikeRecord>,
}
impl<'a> CblcTable<'a> {
pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
if bytes.len() < 8 {
return Err(Error::UnexpectedEof);
}
let major = read_u16(bytes, 0)?;
let _minor = read_u16(bytes, 2)?;
if major != 2 && major != 3 {
return Err(Error::BadStructure("CBLC: unknown major version"));
}
let num_sizes = read_u32(bytes, 4)?;
if num_sizes == 0 || num_sizes > 256 {
return Err(Error::BadStructure("CBLC: implausible numSizes"));
}
let needed = 8usize
.checked_add(num_sizes as usize * BITMAP_SIZE_RECORD_LEN)
.ok_or(Error::BadStructure("CBLC: numSizes overflow"))?;
if bytes.len() < needed {
return Err(Error::UnexpectedEof);
}
let mut strikes = Vec::with_capacity(num_sizes as usize);
for i in 0..num_sizes as usize {
let base = 8 + i * BITMAP_SIZE_RECORD_LEN;
let index_subtable_list_offset = read_u32(bytes, base)?;
let index_subtable_list_size = read_u32(bytes, base + 4)?;
let number_of_index_subtables = read_u32(bytes, base + 8)?;
let after_metrics = base + 16 + 2 * SBIT_LINE_METRICS_LEN;
let start_glyph_index = read_u16(bytes, after_metrics)?;
let end_glyph_index = read_u16(bytes, after_metrics + 2)?;
let ppem_x = read_u8(bytes, after_metrics + 4)?;
let ppem_y = read_u8(bytes, after_metrics + 5)?;
let bit_depth = read_u8(bytes, after_metrics + 6)?;
if (index_subtable_list_offset as u64)
.checked_add(index_subtable_list_size as u64)
.map(|end| end > bytes.len() as u64)
.unwrap_or(true)
{
return Err(Error::BadOffset);
}
strikes.push(StrikeRecord {
index_subtable_list_offset,
index_subtable_list_size,
number_of_index_subtables,
start_glyph_index,
end_glyph_index,
ppem_x,
ppem_y,
bit_depth,
});
}
Ok(Self { bytes, strikes })
}
pub fn ppem_sizes(&self) -> impl Iterator<Item = (u8, u8)> + '_ {
self.strikes.iter().map(|s| (s.ppem_x, s.ppem_y))
}
pub fn num_strikes(&self) -> usize {
self.strikes.len()
}
pub fn lookup_glyph(&self, glyph_id: u16, target_ppem: u8) -> Option<CblcEntry> {
let mut order: Vec<usize> = (0..self.strikes.len()).collect();
order.sort_by_key(|&i| {
let s = &self.strikes[i];
let diff = (s.ppem_y as i32 - target_ppem as i32).abs();
(diff, -(s.ppem_y as i32))
});
for i in order {
if let Some(entry) = self.lookup_in_strike(i, glyph_id).ok().flatten() {
return Some(entry);
}
}
None
}
fn lookup_in_strike(
&self,
strike_idx: usize,
glyph_id: u16,
) -> Result<Option<CblcEntry>, Error> {
let strike = &self.strikes[strike_idx];
if glyph_id < strike.start_glyph_index || glyph_id > strike.end_glyph_index {
return Ok(None);
}
let list_off = strike.index_subtable_list_offset as usize;
let n = strike.number_of_index_subtables as usize;
for i in 0..n {
let rec_off = list_off + i * 8;
let first = read_u16(self.bytes, rec_off)?;
let last = read_u16(self.bytes, rec_off + 2)?;
let sub_rel = read_u32(self.bytes, rec_off + 4)? as usize;
if glyph_id < first || glyph_id > last {
continue;
}
let sub_off = list_off + sub_rel;
return self
.resolve_in_subtable(strike, sub_off, first, last, glyph_id)
.map(Some);
}
Ok(None)
}
fn resolve_in_subtable(
&self,
strike: &StrikeRecord,
sub_off: usize,
first_glyph: u16,
_last_glyph: u16,
glyph_id: u16,
) -> Result<CblcEntry, Error> {
let index_format = read_u16(self.bytes, sub_off)?;
let image_format = read_u16(self.bytes, sub_off + 2)?;
let image_data_offset = read_u32(self.bytes, sub_off + 4)?;
let header_size = 8usize;
match index_format {
1 => {
let local = (glyph_id - first_glyph) as usize;
let off_a = read_u32(self.bytes, sub_off + header_size + local * 4)? as u64;
let off_b = read_u32(self.bytes, sub_off + header_size + (local + 1) * 4)? as u64;
let len = off_b
.checked_sub(off_a)
.ok_or(Error::BadStructure("CBLC: format 1 offsets reversed"))?;
Ok(CblcEntry {
image_format,
image_data_offset: image_data_offset
.checked_add(off_a as u32)
.ok_or(Error::BadStructure("CBLC: format 1 offset overflow"))?,
data_len: len as u32,
ppem_x: strike.ppem_x,
ppem_y: strike.ppem_y,
bit_depth: strike.bit_depth,
fixed_metrics: None,
})
}
2 => {
let image_size = read_u32(self.bytes, sub_off + header_size)?;
let metrics = BigGlyphMetrics::parse(self.bytes, sub_off + header_size + 4)?;
let local = (glyph_id - first_glyph) as u32;
Ok(CblcEntry {
image_format,
image_data_offset: image_data_offset
.checked_add(local.saturating_mul(image_size))
.ok_or(Error::BadStructure("CBLC: format 2 offset overflow"))?,
data_len: image_size,
ppem_x: strike.ppem_x,
ppem_y: strike.ppem_y,
bit_depth: strike.bit_depth,
fixed_metrics: Some(metrics),
})
}
3 => {
let local = (glyph_id - first_glyph) as usize;
let off_a = read_u16(self.bytes, sub_off + header_size + local * 2)? as u32;
let off_b = read_u16(self.bytes, sub_off + header_size + (local + 1) * 2)? as u32;
let len = off_b
.checked_sub(off_a)
.ok_or(Error::BadStructure("CBLC: format 3 offsets reversed"))?;
Ok(CblcEntry {
image_format,
image_data_offset: image_data_offset
.checked_add(off_a)
.ok_or(Error::BadStructure("CBLC: format 3 offset overflow"))?,
data_len: len,
ppem_x: strike.ppem_x,
ppem_y: strike.ppem_y,
bit_depth: strike.bit_depth,
fixed_metrics: None,
})
}
4 => {
let num_glyphs = read_u32(self.bytes, sub_off + header_size)?;
let pairs_off = sub_off + header_size + 4;
let mut hit: Option<(u32, u32)> = None;
let mut next_off: Option<u32> = None;
for i in 0..num_glyphs as usize {
let pair_off = pairs_off + i * 4;
let gid = read_u16(self.bytes, pair_off)?;
let off = read_u16(self.bytes, pair_off + 2)? as u32;
if gid == glyph_id {
let next_pair = pairs_off + (i + 1) * 4;
let n_off = read_u16(self.bytes, next_pair + 2)? as u32;
hit = Some((off, n_off));
next_off = Some(n_off);
break;
}
}
let (off_a, off_b) = match (hit, next_off) {
(Some((a, b)), _) => (a, b),
_ => {
return Err(Error::BadStructure(
"CBLC: format 4 lookup missed (range covered but glyph absent)",
))
}
};
let len = off_b
.checked_sub(off_a)
.ok_or(Error::BadStructure("CBLC: format 4 offsets reversed"))?;
Ok(CblcEntry {
image_format,
image_data_offset: image_data_offset
.checked_add(off_a)
.ok_or(Error::BadStructure("CBLC: format 4 offset overflow"))?,
data_len: len,
ppem_x: strike.ppem_x,
ppem_y: strike.ppem_y,
bit_depth: strike.bit_depth,
fixed_metrics: None,
})
}
5 => {
let image_size = read_u32(self.bytes, sub_off + header_size)?;
let metrics = BigGlyphMetrics::parse(self.bytes, sub_off + header_size + 4)?;
let num_glyphs = read_u32(self.bytes, sub_off + header_size + 12)?;
let arr_off = sub_off + header_size + 16;
for i in 0..num_glyphs as usize {
let gid = read_u16(self.bytes, arr_off + i * 2)?;
if gid == glyph_id {
let local = i as u32;
return Ok(CblcEntry {
image_format,
image_data_offset: image_data_offset
.checked_add(local.saturating_mul(image_size))
.ok_or(Error::BadStructure("CBLC: format 5 offset overflow"))?,
data_len: image_size,
ppem_x: strike.ppem_x,
ppem_y: strike.ppem_y,
bit_depth: strike.bit_depth,
fixed_metrics: Some(metrics),
});
}
}
Err(Error::BadStructure(
"CBLC: format 5 lookup missed (range covered but glyph absent)",
))
}
_ => Err(Error::BadStructure("CBLC: unsupported indexFormat")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_short_header() {
assert!(matches!(
CblcTable::parse(&[0u8; 4]),
Err(Error::UnexpectedEof)
));
}
#[test]
fn rejects_unknown_major_version() {
let mut bytes = vec![0u8; 8 + BITMAP_SIZE_RECORD_LEN];
bytes[0..2].copy_from_slice(&7u16.to_be_bytes()); bytes[4..8].copy_from_slice(&1u32.to_be_bytes()); assert!(matches!(
CblcTable::parse(&bytes),
Err(Error::BadStructure(_))
));
}
#[test]
fn parses_minimal_cblc_with_format2_strike() {
let strike_off = 56u32;
let mut bytes = vec![0u8; 256];
bytes[0..2].copy_from_slice(&3u16.to_be_bytes()); bytes[2..4].copy_from_slice(&0u16.to_be_bytes()); bytes[4..8].copy_from_slice(&1u32.to_be_bytes()); let base = 8;
bytes[base..base + 4].copy_from_slice(&strike_off.to_be_bytes()); bytes[base + 4..base + 8].copy_from_slice(&((8 + 8 + 16) as u32).to_be_bytes()); bytes[base + 8..base + 12].copy_from_slice(&1u32.to_be_bytes()); bytes[base + 12..base + 16].copy_from_slice(&0u32.to_be_bytes()); let after = base + 16 + 24;
bytes[after..after + 2].copy_from_slice(&5u16.to_be_bytes()); bytes[after + 2..after + 4].copy_from_slice(&7u16.to_be_bytes()); bytes[after + 4] = 96; bytes[after + 5] = 96; bytes[after + 6] = 32; bytes[after + 7] = 0x01;
let list_off = strike_off as usize;
bytes[list_off..list_off + 2].copy_from_slice(&5u16.to_be_bytes()); bytes[list_off + 2..list_off + 4].copy_from_slice(&7u16.to_be_bytes()); bytes[list_off + 4..list_off + 8].copy_from_slice(&8u32.to_be_bytes());
let sub_off = list_off + 8;
bytes[sub_off..sub_off + 2].copy_from_slice(&2u16.to_be_bytes()); bytes[sub_off + 2..sub_off + 4].copy_from_slice(&18u16.to_be_bytes()); bytes[sub_off + 4..sub_off + 8].copy_from_slice(&200u32.to_be_bytes()); bytes[sub_off + 8..sub_off + 12].copy_from_slice(&120u32.to_be_bytes()); for i in 0..8u8 {
bytes[sub_off + 12 + i as usize] = i;
}
let cblc = CblcTable::parse(&bytes).expect("parse");
assert_eq!(cblc.num_strikes(), 1);
let entry = cblc.lookup_glyph(5, 96).expect("entry");
assert_eq!(entry.image_format, 18);
assert_eq!(entry.image_data_offset, 200);
assert_eq!(entry.data_len, 120);
assert_eq!(entry.ppem_x, 96);
assert_eq!(entry.ppem_y, 96);
let m = entry.fixed_metrics.expect("constant metrics");
assert_eq!(m.height, 0);
assert_eq!(m.width, 1);
let entry6 = cblc.lookup_glyph(6, 96).expect("entry6");
assert_eq!(entry6.image_data_offset, 200 + 120);
assert_eq!(entry6.data_len, 120);
assert!(cblc.lookup_glyph(8, 96).is_none());
}
}