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}