oxideav_ttf/tables/colr.rs
1//! `COLR` — Color Table (versions 0 and 1).
2//!
3//! The COLR table defines colour glyphs two ways:
4//!
5//! * **Version 0** maps a "base glyph" to an ordered stack of "layer
6//! glyphs", each tagged with a CPAL palette-entry index. To render a
7//! coloured glyph, the consumer takes each layer in order
8//! (back-to-front), resolves its palette index against the CPAL
9//! palette to get an RGBA colour, then paints the layer glyph's
10//! outline at the same pen origin filled with that colour.
11//! * **Version 1** additionally maps a base glyph to the root of a
12//! **directed acyclic graph of Paint tables** (spec §"COLR — Color
13//! Table", OpenType 1.9.1): solid fills, linear / radial / sweep
14//! gradients, glyph-outline clip regions, affine transforms
15//! (translate / scale / rotate / skew / general 2×3), composite /
16//! blend nodes, and layer slices out of a shared LayerList. In
17//! variable fonts every `PaintVar*` form carries a `varIndexBase`
18//! into a `DeltaSetIndexMap` + `ItemVariationStore` pair embedded in
19//! the COLR table itself.
20//!
21//! This module decodes both. The v1 surface is deliberately
22//! **node-by-node**: [`ColrTable::base_glyph_paint`] resolves a base
23//! glyph to a [`PaintRef`] (an opaque validated offset), and
24//! [`ColrTable::paint`] decodes one Paint table into the [`Paint`]
25//! enum with all variation deltas already folded in for the caller's
26//! normalised instance coordinates. Child paints are surfaced as
27//! further `PaintRef`s so the *caller* owns graph traversal and can
28//! bound depth / detect cycles however it likes (the spec requires the
29//! graph to be acyclic, but a hostile font can still tie a loop —
30//! never recurse unboundedly over `PaintRef`s).
31//!
32//! ## Version-0 header layout (14 bytes)
33//!
34//! ```text
35//! Offset Field Type Notes
36//! ------ ---------------------- -------- -------------------------------
37//! +0 version uint16 0 or 1
38//! +2 numBaseGlyphRecords uint16 BaseGlyph record count
39//! +4 baseGlyphRecordsOffset Offset32 from start of COLR
40//! +8 layerRecordsOffset Offset32 from start of COLR
41//! +12 numLayerRecords uint16 Layer record count
42//! ```
43//!
44//! A version-1 header continues with five more Offset32 fields
45//! (each may be NULL):
46//!
47//! ```text
48//! +14 baseGlyphListOffset Offset32 BaseGlyphList (paint roots)
49//! +18 layerListOffset Offset32 LayerList (PaintColrLayers)
50//! +22 clipListOffset Offset32 ClipList (precomputed boxes)
51//! +26 varIndexMapOffset Offset32 DeltaSetIndexMap
52//! +30 itemVariationStoreOffset Offset32 ItemVariationStore
53//! ```
54//!
55//! ## Variation scheme (spec §"COLR" / staged paint-graph reference §7)
56//!
57//! Each variable table/record carries a `uint32 varIndexBase`; its
58//! variable fields consume mapping entries `varIndexBase + 0`,
59//! `varIndexBase + 1`, … in field order. `varIndexBase == 0xFFFFFFFF`
60//! means "no variation data". With a `DeltaSetIndexMap` present the
61//! computed index selects a map entry (clamping to the last entry when
62//! out of range; an entry of `0xFFFF/0xFFFF` means "no variation
63//! data"); without one, an implicit identity mapping splits the index
64//! into `outer = index >> 16`, `inner = index & 0xFFFF`. Deltas are
65//! integers in the wire units of the varied field (F2DOT14 fields are
66//! varied in 1/16384 steps, Fixed fields in 1/65536 steps, FWORD /
67//! UFWORD fields in font units) — the same convention the staged avar
68//! v2 reference states explicitly for its F2DOT14 deltas.
69//!
70//! The `DeltaSetIndexMap` decoder shared with `HVAR` implements both
71//! formats from the staged OFF common-formats chapter: format 0
72//! (16-bit `mapCount`, byte-identical to the ISO/IEC 14496-22:2019
73//! §7.3.5.2 layout) and format 1 (32-bit `mapCount`). The embedded
74//! `ItemVariationStore` also honours the chapter's `LONG_WORDS`
75//! `wordDeltaCount` flag — the int32 + int16 delta representation the
76//! chapter reserves for 32-bit-variable top-level tables, currently
77//! COLR only. A map with an unrecognised future format byte still
78//! parses the font, but its variation deltas resolve to 0 and
79//! [`ColrTable::var_index_map_unsupported`] reports the degradation.
80
81use crate::parser::{read_i16, read_u16, read_u24, read_u32, read_u8};
82use crate::tables::hvar::DeltaSetIndexMap;
83use crate::tables::mvar::ItemVariationStore;
84use crate::Error;
85
86/// One layer of a version-0 colour glyph: an outline-glyph id plus a
87/// CPAL palette-entry index. `palette_index == 0xFFFF` is the spec's
88/// "use the text foreground colour" sentinel; the consumer renderer is
89/// expected to substitute its own foreground in that case.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct ColorLayer {
92 /// Glyph id of the outline that paints this layer (TT/CFF/CFF2).
93 pub layer_glyph_id: u16,
94 /// CPAL palette-entry index; `0xFFFF` = foreground colour.
95 pub palette_index: u16,
96}
97
98/// Opaque reference to one Paint table inside the COLR slice. Obtained
99/// from [`ColrTable::base_glyph_paint`] or from a decoded [`Paint`]
100/// node's child fields; dereferenced with [`ColrTable::paint`].
101///
102/// The wrapped value is the paint's byte offset from the start of the
103/// COLR table — surfaced so tooling can log / dedupe graph nodes, and
104/// so callers can cycle-check traversals by collecting visited offsets.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
106pub struct PaintRef(pub u32);
107
108/// Gradient color-line extend mode. Unrecognised wire values decode to
109/// `Pad` per the spec ("Unrecognized extend values default to
110/// EXTEND_PAD").
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum Extend {
113 /// Use the nearest colour stop outside the stop range.
114 Pad,
115 /// Repeat from the farthest colour stop.
116 Repeat,
117 /// Mirror the colour line from the nearest end.
118 Reflect,
119}
120
121impl Extend {
122 fn from_wire(v: u8) -> Self {
123 match v {
124 1 => Extend::Repeat,
125 2 => Extend::Reflect,
126 _ => Extend::Pad,
127 }
128 }
129}
130
131/// One gradient colour stop, resolved at the requested instance:
132/// `stop_offset` / `alpha` have any variation deltas folded in and
133/// `alpha` is clamped to `[0.0, 1.0]` per the spec ("values outside
134/// this range are reserved and must be clamped").
135#[derive(Debug, Clone, Copy, PartialEq)]
136pub struct ColorStop {
137 /// Position on the colour line (typically in `[0, 1]`, but any
138 /// F2DOT14 value is legal wire data).
139 pub stop_offset: f32,
140 /// CPAL palette-entry index; `0xFFFF` = foreground colour.
141 pub palette_index: u16,
142 /// Alpha in `[0.0, 1.0]`; multiplied with the CPAL entry's own
143 /// alpha by the renderer.
144 pub alpha: f32,
145}
146
147/// A gradient colour line: extend mode plus the resolved stops, sorted
148/// ascending by `stop_offset`. The sort happens **after** instance
149/// stop-offset values are derived, as the spec requires for variable
150/// fonts ("order is established after applying variation deltas").
151#[derive(Debug, Clone, PartialEq)]
152pub struct ColorLine {
153 /// Behaviour outside the defined stop range.
154 pub extend: Extend,
155 /// Stops sorted ascending by `stop_offset` (stable sort — equal
156 /// offsets keep wire order).
157 pub stops: Vec<ColorStop>,
158}
159
160/// A resolved 2×3 affine matrix (`Affine2x3` / `VarAffine2x3`).
161/// Post-transform position: `x' = xx·x + xy·y + dx`,
162/// `y' = yx·x + yy·y + dy`.
163#[derive(Debug, Clone, Copy, PartialEq)]
164pub struct Affine2x3 {
165 /// x-component of the transformed x-basis vector.
166 pub xx: f32,
167 /// y-component of the transformed x-basis vector.
168 pub yx: f32,
169 /// x-component of the transformed y-basis vector.
170 pub xy: f32,
171 /// y-component of the transformed y-basis vector.
172 pub yy: f32,
173 /// Translation in x.
174 pub dx: f32,
175 /// Translation in y.
176 pub dy: f32,
177}
178
179/// `PaintComposite` mode (spec CompositeMode enumeration). The twelve
180/// Porter-Duff modes, the eleven separable blend modes, and the four
181/// non-separable HSL blend modes. Unrecognised wire values decode to
182/// `Clear` per the spec ("Unrecognized modes must use
183/// COMPOSITE_CLEAR").
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185#[allow(missing_docs)] // variant names mirror the spec enumeration 1:1
186pub enum CompositeMode {
187 Clear,
188 Src,
189 Dest,
190 SrcOver,
191 DestOver,
192 SrcIn,
193 DestIn,
194 SrcOut,
195 DestOut,
196 SrcAtop,
197 DestAtop,
198 Xor,
199 Plus,
200 Screen,
201 Overlay,
202 Darken,
203 Lighten,
204 ColorDodge,
205 ColorBurn,
206 HardLight,
207 SoftLight,
208 Difference,
209 Exclusion,
210 Multiply,
211 HslHue,
212 HslSaturation,
213 HslColor,
214 HslLuminosity,
215}
216
217impl CompositeMode {
218 fn from_wire(v: u8) -> Self {
219 use CompositeMode::*;
220 match v {
221 0 => Clear,
222 1 => Src,
223 2 => Dest,
224 3 => SrcOver,
225 4 => DestOver,
226 5 => SrcIn,
227 6 => DestIn,
228 7 => SrcOut,
229 8 => DestOut,
230 9 => SrcAtop,
231 10 => DestAtop,
232 11 => Xor,
233 12 => Plus,
234 13 => Screen,
235 14 => Overlay,
236 15 => Darken,
237 16 => Lighten,
238 17 => ColorDodge,
239 18 => ColorBurn,
240 19 => HardLight,
241 20 => SoftLight,
242 21 => Difference,
243 22 => Exclusion,
244 23 => Multiply,
245 24 => HslHue,
246 25 => HslSaturation,
247 26 => HslColor,
248 27 => HslLuminosity,
249 _ => Clear,
250 }
251 }
252
253 /// Whether a `PaintComposite` sub-graph using this mode is
254 /// *bounded*, given the boundedness of its source and backdrop
255 /// sub-graphs (spec §"PaintComposite" boundedness table). Version-1
256 /// colour glyph definitions are required to be bounded.
257 pub fn is_bounded(self, source_bounded: bool, backdrop_bounded: bool) -> bool {
258 use CompositeMode::*;
259 match self {
260 Clear => true,
261 Src | SrcOut => source_bounded,
262 Dest | DestOut => backdrop_bounded,
263 SrcIn | DestIn => source_bounded || backdrop_bounded,
264 _ => source_bounded && backdrop_bounded,
265 }
266 }
267}
268
269/// A precomputed colour-glyph clip box from the ClipList, resolved at
270/// the requested instance. Variable boxes round *outward* (mins toward
271/// −∞, maxes toward +∞) per the spec, so the box only ever expands.
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273pub struct ClipBox {
274 /// Minimum x of the clip box, font units.
275 pub x_min: i32,
276 /// Minimum y of the clip box, font units.
277 pub y_min: i32,
278 /// Maximum x of the clip box, font units.
279 pub x_max: i32,
280 /// Maximum y of the clip box, font units.
281 pub y_max: i32,
282}
283
284/// One decoded Paint table, values resolved at the caller's variation
285/// instance (the `PaintVar*` twin of each format folds its deltas into
286/// the same variant — a renderer never sees the var/non-var split).
287/// Child paints are [`PaintRef`]s to be decoded with
288/// [`ColrTable::paint`]; the caller owns traversal and must bound
289/// depth (hostile fonts can tie cycles through `PaintColrGlyph`).
290#[derive(Debug, Clone, PartialEq)]
291pub enum Paint {
292 /// Formats 1: a bottom-up z-ordered slice of the LayerList,
293 /// composited with source-over.
294 ColrLayers {
295 /// Child paints, bottom (first) to top (last).
296 layers: Vec<PaintRef>,
297 },
298 /// Formats 2/3: a solid CPAL-palette fill.
299 Solid {
300 /// CPAL palette-entry index; `0xFFFF` = foreground colour.
301 palette_index: u16,
302 /// Alpha in `[0.0, 1.0]`.
303 alpha: f32,
304 },
305 /// Formats 4/5: linear gradient along p₀→p₁ with rotation point p₂.
306 LinearGradient {
307 /// The gradient colour line.
308 color_line: ColorLine,
309 /// Start point p₀ x, font units.
310 x0: f32,
311 /// Start point p₀ y.
312 y0: f32,
313 /// End point p₁ x.
314 x1: f32,
315 /// End point p₁ y.
316 y1: f32,
317 /// Rotation point p₂ x.
318 x2: f32,
319 /// Rotation point p₂ y.
320 y2: f32,
321 },
322 /// Formats 6/7: radial gradient between two circles. Radii are
323 /// unsigned on the wire but variation deltas may drive them
324 /// negative; the spec's r(ω) algorithm handles that, so the
325 /// resolved values are surfaced un-clamped.
326 RadialGradient {
327 /// The gradient colour line.
328 color_line: ColorLine,
329 /// Start circle centre x, font units.
330 x0: f32,
331 /// Start circle centre y.
332 y0: f32,
333 /// Start circle radius.
334 radius0: f32,
335 /// End circle centre x.
336 x1: f32,
337 /// End circle centre y.
338 y1: f32,
339 /// End circle radius.
340 radius1: f32,
341 },
342 /// Formats 8/9: sweep gradient around a centre. Angles are in
343 /// counter-clockwise **degrees** with the spec's +1.0 bias already
344 /// applied (wire F2DOT14 −2.0 → −180°, 0.0 → +180°, 1.0 → +360°).
345 SweepGradient {
346 /// The gradient colour line.
347 color_line: ColorLine,
348 /// Centre x, font units.
349 center_x: f32,
350 /// Centre y, font units.
351 center_y: f32,
352 /// Start angle, degrees counter-clockwise from the positive
353 /// x-axis direction.
354 start_angle_degrees: f32,
355 /// End angle, degrees counter-clockwise.
356 end_angle_degrees: f32,
357 },
358 /// Format 10: use a glyph outline as the clip region for the child
359 /// fill sub-graph. Any COLR data for `glyph_id` itself is ignored
360 /// here — it must be an ordinary outline glyph.
361 Glyph {
362 /// The fill sub-graph, clipped to the outline.
363 paint: PaintRef,
364 /// Outline glyph id (`glyf`/`CFF `/`CFF2`).
365 glyph_id: u16,
366 },
367 /// Format 11: reuse another base glyph's whole paint graph as a
368 /// child sub-graph. Resolve through
369 /// [`ColrTable::base_glyph_paint`]; a missing record makes the
370 /// colour glyph not well-formed per the spec.
371 ColrGlyph {
372 /// BaseGlyphList base glyph id.
373 glyph_id: u16,
374 },
375 /// Formats 12/13: general 2×3 affine transform of the child.
376 Transform {
377 /// The transformed sub-graph.
378 paint: PaintRef,
379 /// The resolved matrix.
380 transform: Affine2x3,
381 },
382 /// Formats 14/15: translation of the child, font units.
383 Translate {
384 /// The translated sub-graph.
385 paint: PaintRef,
386 /// Translation in x.
387 dx: f32,
388 /// Translation in y.
389 dy: f32,
390 },
391 /// Formats 16–23: scaling of the child about `(center_x,
392 /// center_y)`. The four wire forms (x/y vs. uniform, origin vs.
393 /// explicit centre) all fold here — uniform forms set
394 /// `scale_x == scale_y`, origin-centred forms set the centre to
395 /// `(0, 0)`; [`ColrTable::paint_format`] recovers the wire form.
396 Scale {
397 /// The scaled sub-graph.
398 paint: PaintRef,
399 /// Scale factor in x.
400 scale_x: f32,
401 /// Scale factor in y.
402 scale_y: f32,
403 /// Centre of scaling x, font units.
404 center_x: f32,
405 /// Centre of scaling y, font units.
406 center_y: f32,
407 },
408 /// Formats 24–27: rotation of the child about `(center_x,
409 /// center_y)`. Degrees counter-clockwise, **no** bias (wire
410 /// F2DOT14 × 180).
411 Rotate {
412 /// The rotated sub-graph.
413 paint: PaintRef,
414 /// Rotation angle, degrees counter-clockwise.
415 angle_degrees: f32,
416 /// Centre of rotation x, font units.
417 center_x: f32,
418 /// Centre of rotation y, font units.
419 center_y: f32,
420 },
421 /// Formats 28–31: skew of the child about `(center_x, center_y)`.
422 /// Degrees, no bias.
423 Skew {
424 /// The skewed sub-graph.
425 paint: PaintRef,
426 /// Skew angle in the x-axis direction, degrees.
427 x_skew_degrees: f32,
428 /// Skew angle in the y-axis direction, degrees.
429 y_skew_degrees: f32,
430 /// Centre of skew x, font units.
431 center_x: f32,
432 /// Centre of skew y, font units.
433 center_y: f32,
434 },
435 /// Format 32: render `backdrop`, render `source`, combine with
436 /// `mode`, then composite onto the surface.
437 Composite {
438 /// Source sub-graph (rendered second).
439 source: PaintRef,
440 /// Combining mode.
441 mode: CompositeMode,
442 /// Backdrop sub-graph (rendered first).
443 backdrop: PaintRef,
444 },
445}
446
447/// Cap on eagerly-decoded v1 array lengths that a hostile header could
448/// otherwise inflate (each entry is bounds-checked against the table
449/// anyway; the cap just bounds allocation before that check bites).
450const MAX_V1_RECORDS: u32 = 1 << 20;
451
452/// Path-depth cap for the boundedness analysis. The spec caps nothing,
453/// but a legitimate paint graph nests a few dozen levels at most.
454const BOUNDEDNESS_MAX_DEPTH: usize = 64;
455
456/// Total node-visit budget for the boundedness analysis: shared
457/// sub-graphs (diamonds) re-evaluate on every path, so an adversarial
458/// DAG could otherwise cost exponential work.
459const BOUNDEDNESS_BUDGET: u32 = 4096;
460
461/// Parsed COLR table (v0 layer stacks + the v1 paint graph).
462#[derive(Debug, Clone)]
463// internal — exposed for tests/fuzz; not part of the stable API
464#[doc(hidden)]
465pub struct ColrTable<'a> {
466 bytes: &'a [u8],
467 /// Number of `BaseGlyphRecord`s (always v0-array-shaped).
468 num_base_records: u16,
469 /// Byte offset (from start of COLR) of the base record array.
470 base_records_offset: u32,
471 /// Number of `LayerRecord`s.
472 num_layer_records: u16,
473 /// Byte offset of the layer record array.
474 layer_records_offset: u32,
475 /// v1 BaseGlyphPaintRecords: `(glyphID, absolute paint offset)`,
476 /// wire order (sorted ascending by glyphID per spec).
477 base_glyph_paints: Vec<(u16, u32)>,
478 /// v1 LayerList: absolute paint offsets, wire order.
479 layer_list: Vec<u32>,
480 /// v1 ClipList: `(startGlyphID, endGlyphID, absolute ClipBox
481 /// offset)`, wire order (sorted ascending by startGlyphID).
482 clip_records: Vec<(u16, u16, u32)>,
483 /// v1 DeltaSetIndexMap (format 0 or 1).
484 var_index_map: Option<DeltaSetIndexMap>,
485 /// A varIndexMap was present but not decodable (an unrecognised
486 /// future format byte, reserved entryFormat bits, or a truncated
487 /// map); variation deltas degrade to 0.
488 var_index_map_unsupported: bool,
489 /// v1 ItemVariationStore.
490 ivs: Option<ItemVariationStore>,
491}
492
493impl<'a> ColrTable<'a> {
494 /// Validate the header, remember the v0 array offsets, and — for a
495 /// version-1 table — eagerly decode the BaseGlyphList / LayerList /
496 /// ClipList arrays plus the embedded DeltaSetIndexMap and
497 /// ItemVariationStore. Versions above 1 are accepted (the v0/v1
498 /// fields keep their offsets; unknown trailing extensions are
499 /// ignored).
500 pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
501 if bytes.len() < 14 {
502 return Err(Error::UnexpectedEof);
503 }
504 let version = read_u16(bytes, 0)?;
505 let num_base_records = read_u16(bytes, 2)?;
506 let base_records_offset = read_u32(bytes, 4)?;
507 let layer_records_offset = read_u32(bytes, 8)?;
508 let num_layer_records = read_u16(bytes, 12)?;
509
510 // Range-check the two v0 arrays against the COLR slice. We
511 // allow an empty array (numFoo == 0) regardless of offset.
512 if num_base_records > 0 {
513 let end = (base_records_offset as u64)
514 .checked_add(num_base_records as u64 * 6)
515 .ok_or(Error::BadOffset)?;
516 if end > bytes.len() as u64 {
517 return Err(Error::BadOffset);
518 }
519 }
520 if num_layer_records > 0 {
521 let end = (layer_records_offset as u64)
522 .checked_add(num_layer_records as u64 * 4)
523 .ok_or(Error::BadOffset)?;
524 if end > bytes.len() as u64 {
525 return Err(Error::BadOffset);
526 }
527 }
528
529 let mut table = Self {
530 bytes,
531 num_base_records,
532 base_records_offset,
533 num_layer_records,
534 layer_records_offset,
535 base_glyph_paints: Vec::new(),
536 layer_list: Vec::new(),
537 clip_records: Vec::new(),
538 var_index_map: None,
539 var_index_map_unsupported: false,
540 ivs: None,
541 };
542
543 if version >= 1 && bytes.len() >= 34 {
544 table.parse_v1_extras()?;
545 }
546 Ok(table)
547 }
548
549 /// Decode the five v1 header offsets and the structures they point
550 /// at. All offsets are from the start of the COLR table; each may
551 /// be NULL (0).
552 fn parse_v1_extras(&mut self) -> Result<(), Error> {
553 let bytes = self.bytes;
554 let base_glyph_list_off = read_u32(bytes, 14)? as usize;
555 let layer_list_off = read_u32(bytes, 18)? as usize;
556 let clip_list_off = read_u32(bytes, 22)? as usize;
557 let var_index_map_off = read_u32(bytes, 26)? as usize;
558 let ivs_off = read_u32(bytes, 30)? as usize;
559
560 if base_glyph_list_off != 0 {
561 if base_glyph_list_off + 4 > bytes.len() {
562 return Err(Error::BadOffset);
563 }
564 let count = read_u32(bytes, base_glyph_list_off)?;
565 if count > MAX_V1_RECORDS {
566 return Err(Error::BadStructure("COLR BaseGlyphList count exceeds cap"));
567 }
568 let end = (base_glyph_list_off as u64)
569 .checked_add(4 + count as u64 * 6)
570 .ok_or(Error::BadOffset)?;
571 if end > bytes.len() as u64 {
572 return Err(Error::BadOffset);
573 }
574 self.base_glyph_paints.reserve(count as usize);
575 for i in 0..count as usize {
576 let off = base_glyph_list_off + 4 + i * 6;
577 let gid = read_u16(bytes, off)?;
578 let paint_off = read_u32(bytes, off + 2)?;
579 // Paint offsets are relative to the BaseGlyphList
580 // start; store absolute + validated-in-bounds.
581 let abs = (base_glyph_list_off as u64)
582 .checked_add(paint_off as u64)
583 .ok_or(Error::BadOffset)?;
584 if paint_off == 0 || abs >= bytes.len() as u64 {
585 return Err(Error::BadOffset);
586 }
587 self.base_glyph_paints.push((gid, abs as u32));
588 }
589 }
590
591 if layer_list_off != 0 {
592 if layer_list_off + 4 > bytes.len() {
593 return Err(Error::BadOffset);
594 }
595 let count = read_u32(bytes, layer_list_off)?;
596 if count > MAX_V1_RECORDS {
597 return Err(Error::BadStructure("COLR LayerList count exceeds cap"));
598 }
599 let end = (layer_list_off as u64)
600 .checked_add(4 + count as u64 * 4)
601 .ok_or(Error::BadOffset)?;
602 if end > bytes.len() as u64 {
603 return Err(Error::BadOffset);
604 }
605 self.layer_list.reserve(count as usize);
606 for i in 0..count as usize {
607 let off = layer_list_off + 4 + i * 4;
608 let paint_off = read_u32(bytes, off)?;
609 let abs = (layer_list_off as u64)
610 .checked_add(paint_off as u64)
611 .ok_or(Error::BadOffset)?;
612 if paint_off == 0 || abs >= bytes.len() as u64 {
613 return Err(Error::BadOffset);
614 }
615 self.layer_list.push(abs as u32);
616 }
617 }
618
619 if clip_list_off != 0 {
620 if clip_list_off + 5 > bytes.len() {
621 return Err(Error::BadOffset);
622 }
623 let format = read_u8(bytes, clip_list_off)?;
624 // Only format 1 is defined; ignore an unrecognised format
625 // (forward compatibility) rather than rejecting the font.
626 if format == 1 {
627 let count = read_u32(bytes, clip_list_off + 1)?;
628 if count > MAX_V1_RECORDS {
629 return Err(Error::BadStructure("COLR ClipList count exceeds cap"));
630 }
631 let end = (clip_list_off as u64)
632 .checked_add(5 + count as u64 * 7)
633 .ok_or(Error::BadOffset)?;
634 if end > bytes.len() as u64 {
635 return Err(Error::BadOffset);
636 }
637 self.clip_records.reserve(count as usize);
638 for i in 0..count as usize {
639 let off = clip_list_off + 5 + i * 7;
640 let start = read_u16(bytes, off)?;
641 let end_gid = read_u16(bytes, off + 2)?;
642 let box_off = read_u24(bytes, off + 4)?;
643 let abs = (clip_list_off as u64)
644 .checked_add(box_off as u64)
645 .ok_or(Error::BadOffset)?;
646 if box_off == 0 || abs >= bytes.len() as u64 {
647 return Err(Error::BadOffset);
648 }
649 self.clip_records.push((start, end_gid, abs as u32));
650 }
651 }
652 }
653
654 if var_index_map_off != 0 {
655 if var_index_map_off >= bytes.len() {
656 return Err(Error::BadOffset);
657 }
658 // The shared decoder implements both defined map formats
659 // (0 and 1). An unrecognised future format / malformed
660 // map degrades to no-variation rather than rejecting the
661 // font, and is flagged.
662 match DeltaSetIndexMap::parse(&bytes[var_index_map_off..]) {
663 Ok(map) => self.var_index_map = Some(map),
664 Err(_) => self.var_index_map_unsupported = true,
665 }
666 }
667
668 if ivs_off != 0 {
669 if ivs_off >= bytes.len() {
670 return Err(Error::BadOffset);
671 }
672 self.ivs = Some(ItemVariationStore::parse(&bytes[ivs_off..])?);
673 }
674 Ok(())
675 }
676
677 // ---- v0 ---------------------------------------------------------------
678
679 /// Locate `glyph_id`'s BaseGlyphRecord by binary search and decode
680 /// its `(first_layer_index, num_layers)` pair. Returns `None` when
681 /// the glyph isn't a base — i.e. it's a single-colour outline glyph
682 /// or a layer-only glyph.
683 fn find_base_record(&self, glyph_id: u16) -> Option<(u16, u16)> {
684 // Records are required to be sorted ascending by glyphID.
685 let base = self.base_records_offset as usize;
686 let mut lo = 0i32;
687 let mut hi = self.num_base_records as i32 - 1;
688 while lo <= hi {
689 let mid = ((lo + hi) >> 1) as usize;
690 let off = base + mid * 6;
691 let gid = read_u16(self.bytes, off).ok()?;
692 match gid.cmp(&glyph_id) {
693 std::cmp::Ordering::Less => lo = mid as i32 + 1,
694 std::cmp::Ordering::Greater => hi = mid as i32 - 1,
695 std::cmp::Ordering::Equal => {
696 let first = read_u16(self.bytes, off + 2).ok()?;
697 let count = read_u16(self.bytes, off + 4).ok()?;
698 return Some((first, count));
699 }
700 }
701 }
702 None
703 }
704
705 /// All version-0 colour layers for `glyph_id`, in back-to-front
706 /// paint order (= the order the layer records appear in the table).
707 /// Returns an empty `Vec` when the glyph isn't a colour glyph or
708 /// the COLR table is empty. Note the spec prefers a version-1
709 /// paint graph over a version-0 layer stack for the same base
710 /// glyph — check [`Self::base_glyph_paint`] first.
711 pub fn layers(&self, glyph_id: u16) -> Vec<ColorLayer> {
712 let (first, count) = match self.find_base_record(glyph_id) {
713 Some(p) => p,
714 None => return Vec::new(),
715 };
716 let mut out = Vec::with_capacity(count as usize);
717 let layer_base = self.layer_records_offset as usize;
718 for i in 0..count {
719 let idx = first as usize + i as usize;
720 // Spec says firstLayerIndex+numLayers must be <=
721 // numLayerRecords; we range-check defensively anyway.
722 if idx >= self.num_layer_records as usize {
723 break;
724 }
725 let off = layer_base + idx * 4;
726 let layer_glyph_id = match read_u16(self.bytes, off) {
727 Ok(v) => v,
728 Err(_) => break,
729 };
730 let palette_index = match read_u16(self.bytes, off + 2) {
731 Ok(v) => v,
732 Err(_) => break,
733 };
734 out.push(ColorLayer {
735 layer_glyph_id,
736 palette_index,
737 });
738 }
739 out
740 }
741
742 /// Number of `BaseGlyphRecord`s the table ships. Mostly useful for
743 /// tests / debug printing; consumers should call `layers` directly.
744 pub fn num_base_records(&self) -> u16 {
745 self.num_base_records
746 }
747
748 // ---- v1: base glyphs, layers, clips -----------------------------------
749
750 /// `true` when the table carries a version-1 BaseGlyphList with at
751 /// least one paint record.
752 pub fn has_paint_graph(&self) -> bool {
753 !self.base_glyph_paints.is_empty()
754 }
755
756 /// Number of `BaseGlyphPaintRecord`s in the BaseGlyphList.
757 pub fn num_base_glyph_paint_records(&self) -> u32 {
758 self.base_glyph_paints.len() as u32
759 }
760
761 /// Resolve `glyph_id` to the root Paint of its version-1 colour
762 /// glyph graph (binary search over the sorted BaseGlyphList).
763 pub fn base_glyph_paint(&self, glyph_id: u16) -> Option<PaintRef> {
764 self.base_glyph_paints
765 .binary_search_by_key(&glyph_id, |&(g, _)| g)
766 .ok()
767 .map(|i| PaintRef(self.base_glyph_paints[i].1))
768 }
769
770 /// Enumerate every `(glyphID, root PaintRef)` pair in the
771 /// BaseGlyphList, wire order.
772 pub fn base_glyph_paint_records(&self) -> impl Iterator<Item = (u16, PaintRef)> + '_ {
773 self.base_glyph_paints
774 .iter()
775 .map(|&(g, off)| (g, PaintRef(off)))
776 }
777
778 /// Number of entries in the LayerList.
779 pub fn layer_list_len(&self) -> u32 {
780 self.layer_list.len() as u32
781 }
782
783 /// A varIndexMap is present but does not decode — an
784 /// unrecognised format byte (a future revision), reserved
785 /// entryFormat bits, or a truncated map. Both defined formats
786 /// (0 and 1) decode, so this only fires on malformed or
787 /// future-format maps — all variation deltas resolve to 0 for
788 /// such a font.
789 pub fn var_index_map_unsupported(&self) -> bool {
790 self.var_index_map_unsupported
791 }
792
793 /// `true` when the table embeds an `ItemVariationStore` (i.e. the
794 /// paint graph can vary across instances).
795 pub fn has_variations(&self) -> bool {
796 self.ivs.is_some()
797 }
798
799 /// The precomputed clip box covering `glyph_id`, resolved at
800 /// `coords` (pass `&[]` for the static instance). Binary search
801 /// over the sorted, non-overlapping ClipList ranges. Variable
802 /// boxes (ClipBoxFormat 2) fold their deltas and round outward.
803 pub fn clip_box(&self, glyph_id: u16, coords: &[f32]) -> Option<ClipBox> {
804 let idx = self
805 .clip_records
806 .partition_point(|&(start, _, _)| start <= glyph_id)
807 .checked_sub(1)?;
808 let (start, end, abs) = self.clip_records[idx];
809 if glyph_id < start || glyph_id > end {
810 return None;
811 }
812 let off = abs as usize;
813 let format = read_u8(self.bytes, off).ok()?;
814 let x_min = read_i16(self.bytes, off + 1).ok()?;
815 let y_min = read_i16(self.bytes, off + 3).ok()?;
816 let x_max = read_i16(self.bytes, off + 5).ok()?;
817 let y_max = read_i16(self.bytes, off + 7).ok()?;
818 match format {
819 1 => Some(ClipBox {
820 x_min: x_min as i32,
821 y_min: y_min as i32,
822 x_max: x_max as i32,
823 y_max: y_max as i32,
824 }),
825 2 => {
826 let base = read_u32(self.bytes, off + 9).ok()?;
827 // Round so the box expands: mins toward −∞, maxes
828 // toward +∞ (spec ClipBoxFormat2 rule).
829 Some(ClipBox {
830 x_min: (x_min as f32 + self.var_delta(base, 0, coords)).floor() as i32,
831 y_min: (y_min as f32 + self.var_delta(base, 1, coords)).floor() as i32,
832 x_max: (x_max as f32 + self.var_delta(base, 2, coords)).ceil() as i32,
833 y_max: (y_max as f32 + self.var_delta(base, 3, coords)).ceil() as i32,
834 })
835 }
836 _ => None,
837 }
838 }
839
840 // ---- v1: variation resolution ------------------------------------------
841
842 /// Delta for variable field `field` of the record based at
843 /// `var_index_base`, at `coords`. 0.0 whenever there is no
844 /// variation data (no IVS, the 0xFFFFFFFF sentinel, a 0xFFFF/0xFFFF
845 /// map entry, an out-of-range store index, or an undecodable map).
846 fn var_delta(&self, var_index_base: u32, field: u32, coords: &[f32]) -> f32 {
847 if var_index_base == 0xFFFF_FFFF {
848 return 0.0;
849 }
850 let Some(ivs) = self.ivs.as_ref() else {
851 // Spec: without an ItemVariationStore, varIndexBase is
852 // ignored entirely.
853 return 0.0;
854 };
855 if self.var_index_map_unsupported {
856 return 0.0;
857 }
858 let Some(index) = var_index_base.checked_add(field) else {
859 // "The index sequence must not exceed 0xFFFFFFFF."
860 return 0.0;
861 };
862 let (outer, inner) = match self.var_index_map.as_ref() {
863 Some(map) => {
864 let entries = map.entries();
865 if entries.is_empty() {
866 return 0.0;
867 }
868 // Out-of-range indices clamp to the last entry.
869 let e = entries[(index as usize).min(entries.len() - 1)];
870 if e == (0xFFFF, 0xFFFF) {
871 // "No variation data for this item."
872 return 0.0;
873 }
874 e
875 }
876 // Implicit identity mapping: high 16 bits outer, low 16
877 // bits inner.
878 None => ((index >> 16) as u16, (index & 0xFFFF) as u16),
879 };
880 ivs.delta(outer, inner, coords).unwrap_or(0.0)
881 }
882
883 // ---- v1: boundedness ---------------------------------------------------
884
885 /// Whether the colour glyph rooted at `glyph_id` is *bounded* — a
886 /// well-formedness requirement for version-1 colour glyphs (staged
887 /// reference §9: "A version-1 color glyph definition must be
888 /// bounded"). `Some(false)` means the graph decodes but paints an
889 /// unbounded region (e.g. a bare gradient with no `PaintGlyph`
890 /// clip); `None` means the graph is not well-formed (missing base
891 /// glyph, undecodable node, a cycle, or an adversarially-deep /
892 /// -wide graph that exhausts the analysis budget).
893 pub fn color_glyph_is_bounded(&self, glyph_id: u16) -> Option<bool> {
894 let root = self.base_glyph_paint(glyph_id)?;
895 self.paint_is_bounded(root)
896 }
897
898 /// [`Self::color_glyph_is_bounded`] for an arbitrary sub-graph
899 /// root.
900 pub fn paint_is_bounded(&self, paint: PaintRef) -> Option<bool> {
901 let mut path = Vec::new();
902 let mut budget = BOUNDEDNESS_BUDGET;
903 self.bounded_inner(paint, &mut path, &mut budget)
904 }
905
906 fn bounded_inner(
907 &self,
908 paint: PaintRef,
909 path: &mut Vec<u32>,
910 budget: &mut u32,
911 ) -> Option<bool> {
912 if *budget == 0 || path.len() >= BOUNDEDNESS_MAX_DEPTH {
913 return None;
914 }
915 *budget -= 1;
916 if path.contains(&paint.0) {
917 // A cycle is not a DAG: the glyph is not well-formed.
918 return None;
919 }
920 path.push(paint.0);
921 // Boundedness is structural — modes, shapes, and graph edges
922 // don't move with variation deltas — so the default instance
923 // suffices.
924 let result = match self.paint(paint, &[])? {
925 // The union of bounded regions is bounded; an empty layer
926 // slice paints nothing (bounded).
927 Paint::ColrLayers { layers } => {
928 let mut all = true;
929 for layer in layers {
930 match self.bounded_inner(layer, path, budget) {
931 Some(b) => all &= b,
932 None => {
933 path.pop();
934 return None;
935 }
936 }
937 }
938 Some(all)
939 }
940 // Fills cover the whole clip region: unbounded on their
941 // own.
942 Paint::Solid { .. }
943 | Paint::LinearGradient { .. }
944 | Paint::RadialGradient { .. }
945 | Paint::SweepGradient { .. } => Some(false),
946 // §9: PaintGlyph is inherently bounded (the child fill is
947 // clipped to the outline).
948 Paint::Glyph { .. } => Some(true),
949 // Reuse: bounded iff the referenced glyph's graph is.
950 // A missing BaseGlyphPaintRecord is not well-formed.
951 Paint::ColrGlyph { glyph_id } => {
952 let root = self.base_glyph_paint(glyph_id);
953 match root {
954 Some(root) => self.bounded_inner(root, path, budget),
955 None => None,
956 }
957 }
958 // An affine image of a bounded region is bounded.
959 Paint::Transform { paint, .. }
960 | Paint::Translate { paint, .. }
961 | Paint::Scale { paint, .. }
962 | Paint::Rotate { paint, .. }
963 | Paint::Skew { paint, .. } => self.bounded_inner(paint, path, budget),
964 // The §6 per-mode table via CompositeMode::is_bounded.
965 Paint::Composite {
966 source,
967 mode,
968 backdrop,
969 } => {
970 let s = self.bounded_inner(source, path, budget);
971 let b = self.bounded_inner(backdrop, path, budget);
972 match (s, b) {
973 (Some(s), Some(b)) => Some(mode.is_bounded(s, b)),
974 _ => None,
975 }
976 }
977 };
978 path.pop();
979 result
980 }
981
982 // ---- v1: paint decode ---------------------------------------------------
983
984 /// The wire `format` byte of the Paint table at `paint`, without
985 /// decoding it. Lets tooling distinguish e.g. the four scale wire
986 /// forms that [`Self::paint`] folds into [`Paint::Scale`], and a
987 /// `PaintVar*` from its static twin.
988 pub fn paint_format(&self, paint: PaintRef) -> Option<u8> {
989 read_u8(self.bytes, paint.0 as usize).ok()
990 }
991
992 /// Resolve an `Offset24` child-paint field at `off` (relative to
993 /// the paint table at `base`) into a validated [`PaintRef`].
994 fn child_paint(&self, base: usize, off: usize) -> Option<PaintRef> {
995 let rel = read_u24(self.bytes, base + off).ok()?;
996 if rel == 0 {
997 return None;
998 }
999 let abs = (base as u64).checked_add(rel as u64)?;
1000 if abs >= self.bytes.len() as u64 {
1001 return None;
1002 }
1003 Some(PaintRef(abs as u32))
1004 }
1005
1006 /// Decode the ColorLine / VarColorLine at absolute offset `abs`.
1007 fn color_line(&self, abs: usize, variable: bool, coords: &[f32]) -> Option<ColorLine> {
1008 let extend = Extend::from_wire(read_u8(self.bytes, abs).ok()?);
1009 let num_stops = read_u16(self.bytes, abs + 1).ok()?;
1010 let stride = if variable { 10 } else { 6 };
1011 let mut stops = Vec::with_capacity(num_stops as usize);
1012 for i in 0..num_stops as usize {
1013 let off = abs + 3 + i * stride;
1014 let raw_offset = read_i16(self.bytes, off).ok()?;
1015 let palette_index = read_u16(self.bytes, off + 2).ok()?;
1016 let raw_alpha = read_i16(self.bytes, off + 4).ok()?;
1017 let (d_offset, d_alpha) = if variable {
1018 let base = read_u32(self.bytes, off + 6).ok()?;
1019 (
1020 self.var_delta(base, 0, coords),
1021 self.var_delta(base, 1, coords),
1022 )
1023 } else {
1024 (0.0, 0.0)
1025 };
1026 stops.push(ColorStop {
1027 stop_offset: f2dot14_var(raw_offset, d_offset),
1028 palette_index,
1029 alpha: f2dot14_var(raw_alpha, d_alpha).clamp(0.0, 1.0),
1030 });
1031 }
1032 // "Color stops must be applied in increasing stopOffset order",
1033 // established *after* instance values are derived.
1034 stops.sort_by(|a, b| {
1035 a.stop_offset
1036 .partial_cmp(&b.stop_offset)
1037 .unwrap_or(std::cmp::Ordering::Equal)
1038 });
1039 Some(ColorLine { extend, stops })
1040 }
1041
1042 /// Decode the `Offset24` ColorLine field at `off` within the paint
1043 /// at `base`.
1044 fn paint_color_line(
1045 &self,
1046 base: usize,
1047 off: usize,
1048 variable: bool,
1049 coords: &[f32],
1050 ) -> Option<ColorLine> {
1051 let rel = read_u24(self.bytes, base + off).ok()?;
1052 if rel == 0 {
1053 return None;
1054 }
1055 let abs = (base as u64).checked_add(rel as u64)?;
1056 if abs >= self.bytes.len() as u64 {
1057 return None;
1058 }
1059 self.color_line(abs as usize, variable, coords)
1060 }
1061
1062 /// Decode one Paint table at the caller's variation instance
1063 /// (`coords` = the avar-bent normalised coordinate vector; pass
1064 /// `&[]` for the default / static instance). Every `PaintVar*`
1065 /// form resolves to the same [`Paint`] variant as its static twin
1066 /// with the deltas folded in. Returns `None` for an unrecognised
1067 /// format (the spec's forward-compatibility behaviour: ignore) or
1068 /// a malformed table.
1069 pub fn paint(&self, paint: PaintRef, coords: &[f32]) -> Option<Paint> {
1070 let b = self.bytes;
1071 let p = paint.0 as usize;
1072 let format = read_u8(b, p).ok()?;
1073 match format {
1074 // PaintColrLayers
1075 1 => {
1076 let num_layers = read_u8(b, p + 1).ok()? as usize;
1077 let first = read_u32(b, p + 2).ok()? as usize;
1078 let mut layers = Vec::with_capacity(num_layers);
1079 for i in 0..num_layers {
1080 // Defensive: a slice reaching past the LayerList
1081 // truncates rather than failing the whole node.
1082 let Some(&abs) = self.layer_list.get(first + i) else {
1083 break;
1084 };
1085 layers.push(PaintRef(abs));
1086 }
1087 Some(Paint::ColrLayers { layers })
1088 }
1089 // PaintSolid / PaintVarSolid
1090 2 | 3 => {
1091 let palette_index = read_u16(b, p + 1).ok()?;
1092 let raw_alpha = read_i16(b, p + 3).ok()?;
1093 let d_alpha = if format == 3 {
1094 let base = read_u32(b, p + 5).ok()?;
1095 self.var_delta(base, 0, coords)
1096 } else {
1097 0.0
1098 };
1099 Some(Paint::Solid {
1100 palette_index,
1101 alpha: f2dot14_var(raw_alpha, d_alpha).clamp(0.0, 1.0),
1102 })
1103 }
1104 // PaintLinearGradient / PaintVarLinearGradient
1105 4 | 5 => {
1106 let variable = format == 5;
1107 let color_line = self.paint_color_line(p, 1, variable, coords)?;
1108 let mut v = [0.0f32; 6];
1109 let vb = if variable {
1110 read_u32(b, p + 16).ok()?
1111 } else {
1112 0xFFFF_FFFF
1113 };
1114 for (i, slot) in v.iter_mut().enumerate() {
1115 let raw = read_i16(b, p + 4 + i * 2).ok()?;
1116 let d = if variable {
1117 self.var_delta(vb, i as u32, coords)
1118 } else {
1119 0.0
1120 };
1121 *slot = raw as f32 + d;
1122 }
1123 Some(Paint::LinearGradient {
1124 color_line,
1125 x0: v[0],
1126 y0: v[1],
1127 x1: v[2],
1128 y1: v[3],
1129 x2: v[4],
1130 y2: v[5],
1131 })
1132 }
1133 // PaintRadialGradient / PaintVarRadialGradient
1134 6 | 7 => {
1135 let variable = format == 7;
1136 let color_line = self.paint_color_line(p, 1, variable, coords)?;
1137 let vb = if variable {
1138 read_u32(b, p + 16).ok()?
1139 } else {
1140 0xFFFF_FFFF
1141 };
1142 let mut v = [0.0f32; 6];
1143 for (i, slot) in v.iter_mut().enumerate() {
1144 // Fields 2 and 5 (radius0 / radius1) are UFWORD;
1145 // the rest FWORD. Deltas are font-unit integers
1146 // either way and may drive a radius negative.
1147 let raw = if i == 2 || i == 5 {
1148 read_u16(b, p + 4 + i * 2).ok()? as f32
1149 } else {
1150 read_i16(b, p + 4 + i * 2).ok()? as f32
1151 };
1152 let d = if variable {
1153 self.var_delta(vb, i as u32, coords)
1154 } else {
1155 0.0
1156 };
1157 *slot = raw + d;
1158 }
1159 Some(Paint::RadialGradient {
1160 color_line,
1161 x0: v[0],
1162 y0: v[1],
1163 radius0: v[2],
1164 x1: v[3],
1165 y1: v[4],
1166 radius1: v[5],
1167 })
1168 }
1169 // PaintSweepGradient / PaintVarSweepGradient
1170 8 | 9 => {
1171 let variable = format == 9;
1172 let color_line = self.paint_color_line(p, 1, variable, coords)?;
1173 let vb = if variable {
1174 read_u32(b, p + 12).ok()?
1175 } else {
1176 0xFFFF_FFFF
1177 };
1178 let cx = read_i16(b, p + 4).ok()?;
1179 let cy = read_i16(b, p + 6).ok()?;
1180 let sa = read_i16(b, p + 8).ok()?;
1181 let ea = read_i16(b, p + 10).ok()?;
1182 let (d0, d1, d2, d3) = if variable {
1183 (
1184 self.var_delta(vb, 0, coords),
1185 self.var_delta(vb, 1, coords),
1186 self.var_delta(vb, 2, coords),
1187 self.var_delta(vb, 3, coords),
1188 )
1189 } else {
1190 (0.0, 0.0, 0.0, 0.0)
1191 };
1192 // Sweep angles carry the +1.0 bias: degrees =
1193 // (value + 1.0) × 180, counter-clockwise.
1194 Some(Paint::SweepGradient {
1195 color_line,
1196 center_x: cx as f32 + d0,
1197 center_y: cy as f32 + d1,
1198 start_angle_degrees: (f2dot14_var(sa, d2) + 1.0) * 180.0,
1199 end_angle_degrees: (f2dot14_var(ea, d3) + 1.0) * 180.0,
1200 })
1201 }
1202 // PaintGlyph
1203 10 => {
1204 let child = self.child_paint(p, 1)?;
1205 let glyph_id = read_u16(b, p + 4).ok()?;
1206 Some(Paint::Glyph {
1207 paint: child,
1208 glyph_id,
1209 })
1210 }
1211 // PaintColrGlyph
1212 11 => {
1213 let glyph_id = read_u16(b, p + 1).ok()?;
1214 Some(Paint::ColrGlyph { glyph_id })
1215 }
1216 // PaintTransform / PaintVarTransform
1217 12 | 13 => {
1218 let child = self.child_paint(p, 1)?;
1219 let t_rel = read_u24(b, p + 4).ok()?;
1220 if t_rel == 0 {
1221 return None;
1222 }
1223 let t = (p as u64).checked_add(t_rel as u64)?;
1224 if t >= b.len() as u64 {
1225 return None;
1226 }
1227 let t = t as usize;
1228 let vb = if format == 13 {
1229 read_u32(b, t + 24).ok()?
1230 } else {
1231 0xFFFF_FFFF
1232 };
1233 let mut v = [0.0f32; 6];
1234 for (i, slot) in v.iter_mut().enumerate() {
1235 let raw = crate::parser::read_i32(b, t + i * 4).ok()?;
1236 let d = if format == 13 {
1237 self.var_delta(vb, i as u32, coords)
1238 } else {
1239 0.0
1240 };
1241 // Fixed (16.16): deltas are integers in 1/65536
1242 // wire steps.
1243 *slot = (raw as f32 + d) / 65536.0;
1244 }
1245 Some(Paint::Transform {
1246 paint: child,
1247 transform: Affine2x3 {
1248 xx: v[0],
1249 yx: v[1],
1250 xy: v[2],
1251 yy: v[3],
1252 dx: v[4],
1253 dy: v[5],
1254 },
1255 })
1256 }
1257 // PaintTranslate / PaintVarTranslate
1258 14 | 15 => {
1259 let child = self.child_paint(p, 1)?;
1260 let dx = read_i16(b, p + 4).ok()?;
1261 let dy = read_i16(b, p + 6).ok()?;
1262 let (d0, d1) = if format == 15 {
1263 let vb = read_u32(b, p + 8).ok()?;
1264 (self.var_delta(vb, 0, coords), self.var_delta(vb, 1, coords))
1265 } else {
1266 (0.0, 0.0)
1267 };
1268 Some(Paint::Translate {
1269 paint: child,
1270 dx: dx as f32 + d0,
1271 dy: dy as f32 + d1,
1272 })
1273 }
1274 // PaintScale family (16..=23)
1275 16..=23 => {
1276 let child = self.child_paint(p, 1)?;
1277 let uniform = format >= 20;
1278 let around_center = matches!(format, 18 | 19 | 22 | 23);
1279 let variable = format % 2 == 1;
1280 // Field layout after the child offset: scale factors
1281 // (1 or 2 × F2DOT14), then optional centre (2 × FWORD),
1282 // then optional varIndexBase.
1283 let n_scales = if uniform { 1 } else { 2 };
1284 let mut off = p + 4;
1285 let mut raw = [0i16; 4];
1286 let n_fields = n_scales + if around_center { 2 } else { 0 };
1287 for slot in raw.iter_mut().take(n_fields) {
1288 *slot = read_i16(b, off).ok()?;
1289 off += 2;
1290 }
1291 let vb = if variable {
1292 read_u32(b, off).ok()?
1293 } else {
1294 0xFFFF_FFFF
1295 };
1296 let d = |i: u32| -> f32 {
1297 if variable {
1298 self.var_delta(vb, i, coords)
1299 } else {
1300 0.0
1301 }
1302 };
1303 let scale_x = f2dot14_var(raw[0], d(0));
1304 let scale_y = if uniform {
1305 scale_x
1306 } else {
1307 f2dot14_var(raw[1], d(1))
1308 };
1309 let (center_x, center_y) = if around_center {
1310 let ci = n_scales as u32;
1311 (
1312 raw[n_scales] as f32 + d(ci),
1313 raw[n_scales + 1] as f32 + d(ci + 1),
1314 )
1315 } else {
1316 (0.0, 0.0)
1317 };
1318 Some(Paint::Scale {
1319 paint: child,
1320 scale_x,
1321 scale_y,
1322 center_x,
1323 center_y,
1324 })
1325 }
1326 // PaintRotate family (24..=27)
1327 24..=27 => {
1328 let child = self.child_paint(p, 1)?;
1329 let around_center = format >= 26;
1330 let variable = format % 2 == 1;
1331 let angle = read_i16(b, p + 4).ok()?;
1332 let (cx, cy) = if around_center {
1333 (read_i16(b, p + 6).ok()?, read_i16(b, p + 8).ok()?)
1334 } else {
1335 (0, 0)
1336 };
1337 let vb_off = if around_center { p + 10 } else { p + 6 };
1338 let vb = if variable {
1339 read_u32(b, vb_off).ok()?
1340 } else {
1341 0xFFFF_FFFF
1342 };
1343 let d = |i: u32| -> f32 {
1344 if variable {
1345 self.var_delta(vb, i, coords)
1346 } else {
1347 0.0
1348 }
1349 };
1350 // No bias for rotate angles: degrees = value × 180.
1351 Some(Paint::Rotate {
1352 paint: child,
1353 angle_degrees: f2dot14_var(angle, d(0)) * 180.0,
1354 center_x: if around_center { cx as f32 + d(1) } else { 0.0 },
1355 center_y: if around_center { cy as f32 + d(2) } else { 0.0 },
1356 })
1357 }
1358 // PaintSkew family (28..=31)
1359 28..=31 => {
1360 let child = self.child_paint(p, 1)?;
1361 let around_center = format >= 30;
1362 let variable = format % 2 == 1;
1363 let xa = read_i16(b, p + 4).ok()?;
1364 let ya = read_i16(b, p + 6).ok()?;
1365 let (cx, cy) = if around_center {
1366 (read_i16(b, p + 8).ok()?, read_i16(b, p + 10).ok()?)
1367 } else {
1368 (0, 0)
1369 };
1370 let vb_off = if around_center { p + 12 } else { p + 8 };
1371 let vb = if variable {
1372 read_u32(b, vb_off).ok()?
1373 } else {
1374 0xFFFF_FFFF
1375 };
1376 let d = |i: u32| -> f32 {
1377 if variable {
1378 self.var_delta(vb, i, coords)
1379 } else {
1380 0.0
1381 }
1382 };
1383 Some(Paint::Skew {
1384 paint: child,
1385 x_skew_degrees: f2dot14_var(xa, d(0)) * 180.0,
1386 y_skew_degrees: f2dot14_var(ya, d(1)) * 180.0,
1387 center_x: if around_center { cx as f32 + d(2) } else { 0.0 },
1388 center_y: if around_center { cy as f32 + d(3) } else { 0.0 },
1389 })
1390 }
1391 // PaintComposite
1392 32 => {
1393 let source = self.child_paint(p, 1)?;
1394 let mode = CompositeMode::from_wire(read_u8(b, p + 4).ok()?);
1395 let backdrop = self.child_paint(p, 5)?;
1396 Some(Paint::Composite {
1397 source,
1398 mode,
1399 backdrop,
1400 })
1401 }
1402 // Unrecognised paint formats should be ignored (future
1403 // minor versions may add formats).
1404 _ => None,
1405 }
1406 }
1407}
1408
1409/// Resolve an F2DOT14 wire value plus an integer wire-unit delta into
1410/// its real value.
1411#[inline]
1412fn f2dot14_var(raw: i16, delta: f32) -> f32 {
1413 (raw as f32 + delta) / 16384.0
1414}
1415
1416#[cfg(test)]
1417mod tests {
1418 use super::*;
1419
1420 /// Hand-build a 4-byte-aligned COLR v0 fragment with one base
1421 /// glyph (gid 65) that points at three layers.
1422 fn synth_colr_one_base_three_layers() -> Vec<u8> {
1423 // Header (14 B) + 1 BaseGlyphRecord (6 B) + 3 LayerRecord (12 B) = 32 B
1424 let mut bytes = vec![0u8; 32];
1425 // version = 0
1426 bytes[0..2].copy_from_slice(&0u16.to_be_bytes());
1427 // numBaseGlyphRecords = 1
1428 bytes[2..4].copy_from_slice(&1u16.to_be_bytes());
1429 // baseGlyphRecordsOffset = 14
1430 bytes[4..8].copy_from_slice(&14u32.to_be_bytes());
1431 // layerRecordsOffset = 20
1432 bytes[8..12].copy_from_slice(&20u32.to_be_bytes());
1433 // numLayerRecords = 3
1434 bytes[12..14].copy_from_slice(&3u16.to_be_bytes());
1435
1436 // BaseGlyphRecord at +14: glyphID=65, firstLayerIndex=0, numLayers=3
1437 bytes[14..16].copy_from_slice(&65u16.to_be_bytes());
1438 bytes[16..18].copy_from_slice(&0u16.to_be_bytes());
1439 bytes[18..20].copy_from_slice(&3u16.to_be_bytes());
1440
1441 // LayerRecord[0..3] at +20
1442 // Layer 0: glyphID=100, paletteIndex=2
1443 bytes[20..22].copy_from_slice(&100u16.to_be_bytes());
1444 bytes[22..24].copy_from_slice(&2u16.to_be_bytes());
1445 // Layer 1: glyphID=101, paletteIndex=5
1446 bytes[24..26].copy_from_slice(&101u16.to_be_bytes());
1447 bytes[26..28].copy_from_slice(&5u16.to_be_bytes());
1448 // Layer 2: glyphID=102, paletteIndex=0xFFFF (foreground)
1449 bytes[28..30].copy_from_slice(&102u16.to_be_bytes());
1450 bytes[30..32].copy_from_slice(&0xFFFFu16.to_be_bytes());
1451 bytes
1452 }
1453
1454 #[test]
1455 fn parses_v0_header() {
1456 let bytes = synth_colr_one_base_three_layers();
1457 let colr = ColrTable::parse(&bytes).expect("parse");
1458 assert_eq!(colr.num_base_records(), 1);
1459 assert!(!colr.has_paint_graph());
1460 assert!(!colr.has_variations());
1461 }
1462
1463 #[test]
1464 fn layers_for_known_base_glyph() {
1465 let bytes = synth_colr_one_base_three_layers();
1466 let colr = ColrTable::parse(&bytes).expect("parse");
1467 let layers = colr.layers(65);
1468 assert_eq!(
1469 layers,
1470 vec![
1471 ColorLayer {
1472 layer_glyph_id: 100,
1473 palette_index: 2
1474 },
1475 ColorLayer {
1476 layer_glyph_id: 101,
1477 palette_index: 5
1478 },
1479 ColorLayer {
1480 layer_glyph_id: 102,
1481 palette_index: 0xFFFF
1482 },
1483 ]
1484 );
1485 }
1486
1487 #[test]
1488 fn layers_for_non_base_glyph_is_empty() {
1489 let bytes = synth_colr_one_base_three_layers();
1490 let colr = ColrTable::parse(&bytes).expect("parse");
1491 assert!(colr.layers(0).is_empty());
1492 assert!(colr.layers(64).is_empty());
1493 assert!(colr.layers(66).is_empty());
1494 assert!(colr.layers(0xFFFF).is_empty());
1495 }
1496
1497 #[test]
1498 fn rejects_truncated_header() {
1499 assert!(matches!(
1500 ColrTable::parse(&[0u8; 10]),
1501 Err(Error::UnexpectedEof)
1502 ));
1503 }
1504
1505 #[test]
1506 fn rejects_array_past_end() {
1507 let mut bytes = vec![0u8; 14];
1508 // numBaseGlyphRecords = 1, baseRecordsOffset = 14 (but no data after).
1509 bytes[2..4].copy_from_slice(&1u16.to_be_bytes());
1510 bytes[4..8].copy_from_slice(&14u32.to_be_bytes());
1511 assert!(matches!(ColrTable::parse(&bytes), Err(Error::BadOffset)));
1512 }
1513
1514 /// Three base glyphs with random-but-sorted gids: verify binary
1515 /// search lands on the correct middle / left / right elements.
1516 #[test]
1517 fn binary_search_three_records() {
1518 let mut bytes = vec![0u8; 14 + 18 + 12];
1519 bytes[0..2].copy_from_slice(&0u16.to_be_bytes());
1520 bytes[2..4].copy_from_slice(&3u16.to_be_bytes());
1521 bytes[4..8].copy_from_slice(&14u32.to_be_bytes());
1522 bytes[8..12].copy_from_slice(&32u32.to_be_bytes());
1523 bytes[12..14].copy_from_slice(&3u16.to_be_bytes());
1524
1525 // Records (sorted by gid): 10/0/1, 50/1/1, 200/2/1
1526 let recs: [(u16, u16, u16); 3] = [(10, 0, 1), (50, 1, 1), (200, 2, 1)];
1527 for (i, (g, first, count)) in recs.iter().enumerate() {
1528 let off = 14 + i * 6;
1529 bytes[off..off + 2].copy_from_slice(&g.to_be_bytes());
1530 bytes[off + 2..off + 4].copy_from_slice(&first.to_be_bytes());
1531 bytes[off + 4..off + 6].copy_from_slice(&count.to_be_bytes());
1532 }
1533 // Layers: gid 1000+i / palette i
1534 for i in 0..3 {
1535 let off = 32 + i * 4;
1536 bytes[off..off + 2].copy_from_slice(&(1000 + i as u16).to_be_bytes());
1537 bytes[off + 2..off + 4].copy_from_slice(&(i as u16).to_be_bytes());
1538 }
1539
1540 let colr = ColrTable::parse(&bytes).expect("parse");
1541 // Hits
1542 for (gid, _first, _count) in &recs {
1543 let layers = colr.layers(*gid);
1544 assert_eq!(layers.len(), 1, "gid {gid}");
1545 }
1546 // Misses
1547 assert!(colr.layers(0).is_empty());
1548 assert!(colr.layers(11).is_empty());
1549 assert!(colr.layers(199).is_empty());
1550 assert!(colr.layers(201).is_empty());
1551 }
1552}