Coverage Report

Created: 2026-10-24 07:19

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/read-fonts-0.46.0/src/tables/colr.rs
Line
Count
Source
1
//! The [COLR](https://docs.microsoft.com/en-us/typography/opentype/spec/colr) table
2
3
#[cfg(feature = "std")]
4
mod closure;
5
6
use super::variations::{DeltaSetIndexMap, ItemVariationStore};
7
8
include!("../../generated/generated_colr.rs");
9
10
/// Unique paint identifier used for detecting cycles in the paint graph.
11
pub type PaintId = usize;
12
13
impl<'a> Colr<'a> {
14
    /// Returns the COLRv0 base glyph for the given glyph identifier.
15
    ///
16
    /// The return value is a range of layer indices that can be passed to
17
    /// [`v0_layer`](Self::v0_layer) to retrieve the layer glyph identifiers
18
    /// and palette color indices.
19
1.15k
    pub fn v0_base_glyph(&self, glyph_id: GlyphId) -> Option<Range<usize>> {
20
1.15k
        let records = self.base_glyph_records()?.ok()?;
21
525
        let glyph_id = glyph_id.try_into().ok()?;
22
525
        let ix = records
23
2.33k
            .binary_search_by(|rec| rec.glyph_id().cmp(&glyph_id))
24
525
            .ok()?;
25
484
        let record = records.get(ix)?;
26
484
        let start = record.first_layer_index() as usize;
27
484
        Some(start..start.checked_add(record.num_layers() as usize)?)
28
1.15k
    }
29
30
    /// Returns the COLRv0 layer at the given index.
31
    ///
32
    /// The layer is represented by a tuple containing the glyph identifier of
33
    /// the associated outline and the palette color index.
34
544
    pub fn v0_layer(&self, index: usize) -> Option<(GlyphId16, u16)> {
35
544
        let layer = self.layer_records()?.ok()?.get(index)?;
36
414
        Some((layer.glyph_id(), layer.palette_index()))
37
544
    }
38
39
    /// Returns the COLRv1 base glyph for the given glyph identifier.
40
    ///
41
    /// The second value in the tuple is a unique identifier for the paint that
42
    /// may be used to detect recursion in the paint graph.
43
1.46k
    pub fn v1_base_glyph(&self, glyph_id: GlyphId) -> Option<(Paint<'a>, PaintId)> {
44
1.46k
        let glyph_id = glyph_id.try_into().ok()?;
45
1.46k
        let list = self.base_glyph_list()?.ok()?;
46
536
        let records = list.base_glyph_paint_records();
47
536
        let ix = records
48
2.08k
            .binary_search_by(|rec| rec.glyph_id().cmp(&glyph_id))
49
536
            .ok()?;
50
406
        let record = records.get(ix)?;
51
406
        let offset_data = list.offset_data();
52
        // Use the address of the paint as an identifier for the recursion
53
        // blacklist.
54
406
        let id = record.paint_offset().to_u32() as usize + offset_data.as_ref().as_ptr() as usize;
55
406
        Some((record.paint(offset_data).ok()?, id))
56
1.46k
    }
57
58
    /// Returns the COLRv1 layer at the given index.
59
    ///
60
    /// The second value in the tuple is a unique identifier for the paint that
61
    /// may be used to detect recursion in the paint graph.
62
16
    pub fn v1_layer(&self, index: usize) -> Option<(Paint<'a>, PaintId)> {
63
16
        let list = self.layer_list()?.ok()?;
64
0
        let offset = list.paint_offsets().get(index)?.get();
65
0
        let offset_data = list.offset_data();
66
        // Use the address of the paint as an identifier for the recursion
67
        // blacklist.
68
0
        let id = offset.to_u32() as usize + offset_data.as_ref().as_ptr() as usize;
69
0
        Some((offset.resolve(offset_data).ok()?, id))
70
16
    }
71
72
    /// Returns the COLRv1 clip box for the given glyph identifier.
73
1.10k
    pub fn v1_clip_box(&self, glyph_id: GlyphId) -> Option<ClipBox<'a>> {
74
        use core::cmp::Ordering;
75
1.10k
        let glyph_id: GlyphId16 = glyph_id.try_into().ok()?;
76
1.10k
        let list = self.clip_list()?.ok()?;
77
174
        let clips = list.clips();
78
174
        let ix = clips
79
530
            .binary_search_by(|clip| {
80
530
                if glyph_id < clip.start_glyph_id() {
81
326
                    Ordering::Greater
82
204
                } else if glyph_id > clip.end_glyph_id() {
83
0
                    Ordering::Less
84
                } else {
85
204
                    Ordering::Equal
86
                }
87
530
            })
88
174
            .ok()?;
89
84
        let clip = clips.get(ix)?;
90
84
        clip.clip_box(list.offset_data()).ok()
91
1.10k
    }
92
}
93
94
#[cfg(test)]
95
mod tests {
96
    use super::*;
97
    use font_test_data::bebuffer::BeBuffer;
98
99
    #[test]
100
    fn paint_glyph2_reads_full_glyph_id() {
101
        for gid in [0, 0xffff, 0x10000, 0x123456, 0xffffff] {
102
            let data = BeBuffer::new()
103
                .push(33u8)
104
                .push(Offset24::new(Uint24::new(7)))
105
                .push(GlyphId24::new(gid))
106
                .extend([2u8, 0, 7, 0x40, 0]);
107
            let Paint::Glyph2(paint) = Paint::read(FontData::new(&data)).unwrap() else {
108
                panic!("expected PaintGlyph2");
109
            };
110
            assert_eq!(paint.glyph_id().to_u32(), gid);
111
            assert_eq!(paint.paint_offset().to_u32(), 7);
112
            let Paint::Solid(solid) = paint.paint().unwrap() else {
113
                panic!("expected PaintSolid");
114
            };
115
            assert_eq!(solid.palette_index(), 7);
116
            assert_eq!(solid.alpha(), F2Dot14::ONE);
117
        }
118
    }
119
120
    #[test]
121
    fn paint_glyph2_rejects_truncated_header_and_child() {
122
        let data = font_test_data::colr::PAINT_GLYPH2;
123
        for len in 0..7 {
124
            assert!(Paint::read(FontData::new(&data[..len])).is_err());
125
        }
126
        for len in 7..data.len() {
127
            let Paint::Glyph2(paint) = Paint::read(FontData::new(&data[..len])).unwrap() else {
128
                panic!("expected PaintGlyph2");
129
            };
130
            assert!(paint.paint().is_err());
131
        }
132
    }
133
}