Skip to main content

oxideav_ttf/tables/
ebsc.rs

1//! `EBSC` — Embedded Bitmap Scaling table (ISO/IEC 14496-22:2019 §5.6.4).
2//!
3//! `EBSC` lets a font declare a bitmap strike that does not exist as real
4//! pixel data, but is instead produced by *scaling* a strike that DOES
5//! exist in `EBLC`/`EBDT`. The spec motivates this with small Kanji sizes,
6//! where scaling an authored bitmap reads better than scan-converting an
7//! outline at the same ppem. It carries no glyph imagery itself; it is a
8//! redirection layer on top of the embedded-bitmap pair.
9//!
10//! The on-wire layout we walk:
11//!
12//! ```text
13//! EbscHeader {
14//!     u16 majorVersion;   // = 2
15//!     u16 minorVersion;   // = 0
16//!     u32 numSizes;
17//!     BitmapScale bitmapScales[numSizes];
18//! }
19//! BitmapScale {
20//!     SbitLineMetrics hori;          // 12 bytes
21//!     SbitLineMetrics vert;          // 12 bytes
22//!     u8  ppemX;                     // target horizontal ppem
23//!     u8  ppemY;                     // target vertical ppem
24//!     u8  substitutePpemX;           // source (existing) horizontal ppem
25//!     u8  substitutePpemY;           // source (existing) vertical ppem
26//! }
27//! SbitLineMetrics {                  // §5.6.3.2, shared with EBLC
28//!     i8 ascender;
29//!     i8 descender;
30//!     u8 widthMax;
31//!     i8 caretSlopeNumerator;
32//!     i8 caretSlopeDenominator;
33//!     i8 caretOffset;
34//!     i8 minOriginSB;
35//!     i8 minAdvanceSB;
36//!     i8 maxBeforeBL;
37//!     i8 minAfterBL;
38//!     i8 pad1;
39//!     i8 pad2;
40//! }
41//! ```
42//!
43//! Per §5.6.4 each `BitmapScale` describes the strike *after* scaling:
44//! the `ppemX`/`ppemY` give the synthesised size and the line metrics
45//! refer to that scaled, font-wide geometry. `substitutePpemX`/
46//! `substitutePpemY` name the real strike (an sbit in `EBLC`/`EBDT`) to
47//! scale up or down. The spec notes the x and y scale factors are
48//! independent — a square strike may be redirected to a non-square one —
49//! and that "Glyph metrics are scaled by the same factor as the pixels
50//! per Em (in the appropriate direction), and are rounded to the nearest
51//! integer pixel."
52
53use crate::parser::{read_i8, read_u16, read_u32, read_u8};
54use crate::Error;
55
56/// Major version of an `EBSC` table per §5.6.4.
57pub const EBSC_MAJOR_VERSION: u16 = 2;
58/// Minor version of an `EBSC` table per §5.6.4.
59pub const EBSC_MINOR_VERSION: u16 = 0;
60
61const SBIT_LINE_METRICS_LEN: usize = 12;
62/// 12 (hori) + 12 (vert) + ppemX + ppemY + substitutePpemX + substitutePpemY.
63const BITMAP_SCALE_LEN: usize = SBIT_LINE_METRICS_LEN * 2 + 4; // 28
64
65/// Per-strike font-wide line metrics (§5.6.3.2). Twelve signed/unsigned
66/// bytes shared between `EBLC`'s `BitmapSize` and `EBSC`'s `BitmapScale`.
67/// "The line metrics are not used directly by the rasterizer, but are
68/// available to clients who want to parse the table."
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
70pub struct SbitLineMetrics {
71    pub ascender: i8,
72    pub descender: i8,
73    pub width_max: u8,
74    pub caret_slope_numerator: i8,
75    pub caret_slope_denominator: i8,
76    pub caret_offset: i8,
77    pub min_origin_sb: i8,
78    pub min_advance_sb: i8,
79    pub max_before_bl: i8,
80    pub min_after_bl: i8,
81    pub pad1: i8,
82    pub pad2: i8,
83}
84
85impl SbitLineMetrics {
86    fn parse(bytes: &[u8], off: usize) -> Result<Self, Error> {
87        Ok(Self {
88            ascender: read_i8(bytes, off)?,
89            descender: read_i8(bytes, off + 1)?,
90            width_max: read_u8(bytes, off + 2)?,
91            caret_slope_numerator: read_i8(bytes, off + 3)?,
92            caret_slope_denominator: read_i8(bytes, off + 4)?,
93            caret_offset: read_i8(bytes, off + 5)?,
94            min_origin_sb: read_i8(bytes, off + 6)?,
95            min_advance_sb: read_i8(bytes, off + 7)?,
96            max_before_bl: read_i8(bytes, off + 8)?,
97            min_after_bl: read_i8(bytes, off + 9)?,
98            pad1: read_i8(bytes, off + 10)?,
99            pad2: read_i8(bytes, off + 11)?,
100        })
101    }
102}
103
104/// One `BitmapScale` record (§5.6.4) — a single synthesised strike defined
105/// as a scaled copy of an existing `EBLC`/`EBDT` strike.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct BitmapScale {
108    /// Font-wide horizontal line metrics for the scaled strike.
109    pub hori: SbitLineMetrics,
110    /// Font-wide vertical line metrics for the scaled strike.
111    pub vert: SbitLineMetrics,
112    /// Target (synthesised) horizontal pixels-per-em.
113    pub ppem_x: u8,
114    /// Target (synthesised) vertical pixels-per-em.
115    pub ppem_y: u8,
116    /// Horizontal ppem of the real strike to scale from.
117    pub substitute_ppem_x: u8,
118    /// Vertical ppem of the real strike to scale from.
119    pub substitute_ppem_y: u8,
120}
121
122impl BitmapScale {
123    fn parse(bytes: &[u8], off: usize) -> Result<Self, Error> {
124        let hori = SbitLineMetrics::parse(bytes, off)?;
125        let vert = SbitLineMetrics::parse(bytes, off + SBIT_LINE_METRICS_LEN)?;
126        let base = off + 2 * SBIT_LINE_METRICS_LEN;
127        Ok(Self {
128            hori,
129            vert,
130            ppem_x: read_u8(bytes, base)?,
131            ppem_y: read_u8(bytes, base + 1)?,
132            substitute_ppem_x: read_u8(bytes, base + 2)?,
133            substitute_ppem_y: read_u8(bytes, base + 3)?,
134        })
135    }
136}
137
138/// Parsed `EBSC` table — the version header plus the array of
139/// `BitmapScale` redirection records.
140#[derive(Debug, Clone)]
141// internal — exposed for tests/fuzz; not part of the stable API
142#[doc(hidden)]
143pub struct EbscTable {
144    minor_version: u16,
145    scales: Vec<BitmapScale>,
146}
147
148impl EbscTable {
149    /// Parse the `EBSC` header + `BitmapScale` array. Rejects a
150    /// non-`2.x` major version per §5.6.4 ("Major version of the EBSC
151    /// table, = 2"); the minor version is surfaced rather than fixed so a
152    /// future `2.x` revision still decodes. `numSizes` is capped to bound
153    /// allocation against a truncated / malformed input.
154    pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
155        if bytes.len() < 8 {
156            return Err(Error::UnexpectedEof);
157        }
158        let major = read_u16(bytes, 0)?;
159        if major != EBSC_MAJOR_VERSION {
160            return Err(Error::BadStructure("EBSC: unknown major version"));
161        }
162        let minor_version = read_u16(bytes, 2)?;
163        let num_sizes = read_u32(bytes, 4)?;
164        // Real fonts carry only a handful of scaled strikes; the cap
165        // mirrors the EBLC/CBLC walker's defence against bogus counts.
166        if num_sizes > 256 {
167            return Err(Error::BadStructure("EBSC: implausible numSizes"));
168        }
169        let needed = 8usize
170            .checked_add(num_sizes as usize * BITMAP_SCALE_LEN)
171            .ok_or(Error::BadStructure("EBSC: numSizes overflow"))?;
172        if bytes.len() < needed {
173            return Err(Error::UnexpectedEof);
174        }
175        let mut scales = Vec::with_capacity(num_sizes as usize);
176        for i in 0..num_sizes as usize {
177            scales.push(BitmapScale::parse(bytes, 8 + i * BITMAP_SCALE_LEN)?);
178        }
179        Ok(Self {
180            minor_version,
181            scales,
182        })
183    }
184
185    /// Minor version from the header (`0` for the current revision).
186    pub fn minor_version(&self) -> u16 {
187        self.minor_version
188    }
189
190    /// Every `BitmapScale` record in declaration order.
191    pub fn scales(&self) -> &[BitmapScale] {
192        &self.scales
193    }
194
195    /// Number of synthesised (scaled) strikes the table declares.
196    pub fn num_scales(&self) -> usize {
197        self.scales.len()
198    }
199
200    /// All target `(ppemX, ppemY)` sizes this table synthesises, in
201    /// declaration order. These are the sizes a client could request and
202    /// have satisfied by scaling, *without* a real strike existing at
203    /// that ppem.
204    pub fn target_ppem_sizes(&self) -> impl Iterator<Item = (u8, u8)> + '_ {
205        self.scales.iter().map(|s| (s.ppem_x, s.ppem_y))
206    }
207
208    /// The `BitmapScale` whose target `ppemY` is `target_ppem`, if any.
209    /// Used to discover whether a requested rasterisation size is served
210    /// by a scaled strike, and which real strike (`substitute_ppem_y`) to
211    /// pull bitmaps from. When several records share a target ppemY the
212    /// first in declaration order wins.
213    pub fn scale_for_target_ppem(&self, target_ppem: u8) -> Option<&BitmapScale> {
214        self.scales.iter().find(|s| s.ppem_y == target_ppem)
215    }
216}
217
218/// Scale a metric value (advance, bearing, width, …) by the
219/// `target / substitute` ppem ratio, rounding to the nearest integer
220/// pixel per §5.6.4 ("Glyph metrics are scaled by the same factor as the
221/// pixels per Em … and are rounded to the nearest integer pixel"). The
222/// arithmetic is done in `i32` so an `i8` bearing scaled by a large
223/// factor cannot overflow mid-computation; callers clamp back into range.
224pub(crate) fn scale_metric(value: i32, target_ppem: u8, substitute_ppem: u8) -> i32 {
225    if substitute_ppem == 0 {
226        return value;
227    }
228    let num = value * target_ppem as i32;
229    let den = substitute_ppem as i32;
230    // Round half away from zero (nearest integer) so a 1.5-pixel metric
231    // lands on 2 and a -1.5-pixel one on -2, symmetric about the origin.
232    if num >= 0 {
233        (num + den / 2) / den
234    } else {
235        -((-num + den / 2) / den)
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    /// Build a 12-byte SbitLineMetrics blob from ascender/descender, the
244    /// rest zeroed.
245    fn line_metrics(asc: i8, desc: i8) -> [u8; 12] {
246        let mut b = [0u8; 12];
247        b[0] = asc as u8;
248        b[1] = desc as u8;
249        b
250    }
251
252    fn build_ebsc(scales: &[(u8, u8, u8, u8)]) -> Vec<u8> {
253        let mut v = Vec::new();
254        v.extend_from_slice(&EBSC_MAJOR_VERSION.to_be_bytes());
255        v.extend_from_slice(&EBSC_MINOR_VERSION.to_be_bytes());
256        v.extend_from_slice(&(scales.len() as u32).to_be_bytes());
257        for &(ppx, ppy, spx, spy) in scales {
258            v.extend_from_slice(&line_metrics(ppy as i8, -(ppy as i8) / 4));
259            v.extend_from_slice(&line_metrics(ppx as i8, -(ppx as i8) / 4));
260            v.push(ppx);
261            v.push(ppy);
262            v.push(spx);
263            v.push(spy);
264        }
265        v
266    }
267
268    #[test]
269    fn parses_header_and_records() {
270        let bytes = build_ebsc(&[(20, 20, 16, 16), (24, 24, 16, 16)]);
271        let t = EbscTable::parse(&bytes).unwrap();
272        assert_eq!(t.minor_version(), 0);
273        assert_eq!(t.num_scales(), 2);
274        let sizes: Vec<_> = t.target_ppem_sizes().collect();
275        assert_eq!(sizes, vec![(20, 20), (24, 24)]);
276        let s0 = &t.scales()[0];
277        assert_eq!(s0.ppem_x, 20);
278        assert_eq!(s0.substitute_ppem_x, 16);
279        assert_eq!(s0.hori.ascender, 20);
280    }
281
282    #[test]
283    fn line_metrics_round_trip() {
284        let bytes = build_ebsc(&[(20, 20, 16, 16)]);
285        let t = EbscTable::parse(&bytes).unwrap();
286        let s = &t.scales()[0];
287        // hori built from ppemY=20, vert from ppemX=20.
288        assert_eq!(s.hori.ascender, 20);
289        assert_eq!(s.hori.descender, -5);
290        assert_eq!(s.vert.ascender, 20);
291    }
292
293    #[test]
294    fn lookup_by_target_ppem() {
295        let bytes = build_ebsc(&[(20, 20, 16, 16), (24, 24, 16, 16)]);
296        let t = EbscTable::parse(&bytes).unwrap();
297        let s = t.scale_for_target_ppem(24).unwrap();
298        assert_eq!(s.substitute_ppem_y, 16);
299        assert!(t.scale_for_target_ppem(99).is_none());
300    }
301
302    #[test]
303    fn rejects_wrong_major_version() {
304        let mut bytes = build_ebsc(&[(20, 20, 16, 16)]);
305        bytes[1] = 3; // major = 3
306        assert!(matches!(
307            EbscTable::parse(&bytes),
308            Err(Error::BadStructure(_))
309        ));
310    }
311
312    #[test]
313    fn rejects_truncated_record_array() {
314        let mut bytes = build_ebsc(&[(20, 20, 16, 16)]);
315        bytes.truncate(bytes.len() - 4); // chop a record's tail
316        assert!(matches!(
317            EbscTable::parse(&bytes),
318            Err(Error::UnexpectedEof)
319        ));
320    }
321
322    #[test]
323    fn rejects_implausible_num_sizes() {
324        let mut bytes = vec![0, 2, 0, 0]; // major=2, minor=0
325        bytes.extend_from_slice(&1000u32.to_be_bytes());
326        assert!(matches!(
327            EbscTable::parse(&bytes),
328            Err(Error::BadStructure(_))
329        ));
330    }
331
332    #[test]
333    fn zero_scales_is_valid() {
334        let bytes = build_ebsc(&[]);
335        let t = EbscTable::parse(&bytes).unwrap();
336        assert_eq!(t.num_scales(), 0);
337        assert!(t.scale_for_target_ppem(20).is_none());
338    }
339
340    #[test]
341    fn scale_metric_nearest_integer() {
342        // 16-px advance scaled 20/16 = 20 exactly.
343        assert_eq!(scale_metric(16, 20, 16), 20);
344        // 10 * 24 / 16 = 15 exactly.
345        assert_eq!(scale_metric(10, 24, 16), 15);
346        // 7 * 20 / 16 = 8.75 -> 9 (round half away handled by +den/2).
347        assert_eq!(scale_metric(7, 20, 16), 9);
348        // Down-scale: 20 * 16 / 20 = 16.
349        assert_eq!(scale_metric(20, 16, 20), 16);
350        // Negative bearing scales symmetrically: -7 * 20 / 16 -> -9.
351        assert_eq!(scale_metric(-7, 20, 16), -9);
352        // Substitute ppem 0 is a guard, returns value unchanged.
353        assert_eq!(scale_metric(5, 20, 0), 5);
354    }
355}