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: u32 = u32::from_be_bytes(*b"CPAL");
40
41/// Color Table.
42const COLR: u32 = u32::from_be_bytes(*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: &'a [u8],
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: &[],
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 fn new<'a>(font: ttf_parser::RawFace<'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: ttf_parser::RawFace<'a>) -> std::io::Result<ColorPalettes<'a>> {
85        let table = match font.table(ttf_parser::Tag(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[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[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 = BigEndian::read_u16(&self.table[self.color_record_indices_offset as usize + i as usize * 2..]) as usize * 4;
213
214            let start = self.color_records_array_offset as usize + byte_i;
215            let palette_len = self.num_palette_entries as usize * 4;
216            Some(ColorPalette {
217                table: &self.table[start..start + palette_len],
218                flags: self.index_palette_type(i),
219            })
220        } else {
221            None
222        }
223    }
224
225    /// Iterate over color palettes.
226    pub fn iter(&self) -> impl ExactSizeIterator<Item = ColorPalette<'_>> {
227        (0..self.num_palettes).map(|i| self.palette_get(i).unwrap())
228    }
229}
230
231bitflags! {
232    /// Represents a color palette v1 flag.
233    ///
234    /// See [`ColorPalettes`] for more details.
235    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
236    pub struct ColorPaletteType: u32 {
237        /// Palette is appropriate to use when displaying the font on a light background such as white.
238        const USABLE_WITH_LIGHT_BACKGROUND = 0x0001;
239        /// Palette is appropriate to use when displaying the font on a dark background such as black.
240        const USABLE_WITH_DARK_BACKGROUND = 0x0002;
241    }
242}
243
244/// Represents a color palette entry.
245///
246/// See [`ColorPalettes`] for more details.
247#[non_exhaustive]
248pub struct ColorPalette<'a> {
249    table: &'a [u8],
250    flags: ColorPaletteType,
251}
252impl<'a> ColorPalette<'a> {
253    /// Number of colors in palette.
254    ///
255    /// This is never 0.
256    #[allow(clippy::len_without_is_empty)]
257    pub fn len(&self) -> u16 {
258        (self.table.len() / 4) as u16
259    }
260
261    /// Get the color at `i`.
262    pub fn index(&self, i: u16) -> Rgba {
263        let i = i as usize * 4;
264        let b = self.table[i];
265        let g = self.table[i + 1];
266        let r = self.table[i + 2];
267        let a = self.table[i + 3];
268        Rgba::new(r, g, b, a)
269    }
270
271    /// Get the color at `i`, if `i` is within bounds.
272    pub fn get(&self, i: u16) -> Option<Rgba> {
273        if i < self.len() { Some(self.index(i)) } else { None }
274    }
275
276    /// Iterate over colors.
277    pub fn iter(&self) -> impl ExactSizeIterator<Item = Rgba> + '_ {
278        (0..self.len()).map(|i| self.index(i))
279    }
280
281    /// Palette v1 flags.
282    pub fn flags(&self) -> ColorPaletteType {
283        self.flags
284    }
285}
286
287/// COLR table.
288///
289/// The color glyphs for a font are available in [`FontFace::color_glyphs`].
290///
291/// [`FontFace::color_glyphs`]: crate::FontFace::color_glyphs
292#[derive(Clone, Copy)]
293pub struct ColorGlyphs<'a> {
294    table: &'a [u8],
295    num_base_glyph_records: u16,
296    base_glyph_records_offset: u32,
297    layer_records_offset: u32,
298}
299impl ColorGlyphs<'static> {
300    /// No color glyphs.
301    pub fn empty() -> Self {
302        Self {
303            table: &[],
304            num_base_glyph_records: 0,
305            base_glyph_records_offset: 0,
306            layer_records_offset: 0,
307        }
308    }
309
310    /// New from font.
311    ///
312    /// Color glyphs are parsed on demand.
313    pub fn new<'a>(font: ttf_parser::RawFace<'a>) -> ColorGlyphs<'a> {
314        match Self::new_impl(font) {
315            Ok(g) => g,
316            Err(e) => {
317                tracing::error!("error parsing color glyphs, {e}");
318                Self::empty()
319            }
320        }
321    }
322    fn new_impl<'a>(font: ttf_parser::RawFace<'a>) -> std::io::Result<ColorGlyphs<'a>> {
323        let table = match font.table(ttf_parser::Tag(COLR)) {
324            Some(t) => t,
325            None => return Ok(Self::empty()),
326        };
327
328        /*
329        https://learn.microsoft.com/en-us/typography/opentype/spec/colr#colr-formats
330        COLR version 0
331
332        Type 	 Name 	                Description
333        uint16 	 version 	            Table version number—set to 0.
334        uint16   numBaseGlyphRecords 	Number of BaseGlyph records.
335        Offset32 baseGlyphRecordsOffset	Offset to baseGlyphRecords array.
336        Offset32 layerRecordsOffset 	Offset to layerRecords array.
337        uint16 	 numLayerRecords 	    Number of Layer records.
338        */
339
340        let mut cursor = std::io::Cursor::new(table);
341
342        let _version = cursor.read_u16::<BigEndian>()?;
343        let num_base_glyph_records = cursor.read_u16::<BigEndian>()?;
344        let base_glyph_records_offset = cursor.read_u32::<BigEndian>()?;
345        let layer_records_offset = cursor.read_u32::<BigEndian>()?;
346
347        Ok(ColorGlyphs {
348            table,
349            num_base_glyph_records,
350            base_glyph_records_offset,
351            layer_records_offset,
352        })
353    }
354}
355impl<'a> ColorGlyphs<'a> {
356    /// If the font does not have any colored glyphs.
357    pub fn is_empty(&self) -> bool {
358        self.num_base_glyph_records == 0
359    }
360
361    /// Number of base glyphs that have colored replacements.
362    pub fn len(&self) -> u16 {
363        self.num_base_glyph_records
364    }
365
366    /// Gets the color glyph layers that replace the `base_glyph` to render in color.
367    ///
368    /// The `base_glyph` is the glyph selected by the font during shaping.
369    ///
370    /// Returns a [`ColorGlyph`] that provides the colored glyphs from the back (first item) to the front (last item).
371    /// Paired with each glyph is an index in the font's [`ColorPalette`] or `None` if the base text color must be used.
372    ///
373    /// Returns ``None  if the `base_glyph` has no associated colored replacements.
374    pub fn glyph(&self, base_glyph: GlyphIndex) -> Option<ColorGlyph<'a>> {
375        if self.is_empty() {
376            return None;
377        }
378
379        let (first_layer_index, num_layers) = self.find_base_glyph(base_glyph)?;
380
381        let record_size = 4;
382        let table = &self.table[self.layer_records_offset as usize + (first_layer_index as usize * record_size)..];
383        Some(ColorGlyph { table, num_layers })
384    }
385
386    /// Returns (firstLayerIndex, numLayers)
387    fn find_base_glyph(&self, base_glyph: GlyphIndex) -> Option<(u16, u16)> {
388        /*
389        https://learn.microsoft.com/en-us/typography/opentype/spec/colr#baseglyph-and-layer-records
390
391        BaseGlyph record:
392
393        Type   Name            Description
394        uint16 glyphID         Glyph ID of the base glyph.
395        uint16 firstLayerIndex Index (base 0) into the layerRecords array.
396        uint16 numLayers       Number of color layers associated with this glyph.
397        */
398
399        let base_glyph: u16 = base_glyph.try_into().ok()?;
400
401        let record_size = 6;
402        let base = self.base_glyph_records_offset as usize;
403
404        let mut left = 0;
405        let mut right = self.num_base_glyph_records as isize - 1;
406
407        while left <= right {
408            let mid = (left + right) / 2;
409            let pos = base + mid as usize * record_size;
410
411            // Safety: ensure within bounds
412            if pos + record_size > self.table.len() {
413                return None;
414            }
415
416            let glyph_id = BigEndian::read_u16(&self.table[pos..pos + 2]);
417            match glyph_id.cmp(&base_glyph) {
418                std::cmp::Ordering::Equal => {
419                    let first_layer_index = BigEndian::read_u16(&self.table[pos + 2..pos + 4]);
420                    let num_layers = BigEndian::read_u16(&self.table[pos + 4..pos + 6]);
421                    return if num_layers > 0 {
422                        Some((first_layer_index, num_layers))
423                    } else {
424                        None
425                    };
426                }
427                std::cmp::Ordering::Less => left = mid + 1,
428                std::cmp::Ordering::Greater => right = mid - 1,
429            }
430        }
431
432        None
433    }
434}
435
436/// Color glyph layers.
437///
438/// See [`ColorGlyphs::glyph`] for more details.
439#[derive(Clone, Copy)]
440pub struct ColorGlyph<'a> {
441    table: &'a [u8],
442    num_layers: u16,
443}
444impl<'a> ColorGlyph<'a> {
445    /// Number of layers.
446    ///
447    /// This is always a non zero value as the [`ColorGlyphs`] returns `None` if there are no colored glyph replacements.
448    #[allow(clippy::len_without_is_empty)]
449    pub fn len(&self) -> u16 {
450        self.num_layers
451    }
452
453    /// Get the layer.
454    ///
455    /// # Panics
456    ///
457    /// Panics if `layer` is out of bounds.
458    pub fn index(&self, layer: u16) -> (GlyphIndex, Option<u16>) {
459        /*
460        Layer record:
461
462        Type   Name 	    Description
463        uint16 glyphID      Glyph ID of the glyph used for a given layer.
464        uint16 paletteIndex Index (base 0) for a palette entry in the CPAL table.
465        */
466        let t = &self.table[layer as usize * 4..];
467        let glyph_id = BigEndian::read_u16(t);
468        let pallet_index = BigEndian::read_u16(&t[2..]);
469        if pallet_index == 0xFFFF {
470            (glyph_id as _, None)
471        } else {
472            (glyph_id as _, Some(pallet_index))
473        }
474    }
475
476    /// Iterate over layers, back to front.
477    pub fn iter(&self) -> impl ExactSizeIterator<Item = (GlyphIndex, Option<u16>)> + '_ {
478        (0..self.num_layers).map(move |i| self.index(i))
479    }
480}
481
482/// Color palette selector for colored fonts.
483#[derive(Clone, Copy, PartialEq, Eq)]
484pub enum FontColorPalette {
485    /// Select first font palette tagged [`ColorPaletteType::USABLE_WITH_LIGHT_BACKGROUND`], or 0 if the
486    /// font does not tag any palette or no match is found.
487    Light,
488    /// Select first font palette tagged [`ColorPaletteType::USABLE_WITH_DARK_BACKGROUND`], or 0 if the
489    /// font does not tag any palette or no match is found.
490    Dark,
491    /// Select one of the font provided palette by index.
492    ///
493    /// The palette list of a font is available in [`FontFace::color_palettes`]. If the index
494    /// is not found uses the first font palette.
495    ///
496    /// [`FontFace::color_palettes`]: crate::FontFace::color_palettes
497    Index(u16),
498}
499impl fmt::Debug for FontColorPalette {
500    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
501        if f.alternate() {
502            write!(f, "FontColorPalette::")?;
503        }
504        match self {
505            Self::Light => write!(f, "Light"),
506            Self::Dark => write!(f, "Dark"),
507            Self::Index(arg0) => f.debug_tuple("Index").field(arg0).finish(),
508        }
509    }
510}
511impl_from_and_into_var! {
512    fn from(index: u16) -> FontColorPalette {
513        FontColorPalette::Index(index)
514    }
515
516    fn from(color_scheme: ColorScheme) -> FontColorPalette {
517        match color_scheme {
518            ColorScheme::Light => FontColorPalette::Light,
519            ColorScheme::Dark => FontColorPalette::Dark,
520            _ => FontColorPalette::Light,
521        }
522    }
523}