Skip to main content

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}