Skip to main content

oxideav_ttf/tables/
cbdt.rs

1//! `CBDT` — Color Bitmap Data Table.
2//!
3//! The CBDT table holds the actual per-glyph image data; the `CBLC`
4//! sibling table maps `(glyph_id, ppem)` → byte range here. Per
5//! Microsoft OpenType spec, the table starts with a 4-byte header
6//! (`majorVersion = 3`, `minorVersion = 0`) followed by an opaque blob
7//! of glyph entries whose layout depends on the per-strike `imageFormat`
8//! recorded in CBLC.
9//!
10//! We support the three PNG-based entry formats Noto Color Emoji and
11//! every other Google "embedded PNG" colour-emoji font use:
12//!
13//! ```text
14//! Format 17:  SmallGlyphMetrics (5 B) + u32 dataLen + PNG[dataLen]
15//! Format 18:  BigGlyphMetrics   (8 B) + u32 dataLen + PNG[dataLen]
16//! Format 19:  u32 dataLen + PNG[dataLen]
17//!             (metrics live in CBLC IndexSubtable's BigGlyphMetrics)
18//! ```
19//!
20//! Format 19's metrics are recovered via `CblcEntry::fixed_metrics`
21//! which the CBLC walker populates from the IndexSubtable Format 2 / 5
22//! BigGlyphMetrics field.
23//!
24//! Other CBDT formats (1/2/5/6/7/8/9 inherited from EBDT — monochrome
25//! / grayscale; 32 BGRA uncompressed) are not supported in this round.
26//! They're rare in the wild compared to PNG and the consumer crate
27//! doesn't have a native BGRA path yet.
28
29use crate::parser::read_u32;
30use crate::tables::cblc::{BigGlyphMetrics, CblcEntry, SmallGlyphMetrics};
31use crate::Error;
32
33/// One glyph's worth of data resolved out of CBDT, ready for PNG decode.
34#[derive(Debug, Clone, Copy)]
35pub struct ColorBitmap<'a> {
36    /// Width of the bitmap in pixels (from the per-glyph metrics).
37    pub width: u8,
38    /// Height in pixels.
39    pub height: u8,
40    /// Distance from the horizontal pen origin to the LEFT edge of the
41    /// bitmap, in pixels.
42    pub bearing_x: i8,
43    /// Distance from the horizontal pen origin to the TOP edge of the
44    /// bitmap, in pixels.
45    pub bearing_y: i8,
46    /// Horizontal advance in pixels.
47    pub advance: u8,
48    /// Strike pixels-per-em (the size at which this glyph was authored).
49    pub ppem: u8,
50    /// Raw PNG byte stream — pass to `oxideav_png::decode_png_to_frame`
51    /// in the consumer crate. Borrowed from the parent CBDT slice.
52    pub png_bytes: &'a [u8],
53}
54
55/// Parsed CBDT table.
56#[derive(Debug, Clone)]
57// internal — exposed for tests/fuzz; not part of the stable API
58#[doc(hidden)]
59pub struct CbdtTable<'a> {
60    bytes: &'a [u8],
61}
62
63impl<'a> CbdtTable<'a> {
64    /// Wrap a CBDT byte slice. Validates the header version only — per-
65    /// glyph entry layout is parsed lazily via `lookup`.
66    pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
67        if bytes.len() < 4 {
68            return Err(Error::UnexpectedEof);
69        }
70        let major = u16::from_be_bytes([bytes[0], bytes[1]]);
71        // CBDT = 3, EBDT = 2. We only emit `ColorBitmap` entries
72        // (Formats 17-19 are CBDT-only) but we accept either header
73        // since EBDT's bytes are layout-compatible for everything we
74        // touch (zero, in practice).
75        if major != 2 && major != 3 {
76            return Err(Error::BadStructure("CBDT: unknown major version"));
77        }
78        Ok(Self { bytes })
79    }
80
81    /// Decode a per-glyph entry given the CBLC-resolved descriptor.
82    ///
83    /// Returns `None` if `entry.image_format` is not one of 17/18/19
84    /// (we don't support uncompressed BGRA or monochrome formats yet).
85    /// Returns `Err(_)` only on structural damage (truncated PNG range).
86    pub fn lookup(&self, entry: &CblcEntry) -> Result<Option<ColorBitmap<'a>>, Error> {
87        let off = entry.image_data_offset as usize;
88        let end = off
89            .checked_add(entry.data_len as usize)
90            .ok_or(Error::BadStructure("CBDT: entry overflow"))?;
91        if end > self.bytes.len() {
92            return Err(Error::BadOffset);
93        }
94        let blob = &self.bytes[off..end];
95        match entry.image_format {
96            17 => {
97                // SmallGlyphMetrics (5) + u32 dataLen + PNG.
98                let metrics = SmallGlyphMetrics::parse(blob, 0)?;
99                if blob.len() < 5 + 4 {
100                    return Err(Error::UnexpectedEof);
101                }
102                let data_len = read_u32(blob, 5)? as usize;
103                let png = blob.get(9..9 + data_len).ok_or(Error::BadOffset)?;
104                Ok(Some(ColorBitmap {
105                    width: metrics.width,
106                    height: metrics.height,
107                    bearing_x: metrics.bearing_x,
108                    bearing_y: metrics.bearing_y,
109                    advance: metrics.advance,
110                    ppem: entry.ppem_y,
111                    png_bytes: png,
112                }))
113            }
114            18 => {
115                // BigGlyphMetrics (8) + u32 dataLen + PNG.
116                let metrics = BigGlyphMetrics::parse(blob, 0)?;
117                if blob.len() < 8 + 4 {
118                    return Err(Error::UnexpectedEof);
119                }
120                let data_len = read_u32(blob, 8)? as usize;
121                let png = blob.get(12..12 + data_len).ok_or(Error::BadOffset)?;
122                Ok(Some(ColorBitmap {
123                    width: metrics.width,
124                    height: metrics.height,
125                    bearing_x: metrics.hori_bearing_x,
126                    bearing_y: metrics.hori_bearing_y,
127                    advance: metrics.hori_advance,
128                    ppem: entry.ppem_y,
129                    png_bytes: png,
130                }))
131            }
132            19 => {
133                // u32 dataLen + PNG. Metrics come from CBLC.
134                let metrics = entry.fixed_metrics.ok_or(Error::BadStructure(
135                    "CBDT format 19 needs CBLC fixed metrics",
136                ))?;
137                if blob.len() < 4 {
138                    return Err(Error::UnexpectedEof);
139                }
140                let data_len = read_u32(blob, 0)? as usize;
141                let png = blob.get(4..4 + data_len).ok_or(Error::BadOffset)?;
142                Ok(Some(ColorBitmap {
143                    width: metrics.width,
144                    height: metrics.height,
145                    bearing_x: metrics.hori_bearing_x,
146                    bearing_y: metrics.hori_bearing_y,
147                    advance: metrics.hori_advance,
148                    ppem: entry.ppem_y,
149                    png_bytes: png,
150                }))
151            }
152            _ => Ok(None),
153        }
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn parses_format17_entry() {
163        // Build a CBDT byte slice with a single Format 17 entry at
164        // offset 16:
165        //   [00..04] header (major=3, minor=0)
166        //   [04..16] padding zeros
167        //   [16..21] SmallGlyphMetrics (h=10, w=12, bx=-1, by=8, adv=14)
168        //   [21..25] dataLen = 5
169        //   [25..30] PNG bytes (fake) [0x89, 'P', 'N', 'G', 0x0D]
170        let mut bytes = vec![0u8; 64];
171        bytes[0..2].copy_from_slice(&3u16.to_be_bytes());
172        bytes[16] = 10; // height
173        bytes[17] = 12; // width
174        bytes[18] = (-1i8) as u8; // bearingX
175        bytes[19] = 8; // bearingY
176        bytes[20] = 14; // advance
177        bytes[21..25].copy_from_slice(&5u32.to_be_bytes()); // dataLen
178        bytes[25..30].copy_from_slice(&[0x89, b'P', b'N', b'G', 0x0D]);
179        let cbdt = CbdtTable::parse(&bytes).expect("parse");
180        let entry = CblcEntry {
181            image_format: 17,
182            image_data_offset: 16,
183            data_len: 30 - 16,
184            ppem_x: 96,
185            ppem_y: 96,
186            bit_depth: 32,
187            fixed_metrics: None,
188        };
189        let cb = cbdt.lookup(&entry).expect("lookup ok").expect("entry");
190        assert_eq!(cb.width, 12);
191        assert_eq!(cb.height, 10);
192        assert_eq!(cb.bearing_x, -1);
193        assert_eq!(cb.bearing_y, 8);
194        assert_eq!(cb.advance, 14);
195        assert_eq!(cb.png_bytes.len(), 5);
196        assert_eq!(cb.png_bytes[0], 0x89);
197    }
198
199    #[test]
200    fn parses_format18_entry() {
201        // BigGlyphMetrics is 8 bytes: h, w, hbx, hby, hadv, vbx, vby, vadv.
202        let mut bytes = vec![0u8; 64];
203        bytes[0..2].copy_from_slice(&3u16.to_be_bytes());
204        // Entry at offset 12.
205        bytes[12] = 20; // height
206        bytes[13] = 24; // width
207        bytes[14] = 2; // hori_bearing_x
208        bytes[15] = 18; // hori_bearing_y
209        bytes[16] = 26; // hori_advance
210        bytes[17..20].copy_from_slice(&[0; 3]); // vert metrics ignored
211        bytes[20..24].copy_from_slice(&3u32.to_be_bytes()); // dataLen
212        bytes[24..27].copy_from_slice(&[0xA1, 0xB2, 0xC3]);
213        let cbdt = CbdtTable::parse(&bytes).expect("parse");
214        let entry = CblcEntry {
215            image_format: 18,
216            image_data_offset: 12,
217            data_len: 27 - 12,
218            ppem_x: 109,
219            ppem_y: 109,
220            bit_depth: 32,
221            fixed_metrics: None,
222        };
223        let cb = cbdt.lookup(&entry).expect("lookup ok").expect("entry");
224        assert_eq!(cb.width, 24);
225        assert_eq!(cb.height, 20);
226        assert_eq!(cb.bearing_x, 2);
227        assert_eq!(cb.bearing_y, 18);
228        assert_eq!(cb.advance, 26);
229        assert_eq!(cb.ppem, 109);
230        assert_eq!(cb.png_bytes, &[0xA1, 0xB2, 0xC3]);
231    }
232
233    #[test]
234    fn returns_none_for_unsupported_format() {
235        let mut bytes = vec![0u8; 16];
236        bytes[0..2].copy_from_slice(&3u16.to_be_bytes());
237        let cbdt = CbdtTable::parse(&bytes).expect("parse");
238        let entry = CblcEntry {
239            image_format: 1, // monochrome — not supported
240            image_data_offset: 4,
241            data_len: 4,
242            ppem_x: 32,
243            ppem_y: 32,
244            bit_depth: 1,
245            fixed_metrics: None,
246        };
247        assert!(cbdt.lookup(&entry).expect("lookup ok").is_none());
248    }
249
250    #[test]
251    fn parses_format19_with_cblc_metrics() {
252        let mut bytes = vec![0u8; 32];
253        bytes[0..2].copy_from_slice(&3u16.to_be_bytes());
254        // Entry at offset 8: just dataLen + PNG.
255        bytes[8..12].copy_from_slice(&4u32.to_be_bytes());
256        bytes[12..16].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
257        let cbdt = CbdtTable::parse(&bytes).expect("parse");
258        let entry = CblcEntry {
259            image_format: 19,
260            image_data_offset: 8,
261            data_len: 16 - 8,
262            ppem_x: 64,
263            ppem_y: 64,
264            bit_depth: 32,
265            fixed_metrics: Some(BigGlyphMetrics {
266                height: 7,
267                width: 9,
268                hori_bearing_x: 3,
269                hori_bearing_y: 11,
270                hori_advance: 13,
271                vert_bearing_x: 0,
272                vert_bearing_y: 0,
273                vert_advance: 0,
274            }),
275        };
276        let cb = cbdt.lookup(&entry).expect("lookup ok").expect("entry");
277        assert_eq!(cb.width, 9);
278        assert_eq!(cb.height, 7);
279        assert_eq!(cb.bearing_x, 3);
280        assert_eq!(cb.bearing_y, 11);
281        assert_eq!(cb.advance, 13);
282        assert_eq!(cb.png_bytes, &[0xDE, 0xAD, 0xBE, 0xEF]);
283    }
284}