Skip to main content

zng_ext_font/
emoji_util.rs

1/*
2Loaded data is !Send+!Sync so we probably don't need to cache it.
3*/
4
5use std::{fmt, mem::size_of};
6
7use byteorder::{BigEndian, ByteOrder as _, ReadBytesExt};
8use icu_properties::props::{self, BinaryProperty};
9use zng_color::{ColorScheme, Rgba};
10use zng_var::impl_from_and_into_var;
11use zng_view_api::font::GlyphIndex;
12
13pub(super) fn maybe_emoji(c: char) -> bool {
14    props::Emoji::for_char(c)
15}
16
17pub(super) fn definitely_emoji(c: char) -> bool {
18    props::EmojiPresentation::for_char(c) || is_modifier(c)
19}
20
21pub(super) fn is_modifier(c: char) -> bool {
22    props::EmojiModifier::for_char(c)
23}
24
25pub(super) fn is_component(c: char) -> bool {
26    props::EmojiComponent::for_char(c)
27}
28
29/*
30https://learn.microsoft.com/en-us/typography/opentype/spec/otff
31All OpenType fonts use Motorola-style byte ordering (Big Endian)
32
33Offset32 = uint32
34 */
35
36// OpenType is Big Endian, table IDs are their ASCII name (4 chars) as an `u32`.
37
38/// Color Palette Table
39const CPAL: skrifa::Tag = skrifa::Tag::new(b"CPAL");
40
41/// Color Table.
42const COLR: skrifa::Tag = skrifa::Tag::new(b"COLR");
43
44/// CPAL table.
45///
46/// The palettes for a font are available in [`FontFace::color_palettes`].
47///
48/// [`FontFace::color_palettes`]: crate::FontFace::color_palettes
49#[derive(Clone, Copy)]
50pub struct ColorPalettes<'a> {
51    table: read_fonts::FontData<'a>,
52    num_palettes: u16,
53    num_palette_entries: u16,
54    color_records_array_offset: u32,
55    color_record_indices_offset: u32,
56    /// is `0` for version 0
57    palette_types_array_offset: u32,
58}
59impl ColorPalettes<'static> {
60    /// No color palettes.
61    pub fn empty() -> Self {
62        Self {
63            table: read_fonts::FontData::new(&[]),
64            num_palettes: 0,
65            num_palette_entries: 0,
66            color_records_array_offset: 0,
67            color_record_indices_offset: 0,
68            palette_types_array_offset: 0,
69        }
70    }
71
72    /// New from font.
73    ///
74    /// Palettes are parsed on demand.
75    pub(crate) fn new<'a>(font: read_fonts::FontRef<'a>) -> ColorPalettes<'a> {
76        match Self::new_impl(font) {
77            Ok(g) => g,
78            Err(e) => {
79                tracing::error!("error parsing color palettes, {e}");
80                Self::empty()
81            }
82        }
83    }
84    fn new_impl<'a>(font: read_fonts::FontRef<'a>) -> std::io::Result<ColorPalettes<'a>> {
85        let table = match font.table_data(CPAL) {
86            Some(t) => t,
87            None => return Ok(Self::empty()),
88        };
89        /*
90        https://learn.microsoft.com/en-us/typography/opentype/spec/cpal
91        CPAL version 0
92
93        The CPAL header version 0 is organized as follows:
94        Type 	 Name 	                        Description
95        uint16 	 version 	                    Table version number (=0).
96        uint16 	 numPaletteEntries 	            Number of palette entries in each palette.
97        uint16 	 numPalettes 	                Number of palettes in the table.
98        uint16 	 numColorRecords 	            Total number of color records, combined for all palettes.
99        Offset32 colorRecordsArrayOffset 	    Offset from the beginning of CPAL table to the first ColorRecord.
100        uint16 	 colorRecordIndices[numPalettes] Index of each palette’s first color record in the combined color record array.
101         */
102
103        let mut cursor = std::io::Cursor::new(&table);
104
105        let version = cursor.read_u16::<BigEndian>()?;
106        let num_palette_entries = cursor.read_u16::<BigEndian>()?;
107        let num_palettes = cursor.read_u16::<BigEndian>()?;
108        let _num_color_records = cursor.read_u16::<BigEndian>()?;
109        let color_records_array_offset = cursor.read_u32::<BigEndian>()?;
110
111        let mut palette_types_array_offset = 0;
112        let color_record_indices = cursor.position();
113        if version >= 1 {
114            cursor.set_position(color_record_indices + num_palettes as u64 * size_of::<u16>() as u64);
115
116            /*
117            CPAL version 1
118
119            {version..colorRecordIndices[numPalettes]}
120
121            Offset32 paletteTypesArrayOffset 	   Offset from the beginning of CPAL table to the Palette Types Array. Set to 0 if no array is provided.
122            Offset32 paletteLabelsArrayOffset 	   Offset from the beginning of CPAL table to the Palette Labels Array. Set to 0 if no array is provided.
123            Offset32 paletteEntryLabelsArrayOffset Offset from the beginning of CPAL table to the Palette Entry Labels Array. Set to 0 if no array is provided.
124            */
125            palette_types_array_offset = cursor.read_u32::<BigEndian>()?;
126            let _palette_labels_array_offset = cursor.read_u32::<BigEndian>()? as u64;
127            let _palette_entry_labels_array_offset = cursor.read_u32::<BigEndian>()? as u64;
128        }
129
130        Ok(ColorPalettes {
131            table,
132            num_palettes,
133            num_palette_entries,
134            color_record_indices_offset: color_record_indices as u32,
135            color_records_array_offset,
136            palette_types_array_offset,
137        })
138    }
139}
140impl<'a> ColorPalettes<'a> {
141    /// Number of palettes.
142    pub fn len(&self) -> u16 {
143        self.num_palettes
144    }
145
146    /// If the font does not have any color palette.
147    pub fn is_empty(&self) -> bool {
148        self.num_palettes == 0
149    }
150
151    /// Gets the requested palette or the first if it is not found.
152    pub fn palette(&self, p: impl Into<FontColorPalette>) -> Option<ColorPalette<'a>> {
153        let i = self.palette_i(p.into());
154        self.palette_get(i.unwrap_or(0))
155    }
156
157    /// Gets the requested palette.
158    pub fn palette_exact(&self, p: impl Into<FontColorPalette>) -> Option<ColorPalette<'a>> {
159        let i = self.palette_i(p.into())?;
160        self.palette_get(i)
161    }
162
163    fn palette_types_iter(&self) -> impl Iterator<Item = ColorPaletteType> + 'a {
164        let mut cursor = std::io::Cursor::new(&self.table.as_bytes()[self.palette_types_array_offset as usize..]);
165        let mut i = if self.palette_types_array_offset > 0 {
166            self.num_palettes
167        } else {
168            0
169        };
170        std::iter::from_fn(move || {
171            if i > 0 {
172                i -= 1;
173                let flags = cursor.read_u32::<BigEndian>().ok()?;
174                Some(ColorPaletteType::from_bits_retain(flags))
175            } else {
176                None
177            }
178        })
179    }
180
181    fn palette_i(&self, p: FontColorPalette) -> Option<u16> {
182        match p {
183            FontColorPalette::Light => self
184                .palette_types_iter()
185                .position(|p| p.contains(ColorPaletteType::USABLE_WITH_LIGHT_BACKGROUND))
186                .map(|i| i as u16),
187            FontColorPalette::Dark => self
188                .palette_types_iter()
189                .position(|p| p.contains(ColorPaletteType::USABLE_WITH_DARK_BACKGROUND))
190                .map(|i| i as u16),
191            FontColorPalette::Index(i) => {
192                if i < self.num_palettes {
193                    Some(i as _)
194                } else {
195                    None
196                }
197            }
198        }
199    }
200
201    fn index_palette_type(&self, i: u16) -> ColorPaletteType {
202        if self.palette_types_array_offset == 0 || i >= self.num_palettes {
203            return ColorPaletteType::empty();
204        }
205        let t = &self.table.as_ref()[self.palette_types_array_offset as usize + i as usize * 4..];
206        let flags = BigEndian::read_u32(t);
207        ColorPaletteType::from_bits_retain(flags)
208    }
209
210    fn palette_get(&self, i: u16) -> Option<ColorPalette<'a>> {
211        if i < self.num_palettes {
212            let byte_i =
213                BigEndian::read_u16(&self.table.as_bytes()[self.color_record_indices_offset as usize + i as usize * 2..]) as usize * 4;
214
215            let start = self.color_records_array_offset as usize + byte_i;
216            let palette_len = self.num_palette_entries as usize * 4;
217            Some(ColorPalette {
218                table: &self.table.as_bytes()[start..start + palette_len],
219                flags: self.index_palette_type(i),
220            })
221        } else {
222            None
223        }
224    }
225
226    /// Iterate over color palettes.
227    pub fn iter(&self) -> impl ExactSizeIterator<Item = ColorPalette<'_>> {
228        (0..self.num_palettes).map(|i| self.palette_get(i).unwrap())
229    }
230}
231
232bitflags! {
233    /// Represents a color palette v1 flag.
234    ///
235    /// See [`ColorPalettes`] for more details.
236    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
237    pub struct ColorPaletteType: u32 {
238        /// Palette is appropriate to use when displaying the font on a light background such as white.
239        const USABLE_WITH_LIGHT_BACKGROUND = 0x0001;
240        /// Palette is appropriate to use when displaying the font on a dark background such as black.
241        const USABLE_WITH_DARK_BACKGROUND = 0x0002;
242    }
243}
244
245/// Represents a color palette entry.
246///
247/// See [`ColorPalettes`] for more details.
248#[non_exhaustive]
249pub struct ColorPalette<'a> {
250    table: &'a [u8],
251    flags: ColorPaletteType,
252}
253impl<'a> ColorPalette<'a> {
254    /// Number of colors in palette.
255    ///
256    /// This is never 0.
257    #[allow(clippy::len_without_is_empty)]
258    pub fn len(&self) -> u16 {
259        (self.table.len() / 4) as u16
260    }
261
262    /// Get the color at `i`.
263    pub fn index(&self, i: u16) -> Rgba {
264        let i = i as usize * 4;
265        let b = self.table[i];
266        let g = self.table[i + 1];
267        let r = self.table[i + 2];
268        let a = self.table[i + 3];
269        Rgba::new(r, g, b, a)
270    }
271
272    /// Get the color at `i`, if `i` is within bounds.
273    pub fn get(&self, i: u16) -> Option<Rgba> {
274        if i < self.len() { Some(self.index(i)) } else { None }
275    }
276
277    /// Iterate over colors.
278    pub fn iter(&self) -> impl ExactSizeIterator<Item = Rgba> + '_ {
279        (0..self.len()).map(|i| self.index(i))
280    }
281
282    /// Palette v1 flags.
283    pub fn flags(&self) -> ColorPaletteType {
284        self.flags
285    }
286}
287
288/// COLR table.
289///
290/// The color glyphs for a font are available in [`FontFace::color_glyphs`].
291///
292/// [`FontFace::color_glyphs`]: crate::FontFace::color_glyphs
293#[derive(Clone, Copy)]
294pub struct ColorGlyphs<'a> {
295    table: read_fonts::FontData<'a>,
296    num_base_glyph_records: u16,
297    base_glyph_records_offset: u32,
298    layer_records_offset: u32,
299}
300impl ColorGlyphs<'static> {
301    /// No color glyphs.
302    pub fn empty() -> Self {
303        Self {
304            table: read_fonts::FontData::new(&[]),
305            num_base_glyph_records: 0,
306            base_glyph_records_offset: 0,
307            layer_records_offset: 0,
308        }
309    }
310
311    /// New from font.
312    ///
313    /// Color glyphs are parsed on demand.
314    pub(crate) fn new<'a>(font: read_fonts::FontRef<'a>) -> ColorGlyphs<'a> {
315        match Self::new_impl(font) {
316            Ok(g) => g,
317            Err(e) => {
318                tracing::error!("error parsing color glyphs, {e}");
319                Self::empty()
320            }
321        }
322    }
323    fn new_impl<'a>(font: read_fonts::FontRef<'a>) -> std::io::Result<ColorGlyphs<'a>> {
324        let table = match font.table_data(COLR) {
325            Some(t) => t,
326            None => return Ok(Self::empty()),
327        };
328
329        /*
330        https://learn.microsoft.com/en-us/typography/opentype/spec/colr#colr-formats
331        COLR version 0
332
333        Type 	 Name 	                Description
334        uint16 	 version 	            Table version number—set to 0.
335        uint16   numBaseGlyphRecords 	Number of BaseGlyph records.
336        Offset32 baseGlyphRecordsOffset	Offset to baseGlyphRecords array.
337        Offset32 layerRecordsOffset 	Offset to layerRecords array.
338        uint16 	 numLayerRecords 	    Number of Layer records.
339        */
340
341        let mut cursor = std::io::Cursor::new(table);
342
343        let _version = cursor.read_u16::<BigEndian>()?;
344        let num_base_glyph_records = cursor.read_u16::<BigEndian>()?;
345        let base_glyph_records_offset = cursor.read_u32::<BigEndian>()?;
346        let layer_records_offset = cursor.read_u32::<BigEndian>()?;
347
348        Ok(ColorGlyphs {
349            table,
350            num_base_glyph_records,
351            base_glyph_records_offset,
352            layer_records_offset,
353        })
354    }
355}
356impl<'a> ColorGlyphs<'a> {
357    /// If the font does not have any colored glyphs.
358    pub fn is_empty(&self) -> bool {
359        self.num_base_glyph_records == 0
360    }
361
362    /// Number of base glyphs that have colored replacements.
363    pub fn len(&self) -> u16 {
364        self.num_base_glyph_records
365    }
366
367    /// Gets the color glyph layers that replace the `base_glyph` to render in color.
368    ///
369    /// The `base_glyph` is the glyph selected by the font during shaping.
370    ///
371    /// Returns a [`ColorGlyph`] that provides the colored glyphs from the back (first item) to the front (last item).
372    /// Paired with each glyph is an index in the font's [`ColorPalette`] or `None` if the base text color must be used.
373    ///
374    /// Returns ``None  if the `base_glyph` has no associated colored replacements.
375    pub fn glyph(&self, base_glyph: GlyphIndex) -> Option<ColorGlyph<'a>> {
376        if self.is_empty() {
377            return None;
378        }
379
380        let (first_layer_index, num_layers) = self.find_base_glyph(base_glyph)?;
381
382        let record_size = 4;
383        let table = &self.table.as_bytes()[self.layer_records_offset as usize + (first_layer_index as usize * record_size)..];
384        Some(ColorGlyph { table, num_layers })
385    }
386
387    /// Returns (firstLayerIndex, numLayers)
388    fn find_base_glyph(&self, base_glyph: GlyphIndex) -> Option<(u16, u16)> {
389        /*
390        https://learn.microsoft.com/en-us/typography/opentype/spec/colr#baseglyph-and-layer-records
391
392        BaseGlyph record:
393
394        Type   Name            Description
395        uint16 glyphID         Glyph ID of the base glyph.
396        uint16 firstLayerIndex Index (base 0) into the layerRecords array.
397        uint16 numLayers       Number of color layers associated with this glyph.
398        */
399
400        let base_glyph: u16 = base_glyph.try_into().ok()?;
401
402        let record_size = 6;
403        let base = self.base_glyph_records_offset as usize;
404
405        let mut left = 0;
406        let mut right = self.num_base_glyph_records as isize - 1;
407
408        while left <= right {
409            let mid = (left + right) / 2;
410            let pos = base + mid as usize * record_size;
411
412            // Safety: ensure within bounds
413            if pos + record_size > self.table.len() {
414                return None;
415            }
416
417            let glyph_id = BigEndian::read_u16(&self.table.as_ref()[pos..pos + 2]);
418            match glyph_id.cmp(&base_glyph) {
419                std::cmp::Ordering::Equal => {
420                    let first_layer_index = BigEndian::read_u16(&self.table.as_ref()[pos + 2..pos + 4]);
421                    let num_layers = BigEndian::read_u16(&self.table.as_ref()[pos + 4..pos + 6]);
422                    return if num_layers > 0 {
423                        Some((first_layer_index, num_layers))
424                    } else {
425                        None
426                    };
427                }
428                std::cmp::Ordering::Less => left = mid + 1,
429                std::cmp::Ordering::Greater => right = mid - 1,
430            }
431        }
432
433        None
434    }
435}
436
437/// Color glyph layers.
438///
439/// See [`ColorGlyphs::glyph`] for more details.
440#[derive(Clone, Copy)]
441pub struct ColorGlyph<'a> {
442    table: &'a [u8],
443    num_layers: u16,
444}
445impl<'a> ColorGlyph<'a> {
446    /// Number of layers.
447    ///
448    /// This is always a non zero value as the [`ColorGlyphs`] returns `None` if there are no colored glyph replacements.
449    #[allow(clippy::len_without_is_empty)]
450    pub fn len(&self) -> u16 {
451        self.num_layers
452    }
453
454    /// Get the layer.
455    ///
456    /// # Panics
457    ///
458    /// Panics if `layer` is out of bounds.
459    pub fn index(&self, layer: u16) -> (GlyphIndex, Option<u16>) {
460        /*
461        Layer record:
462
463        Type   Name 	    Description
464        uint16 glyphID      Glyph ID of the glyph used for a given layer.
465        uint16 paletteIndex Index (base 0) for a palette entry in the CPAL table.
466        */
467        let t = &self.table[layer as usize * 4..];
468        let glyph_id = BigEndian::read_u16(t);
469        let pallet_index = BigEndian::read_u16(&t[2..]);
470        if pallet_index == 0xFFFF {
471            (glyph_id as _, None)
472        } else {
473            (glyph_id as _, Some(pallet_index))
474        }
475    }
476
477    /// Iterate over layers, back to front.
478    pub fn iter(&self) -> impl ExactSizeIterator<Item = (GlyphIndex, Option<u16>)> + '_ {
479        (0..self.num_layers).map(move |i| self.index(i))
480    }
481}
482
483/// Color palette selector for colored fonts.
484#[derive(Clone, Copy, PartialEq, Eq)]
485pub enum FontColorPalette {
486    /// Select first font palette tagged [`ColorPaletteType::USABLE_WITH_LIGHT_BACKGROUND`], or 0 if the
487    /// font does not tag any palette or no match is found.
488    Light,
489    /// Select first font palette tagged [`ColorPaletteType::USABLE_WITH_DARK_BACKGROUND`], or 0 if the
490    /// font does not tag any palette or no match is found.
491    Dark,
492    /// Select one of the font provided palette by index.
493    ///
494    /// The palette list of a font is available in [`FontFace::color_palettes`]. If the index
495    /// is not found uses the first font palette.
496    ///
497    /// [`FontFace::color_palettes`]: crate::FontFace::color_palettes
498    Index(u16),
499}
500impl fmt::Debug for FontColorPalette {
501    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
502        if f.alternate() {
503            write!(f, "FontColorPalette::")?;
504        }
505        match self {
506            Self::Light => write!(f, "Light"),
507            Self::Dark => write!(f, "Dark"),
508            Self::Index(arg0) => f.debug_tuple("Index").field(arg0).finish(),
509        }
510    }
511}
512impl_from_and_into_var! {
513    fn from(index: u16) -> FontColorPalette {
514        FontColorPalette::Index(index)
515    }
516
517    fn from(color_scheme: ColorScheme) -> FontColorPalette {
518        match color_scheme {
519            ColorScheme::Light => FontColorPalette::Light,
520            ColorScheme::Dark => FontColorPalette::Dark,
521            _ => FontColorPalette::Light,
522        }
523    }
524}