Skip to content

Commit

Permalink
Add support for COLR/CPAL v0
Browse files Browse the repository at this point in the history
  • Loading branch information
laurmaedje committed Oct 3, 2023
1 parent e052dbb commit c5bd2c3
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
- `COLR` / `CPAL` v0 parsing.

## [0.19.2] - 2023-09-13
### Added
Expand Down
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -64,8 +64,8 @@ There are roughly three types of TrueType tables:
| `bloc` table ||| |
| `CBDT` table | ~ (no 8, 9) || |
| `CBLC` table ||| |
| `COLR` table | || |
| `CPAL` table | || |
| `COLR` table | ~ (only v0) || |
| `CPAL` table | ~ (only v0) || |
| `CFF ` table ||| ~ (no `seac` support) |
| `CFF2` table ||| |
| `cmap` table | ~ (no 8) || ~ (no 2,8,10,14; Unicode-only) |
Expand Down
47 changes: 46 additions & 1 deletion src/lib.rs
Expand Up @@ -71,6 +71,7 @@ mod var_store;
use head::IndexToLocationFormat;
pub use parser::{Fixed, FromData, LazyArray16, LazyArray32, LazyArrayIter16, LazyArrayIter32};
use parser::{NumFrom, Offset, Offset32, Stream, TryNumFrom};
use tables::colr::ColorGlyphPainter;

#[cfg(feature = "variable-fonts")]
pub use fvar::VariationAxis;
Expand All @@ -85,7 +86,7 @@ pub use tables::{ankr, feat, kerx, morx, trak};
pub use tables::{avar, cff2, fvar, gvar, hvar, mvar};
pub use tables::{cbdt, cblc, cff1 as cff, vhea};
pub use tables::{
cmap, glyf, head, hhea, hmtx, kern, loca, maxp, name, os2, post, sbix, svg, vorg,
cmap, colr, cpal, glyf, head, hhea, hmtx, kern, loca, maxp, name, os2, post, sbix, svg, vorg,
};
#[cfg(feature = "opentype-layout")]
pub use tables::{gdef, gpos, gsub, math};
Expand Down Expand Up @@ -749,6 +750,8 @@ pub struct RawFaceTables<'a> {
pub cblc: Option<&'a [u8]>,
pub cff: Option<&'a [u8]>,
pub cmap: Option<&'a [u8]>,
pub colr: Option<&'a [u8]>,
pub cpal: Option<&'a [u8]>,
pub ebdt: Option<&'a [u8]>,
pub eblc: Option<&'a [u8]>,
pub glyf: Option<&'a [u8]>,
Expand Down Expand Up @@ -820,6 +823,8 @@ pub struct FaceTables<'a> {
pub cbdt: Option<cbdt::Table<'a>>,
pub cff: Option<cff::Table<'a>>,
pub cmap: Option<cmap::Table<'a>>,
pub colr: Option<colr::Table<'a>>,
pub cpal: Option<cpal::Table<'a>>,
pub ebdt: Option<cbdt::Table<'a>>,
pub glyf: Option<glyf::Table<'a>>,
pub hmtx: Option<hmtx::Table<'a>>,
Expand Down Expand Up @@ -963,6 +968,8 @@ impl<'a> Face<'a> {
b"CFF " => tables.cff = table_data,
#[cfg(feature = "variable-fonts")]
b"CFF2" => tables.cff2 = table_data,
b"COLR" => tables.colr = table_data,
b"CPAL" => tables.cpal = table_data,
b"EBDT" => tables.ebdt = table_data,
b"EBLC" => tables.eblc = table_data,
#[cfg(feature = "opentype-layout")]
Expand Down Expand Up @@ -1094,6 +1101,15 @@ impl<'a> Face<'a> {
None
};

let cpal = raw_tables.cpal.and_then(cpal::Table::parse);
let colr = if let Some(cpal) = cpal {
raw_tables
.colr
.and_then(|data| colr::Table::parse(cpal, data))
} else {
None
};

Ok(FaceTables {
head,
hhea,
Expand All @@ -1103,6 +1119,8 @@ impl<'a> Face<'a> {
cbdt,
cff: raw_tables.cff.and_then(cff::Table::parse),
cmap: raw_tables.cmap.and_then(cmap::Table::parse),
colr,
cpal,
ebdt,
glyf,
hmtx,
Expand Down Expand Up @@ -1927,6 +1945,33 @@ impl<'a> Face<'a> {
None
}

/// Paints a color glyph from the COLR table.
///
/// **Warning**: since `ttf-parser` is a pull parser, `ColorGlyphPainter`
/// will emit instructions even when the glyph is partially malformed. You
/// must check `paint_color_glyph()` result before using
/// `ColorGlyphPainter`'s output.
///
/// A font can define a glyph using layers of colored shapes instead of a
/// simple outline. Which is primarily used for emojis. This method should
/// be used to access glyphs defines in the COLR table.
///
/// Also, a font can contain both: a layered definition and outlines. So
/// when this method returns `None` you should also try `outline_glyph()`
/// afterwards.
///
/// Returns `None` if the glyph has no COLR definition or if the glyph
/// definition is malformed.
#[inline]
pub fn paint_color_glyph(
&self,
glyph_id: GlyphId,
palette: u16,
painter: &mut dyn ColorGlyphPainter,
) -> Option<()> {
self.tables.colr?.paint(glyph_id, palette, painter)
}

/// Returns a tight glyph bounding box.
///
/// This is just a shorthand for `outline_glyph()` since only the `glyf` table stores
Expand Down
122 changes: 122 additions & 0 deletions src/tables/colr.rs
@@ -0,0 +1,122 @@
//! A [Color Table](
//! https://docs.microsoft.com/en-us/typography/opentype/spec/colr) implementation.

use crate::cpal::{self, Color};
use crate::parser::{FromData, LazyArray16, Offset, Offset32, Stream};
use crate::GlyphId;

/// A [base glyph](https://learn.microsoft.com/en-us/typography/opentype/spec/colr#baseglyph-and-layer-records).
#[derive(Clone, Copy, Debug)]
struct BaseGlyphRecord {
glyph_id: GlyphId,
first_layer_index: u16,
num_layers: u16,
}

impl FromData for BaseGlyphRecord {
const SIZE: usize = 6;

fn parse(data: &[u8]) -> Option<Self> {
let mut s = Stream::new(data);
Some(Self {
glyph_id: s.read::<GlyphId>()?,
first_layer_index: s.read::<u16>()?,
num_layers: s.read::<u16>()?,
})
}
}

/// A [layer](https://learn.microsoft.com/en-us/typography/opentype/spec/colr#baseglyph-and-layer-records).
#[derive(Clone, Copy, Debug)]
struct LayerRecord {
glyph_id: GlyphId,
palette_index: u16,
}

impl FromData for LayerRecord {
const SIZE: usize = 4;

fn parse(data: &[u8]) -> Option<Self> {
let mut s = Stream::new(data);
Some(Self {
glyph_id: s.read::<GlyphId>()?,
palette_index: s.read::<u16>()?,
})
}
}

/// A trait for color glyph painting.
pub trait ColorGlyphPainter {
/// Paints an outline glyph in the given color.
fn glyph(&mut self, id: GlyphId, color: Color);
}

/// A [Color Table](
/// https://docs.microsoft.com/en-us/typography/opentype/spec/colr).
///
/// Currently, only version 0 is supported.
#[derive(Clone, Copy, Debug)]
pub struct Table<'a> {
palettes: cpal::Table<'a>,
base_glyphs: LazyArray16<'a, BaseGlyphRecord>,
layers: LazyArray16<'a, LayerRecord>,
}

impl<'a> Table<'a> {
/// Parses a table from raw data.
pub fn parse(palettes: cpal::Table<'a>, data: &'a [u8]) -> Option<Self> {
let mut s = Stream::new(data);

let version = s.read::<u16>()?;
if version > 1 {
return None;
}

let num_base_glyphs = s.read::<u16>()?;
let base_glyphs_offset = s.read::<Offset32>()?;
let layers_offset = s.read::<Offset32>()?;
let num_layers = s.read::<u16>()?;

let base_glyphs = Stream::new_at(data, base_glyphs_offset.to_usize())?
.read_array16::<BaseGlyphRecord>(num_base_glyphs)?;

let layers = Stream::new_at(data, layers_offset.to_usize())?
.read_array16::<LayerRecord>(num_layers)?;

Some(Self {
palettes,
base_glyphs,
layers,
})
}

/// Whether the table contains a definition for the given glyph.
pub fn contains(&self, glyph_id: GlyphId) -> bool {
self.base_glyphs
.binary_search_by(|base| base.glyph_id.cmp(&glyph_id))
.is_some()
}

/// Paints the color glyph.
pub fn paint(
&self,
glyph_id: GlyphId,
palette: u16,
painter: &mut dyn ColorGlyphPainter,
) -> Option<()> {
let (_, base) = self
.base_glyphs
.binary_search_by(|base| base.glyph_id.cmp(&glyph_id))?;

let start = base.first_layer_index;
let end = start.checked_add(base.num_layers)?;
let layers = self.layers.slice(start..end)?;

for layer in layers {
let color = self.palettes.get(palette, layer.palette_index)?;
painter.glyph(layer.glyph_id, color);
}

Some(())
}
}
72 changes: 72 additions & 0 deletions src/tables/cpal.rs
@@ -0,0 +1,72 @@
//! A [Color Palette Table](
//! https://docs.microsoft.com/en-us/typography/opentype/spec/cpal) implementation.

use crate::parser::{FromData, LazyArray16, Offset, Offset32, Stream};

/// A [Color Palette Table](
/// https://docs.microsoft.com/en-us/typography/opentype/spec/cpal).
#[derive(Clone, Copy, Debug)]
pub struct Table<'a> {
color_indices: LazyArray16<'a, u16>,
colors: LazyArray16<'a, Color>,
}

impl<'a> Table<'a> {
/// Parses a table from raw data.
pub fn parse(data: &'a [u8]) -> Option<Self> {
let mut s = Stream::new(data);

let version = s.read::<u16>()?;
if version > 1 {
return None;
}

s.skip::<u16>(); // number of palette entries

let num_palettes = s.read::<u16>()?;
let num_colors = s.read::<u16>()?;
let color_records_offset = s.read::<Offset32>()?;
let color_indices = s.read_array16::<u16>(num_palettes)?;

let colors = Stream::new_at(data, color_records_offset.to_usize())?
.read_array16::<Color>(num_colors)?;

Some(Self {
color_indices,
colors,
})
}

/// Returns the color at the given index into the given palette.
pub fn get(&self, palette: u16, palette_entry: u16) -> Option<Color> {
let index = self
.color_indices
.get(palette)?
.checked_add(palette_entry)?;
self.colors.get(index)
}
}

/// A BGRA color in sRGB.
#[allow(missing_docs)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Color {
pub blue: u8,
pub green: u8,
pub red: u8,
pub alpha: u8,
}

impl FromData for Color {
const SIZE: usize = 4;

fn parse(data: &[u8]) -> Option<Self> {
let mut s = Stream::new(data);
Some(Self {
blue: s.read::<u8>()?,
green: s.read::<u8>()?,
red: s.read::<u8>()?,
alpha: s.read::<u8>()?,
})
}
}
2 changes: 2 additions & 0 deletions src/tables/mod.rs
Expand Up @@ -2,6 +2,8 @@ pub mod cbdt;
pub mod cblc;
mod cff;
pub mod cmap;
pub mod colr;
pub mod cpal;
pub mod glyf;
pub mod head;
pub mod hhea;
Expand Down

0 comments on commit c5bd2c3

Please sign in to comment.