1
#![deny(missing_docs)]
2

            
3
//! `CPAL` table parsing.
4
//!
5
//! <https://learn.microsoft.com/en-us/typography/opentype/spec/cpal>
6

            
7
use crate::binary::read::{ReadArray, ReadBinary, ReadCtxt, ReadFrom};
8
use crate::binary::{U16Be, U32Be, U8};
9
use crate::error::ParseError;
10
use crate::SafeFrom;
11

            
12
/// `CPAL` — Color Palette Table
13
pub struct CpalTable<'a> {
14
    /// Table version number.
15
    pub version: u16,
16
    /// Number of palette entries in each palette.
17
    num_palette_entries: u16,
18
    /// Color records for all palettes.
19
    color_records_array: ReadArray<'a, ColorRecord>,
20
    /// Index of each palette’s first color record in the combined color record array.
21
    color_record_indices: ReadArray<'a, U16Be>,
22
    /// Palette Types Array.
23
    palette_types_array: Option<ReadArray<'a, U32Be>>,
24
    /// Palette Labels Array.
25
    palette_labels_array: Option<ReadArray<'a, U16Be>>,
26
    /// Palette Entry Labels Array.
27
    palette_entry_labels_array: Option<ReadArray<'a, U16Be>>,
28
}
29

            
30
/// Flags describing features of a palette.
31
#[enumflags2::bitflags]
32
#[repr(u32)]
33
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
34
#[allow(non_camel_case_types)]
35
pub enum PaletteFlag {
36
    /// Palette is appropriate to use when displaying the font on a light background such as white.
37
    USABLE_WITH_LIGHT_BACKGROUND = 0b00000001,
38
    /// Palette is appropriate to use when displaying the font on a dark background such as black.
39
    USABLE_WITH_DARK_BACKGROUND = 0b00000010,
40
}
41

            
42
/// A set of `PaletteFlag` flags.
43
pub type PaletteFlags = enumflags2::BitFlags<PaletteFlag>;
44

            
45
impl<'data> CpalTable<'data> {
46
    /// Obtain the palette at `index`.
47
    ///
48
    /// > The first palette, palette index 0, is the default palette.
49
    /// > A minimum of one palette must be provided in the `CPAL` table if the table is present.
50
    /// > Palettes must have a minimum of one color record.
51
    pub fn palette<'a>(&'a self, index: u16) -> Option<Palette<'a, 'data>> {
52
        let base_index = self.color_record_indices.get_item(usize::from(index))?;
53
        Some(Palette {
54
            cpal: self,
55
            index,
56
            base_index,
57
        })
58
    }
59

            
60
    /// Id of an entry in the [NameTable][crate::tables::NameTable] that
61
    /// provides a user-interface associated with each palette entry.
62
    ///
63
    /// If the palette entry does not have a label, `None` is returned.
64
    pub fn entry_label(&self, entry_index: u16) -> Option<u16> {
65
        // 0xFFFF indicates there is no string for a particular palette entry
66
        self.palette_entry_labels_array
67
            .as_ref()
68
            .and_then(|labels| labels.get_item(usize::from(entry_index)))
69
            .filter(|name_id| *name_id != 0xFFFF)
70
    }
71
}
72

            
73
impl ReadBinary for CpalTable<'_> {
74
    type HostType<'a> = CpalTable<'a>;
75

            
76
    fn read<'a>(ctxt: &mut ReadCtxt<'a>) -> Result<Self::HostType<'a>, ParseError> {
77
        let start = ctxt.scope();
78
        let version = ctxt.read_u16be()?;
79
        // Number of palette entries in each palette.
80
        // Palettes must have a minimum of one color record.
81
        let num_palette_entries = ctxt.read_u16be()?;
82
        ctxt.check(num_palette_entries > 0)?;
83
        let num_palettes = ctxt.read_u16be()?;
84
        // A minimum of one palette must be provided in the CPAL table if the table is present.
85
        ctxt.check(num_palettes > 0)?;
86
        let num_color_records = ctxt.read_u16be()?;
87
        let color_records_array_offset = ctxt.read_u32be()?;
88
        // Multiple colorRecordIndices may refer to the same color record, in which case multiple
89
        // palettes would use the same color records
90
        let color_record_indices = ctxt.read_array(usize::from(num_palettes))?;
91
        let color_records_array = start
92
            .offset(usize::safe_from(color_records_array_offset))
93
            .ctxt()
94
            .read_array(usize::from(num_color_records))?;
95

            
96
        let (
97
            palette_types_array_offset,
98
            palette_labels_array_offset,
99
            palette_entry_labels_array_offset,
100
        ) = if version == 1 {
101
            let palette_types_array_offset = ctxt.read_u32be()?;
102
            let palette_labels_array_offset = ctxt.read_u32be()?;
103
            let palette_entry_labels_array_offset = ctxt.read_u32be()?;
104
            (
105
                palette_types_array_offset,
106
                palette_labels_array_offset,
107
                palette_entry_labels_array_offset,
108
            )
109
        } else {
110
            (0, 0, 0)
111
        };
112

            
113
        let palette_types_array =
114
            start.read_optional_array(palette_types_array_offset, num_palettes)?;
115

            
116
        let palette_labels_array =
117
            start.read_optional_array(palette_labels_array_offset, num_palettes)?;
118

            
119
        let palette_entry_labels_array =
120
            start.read_optional_array(palette_entry_labels_array_offset, num_palette_entries)?;
121

            
122
        Ok(CpalTable {
123
            version,
124
            num_palette_entries,
125
            color_records_array,
126
            color_record_indices,
127
            palette_types_array,
128
            palette_labels_array,
129
            palette_entry_labels_array,
130
        })
131
    }
132
}
133

            
134
/// A `CPAL` palette.
135
#[derive(Copy, Clone)]
136
pub struct Palette<'a, 'data> {
137
    cpal: &'a CpalTable<'data>,
138
    /// Palette index of this palette.
139
    index: u16,
140
    /// Base index in the first color record in the color record array for this palette.
141
    base_index: u16,
142
}
143

            
144
impl Palette<'_, '_> {
145
    /// Retrieve the color record at `index` in this palette.
146
    pub fn color(&self, index: u16) -> Option<ColorRecord> {
147
        // TODO: A palette entry index value of 0xFFFF is a special case
148
        // indicating that the text foreground color (defined by the application) should be used,
149
        // and must not be treated as an actual index into the CPAL ColorRecord array.
150
        if index == 0xFFFF {
151
            return Some(ColorRecord {
152
                blue: 0,
153
                green: 0,
154
                red: 0,
155
                alpha: u8::MAX,
156
            });
157
        } else if index >= self.cpal.num_palette_entries {
158
            return None;
159
        }
160

            
161
        let color_index = u32::from(self.base_index) + u32::from(index);
162
        self.cpal
163
            .color_records_array
164
            .get_item(usize::safe_from(color_index))
165
    }
166

            
167
    /// Returns the id of an entry in the [NameTable][crate::tables::NameTable] that
168
    /// provides a user-interface string for the palette.
169
    ///
170
    /// If the palette does not have a label, `None` is returned.
171
    pub fn label(&self) -> Option<u16> {
172
        // 0xFFFF indicates there is no string for a particular palette
173
        self.cpal
174
            .palette_labels_array
175
            .as_ref()
176
            .and_then(|labels| labels.get_item(usize::from(self.index)))
177
            .filter(|name_id| *name_id != 0xFFFF)
178
    }
179

            
180
    /// Retrieve the flags for this palette.
181
    ///
182
    /// **Note:** The USABLE_WITH_LIGHT_BACKGROUND and USABLE_WITH_DARK_BACKGROUND flags
183
    /// are not mutually exclusive: they may both be set.
184
    pub fn flags(&self) -> PaletteFlags {
185
        self.cpal
186
            .palette_types_array
187
            .as_ref()
188
            .and_then(|types| types.get_item(usize::from(self.index)))
189
            .map(PaletteFlags::from_bits_truncate)
190
            .unwrap_or(PaletteFlags::empty())
191
    }
192
}
193

            
194
/// A BGRA color record.
195
#[derive(Debug, Copy, Clone)]
196
pub struct ColorRecord {
197
    /// Blue value (B0).
198
    pub blue: u8,
199
    /// Green value (B1).
200
    pub green: u8,
201
    /// Red value (B2).
202
    pub red: u8,
203
    /// Alpha value (B3).
204
    pub alpha: u8,
205
}
206

            
207
impl ReadFrom for ColorRecord {
208
    type ReadType = (U8, U8, U8, U8);
209

            
210
    fn read_from((blue, green, red, alpha): (u8, u8, u8, u8)) -> Self {
211
        ColorRecord {
212
            blue,
213
            green,
214
            red,
215
            alpha,
216
        }
217
    }
218
}
219

            
220
#[cfg(test)]
221
mod tests {
222
    use super::*;
223
    use crate::{
224
        binary::read::ReadScope,
225
        tables::{FontTableProvider, OpenTypeFont},
226
        tag,
227
        tests::read_fixture,
228
    };
229

            
230
    #[test]
231
    fn test_read_cpal_v1_variable() {
232
        let buffer = read_fixture(
233
            "tests/fonts/colr/SixtyfourConvergence-Regular-VariableFont_BLED,SCAN,XELA,YELA.ttf",
234
        );
235
        let otf = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
236
        let table_provider = otf.table_provider(0).expect("error reading font file");
237

            
238
        let cpal_data = table_provider
239
            .read_table_data(tag::CPAL)
240
            .expect("unable to read CPAL data");
241
        let cpal = ReadScope::new(&cpal_data)
242
            .read::<CpalTable<'_>>()
243
            .expect("unable to parse CPAL table");
244

            
245
        assert_eq!(cpal.version, 1);
246
        assert_eq!(cpal.num_palette_entries, 6);
247
        assert_eq!(cpal.color_records_array.len(), 12);
248
        assert_eq!(cpal.color_record_indices.len(), 2);
249
        assert_eq!(
250
            cpal.palette_types_array.as_ref().map(|map| map.len()),
251
            Some(2)
252
        );
253
        assert!(cpal.palette_labels_array.is_none());
254
        assert!(cpal.palette_entry_labels_array.is_none());
255
    }
256
}