mod instance;
mod transform;
mod traversal;
#[cfg(test)]
mod traversal_tests;
use raw::{
tables::{colr, cpal},
types::BigEndian,
FontRef,
};
#[cfg(test)]
use serde::{Deserialize, Serialize};
pub use fontcull_read_fonts::tables::colr::{CompositeMode, Extend};
use fontcull_read_fonts::{
types::{BoundingBox, GlyphId, Point},
ReadError, TableProvider,
};
#[doc(inline)]
pub use cpal::ColorRecord as Color;
use std::{fmt::Debug, ops::Range};
use traversal::{
get_clipbox_font_units, traverse_v0_range, traverse_with_callbacks, PaintDecycler,
};
pub use transform::Transform;
use crate::prelude::{LocationRef, Size};
use crate::string::StringId;
use self::instance::{resolve_paint, PaintId};
#[derive(Debug, Clone)]
pub enum PaintError {
ParseError(ReadError),
GlyphNotFound(GlyphId),
PaintCycleDetected,
DepthLimitExceeded,
}
impl std::fmt::Display for PaintError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
PaintError::ParseError(read_error) => {
write!(f, "Error parsing font data: {read_error}")
}
PaintError::GlyphNotFound(glyph_id) => {
write!(f, "No COLRv1 glyph found for glyph id: {glyph_id}")
}
PaintError::PaintCycleDetected => write!(f, "Paint cycle detected in COLRv1 glyph."),
PaintError::DepthLimitExceeded => write!(f, "Depth limit exceeded in COLRv1 glyph."),
}
}
}
impl From<ReadError> for PaintError {
fn from(value: ReadError) -> Self {
PaintError::ParseError(value)
}
}
#[derive(Copy, Clone, PartialEq, Debug, Default)]
#[cfg_attr(test, derive(Serialize, Deserialize))]
#[repr(C)]
pub struct ColorStop {
pub offset: f32,
pub palette_index: u16,
pub alpha: f32,
}
#[derive(Debug, PartialEq)]
pub enum Brush<'a> {
Solid { palette_index: u16, alpha: f32 },
LinearGradient {
p0: Point<f32>,
p1: Point<f32>,
color_stops: &'a [ColorStop],
extend: Extend,
},
RadialGradient {
c0: Point<f32>,
r0: f32,
c1: Point<f32>,
r1: f32,
color_stops: &'a [ColorStop],
extend: Extend,
},
SweepGradient {
c0: Point<f32>,
start_angle: f32,
end_angle: f32,
color_stops: &'a [ColorStop],
extend: Extend,
},
}
pub enum PaintCachedColorGlyph {
Ok,
Unimplemented,
}
pub trait ColorPainter {
fn push_transform(&mut self, transform: Transform);
fn pop_transform(&mut self);
fn push_clip_glyph(&mut self, glyph_id: GlyphId);
fn push_clip_box(&mut self, clip_box: BoundingBox<f32>);
fn pop_clip(&mut self);
fn fill(&mut self, brush: Brush<'_>);
fn fill_glyph(
&mut self,
glyph_id: GlyphId,
brush_transform: Option<Transform>,
brush: Brush<'_>,
) {
self.push_clip_glyph(glyph_id);
if let Some(wrap_in_transform) = brush_transform {
self.push_transform(wrap_in_transform);
self.fill(brush);
self.pop_transform();
} else {
self.fill(brush);
}
self.pop_clip();
}
fn paint_cached_color_glyph(
&mut self,
_glyph: GlyphId,
) -> Result<PaintCachedColorGlyph, PaintError> {
Ok(PaintCachedColorGlyph::Unimplemented)
}
fn push_layer(&mut self, composite_mode: CompositeMode);
fn pop_layer(&mut self) {}
fn pop_layer_with_mode(&mut self, _composite_mode: CompositeMode) {
self.pop_layer();
}
}
#[derive(Clone, Copy)]
pub enum ColorGlyphFormat {
ColrV0,
ColrV1,
}
#[derive(Clone)]
pub struct ColorGlyph<'a> {
colr: colr::Colr<'a>,
root_paint_ref: ColorGlyphRoot<'a>,
}
#[derive(Clone)]
enum ColorGlyphRoot<'a> {
V0Range(Range<usize>),
V1Paint(colr::Paint<'a>, PaintId, GlyphId, Result<u16, ReadError>),
}
impl<'a> ColorGlyph<'a> {
pub fn format(&self) -> ColorGlyphFormat {
match &self.root_paint_ref {
ColorGlyphRoot::V0Range(_) => ColorGlyphFormat::ColrV0,
ColorGlyphRoot::V1Paint(..) => ColorGlyphFormat::ColrV1,
}
}
pub fn bounding_box(
&self,
location: impl Into<LocationRef<'a>>,
size: Size,
) -> Option<BoundingBox<f32>> {
match &self.root_paint_ref {
ColorGlyphRoot::V1Paint(_paint, _paint_id, glyph_id, upem) => {
let instance =
instance::ColrInstance::new(self.colr.clone(), location.into().coords());
let resolved_bounding_box = get_clipbox_font_units(&instance, *glyph_id);
resolved_bounding_box.map(|bounding_box| {
let scale_factor = size.linear_scale((*upem).clone().unwrap_or(0));
bounding_box.scale(scale_factor)
})
}
_ => None,
}
}
pub fn paint(
&self,
location: impl Into<LocationRef<'a>>,
painter: &mut impl ColorPainter,
) -> Result<(), PaintError> {
let instance =
instance::ColrInstance::new(self.colr.clone(), location.into().effective_coords());
let mut resolved_stops = traversal::ColorStopVec::default();
match &self.root_paint_ref {
ColorGlyphRoot::V1Paint(paint, paint_id, glyph_id, _) => {
let clipbox = get_clipbox_font_units(&instance, *glyph_id);
if let Some(rect) = clipbox {
painter.push_clip_box(rect);
}
let mut decycler = PaintDecycler::default();
let mut cycle_guard = decycler.enter(*paint_id)?;
traverse_with_callbacks(
&resolve_paint(&instance, paint)?,
&instance,
painter,
&mut cycle_guard,
&mut resolved_stops,
0,
)?;
if clipbox.is_some() {
painter.pop_clip();
}
Ok(())
}
ColorGlyphRoot::V0Range(range) => {
traverse_v0_range(range, &instance, painter)?;
Ok(())
}
}
}
}
#[derive(Clone)]
pub struct ColorGlyphCollection<'a> {
colr: Option<colr::Colr<'a>>,
upem: Result<u16, ReadError>,
}
impl<'a> ColorGlyphCollection<'a> {
pub fn new(font: &FontRef<'a>) -> Self {
let colr = font.colr().ok();
let upem = font.head().map(|h| h.units_per_em());
Self { colr, upem }
}
pub fn get_with_format(
&self,
glyph_id: GlyphId,
glyph_format: ColorGlyphFormat,
) -> Option<ColorGlyph<'a>> {
let colr = self.colr.clone()?;
let root_paint_ref = match glyph_format {
ColorGlyphFormat::ColrV0 => {
let layer_range = colr.v0_base_glyph(glyph_id).ok()??;
ColorGlyphRoot::V0Range(layer_range)
}
ColorGlyphFormat::ColrV1 => {
let (paint, paint_id) = colr.v1_base_glyph(glyph_id).ok()??;
ColorGlyphRoot::V1Paint(paint, paint_id, glyph_id, self.upem.clone())
}
};
Some(ColorGlyph {
colr,
root_paint_ref,
})
}
pub fn get(&self, glyph_id: GlyphId) -> Option<ColorGlyph<'a>> {
self.get_with_format(glyph_id, ColorGlyphFormat::ColrV1)
.or_else(|| self.get_with_format(glyph_id, ColorGlyphFormat::ColrV0))
}
}
pub struct ColorPalette<'a> {
cpal: cpal::Cpal<'a>,
sub_array: &'a [Color],
index: u16,
}
impl<'a> ColorPalette<'a> {
pub fn colors(&self) -> &[Color] {
self.sub_array
}
pub fn palette_type(&self) -> Option<cpal::PaletteType> {
self.cpal
.palette_types_array()?
.ok()?
.get(usize::from(self.index))
.map(|p| p.get())
}
pub fn label(&self) -> Option<StringId> {
self.cpal
.palette_labels_array()?
.ok()?
.get(usize::from(self.index))
.and_then(|p| {
let name_id = p.get();
Some(name_id).filter(|name_id| name_id.to_u16() != 0xFFFF)
})
}
pub fn index(&self) -> u16 {
self.index
}
}
pub struct ColorPalettes<'a> {
cpal: Option<cpal::Cpal<'a>>,
}
impl<'a> ColorPalettes<'a> {
pub fn new(font: &FontRef<'a>) -> Self {
Self {
cpal: font.cpal().ok(),
}
}
pub fn len(&self) -> u16 {
self.cpal.as_ref().map_or(0, |cpal| cpal.num_palettes())
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn get(&self, index: u16) -> Option<ColorPalette<'_>> {
let cpal = self.cpal.clone()?;
let start_index: &BigEndian<u16> = cpal.color_record_indices().get(usize::from(index))?;
let start_index = usize::from(start_index.get());
let num_palette_entries = usize::from(cpal.num_palette_entries());
let color_records_array = cpal.color_records_array()?.ok()?;
let sub_array = color_records_array.get(start_index..start_index + num_palette_entries)?;
Some(ColorPalette {
cpal,
sub_array,
index,
})
}
pub fn color_label(&self, color_index: u16) -> Option<StringId> {
let name_id = self
.cpal
.as_ref()?
.palette_entry_labels_array()?
.ok()?
.get(usize::from(color_index))?
.get();
Some(name_id).filter(|name_id| name_id.to_u16() != 0xFFFF)
}
}
#[cfg(test)]
mod tests {
use crate::{
color::traversal_tests::test_glyph_defs::PAINTCOLRGLYPH_CYCLE,
prelude::{LocationRef, Size},
MetadataProvider,
};
use fontcull_read_fonts::{types::BoundingBox, FontRef};
use raw::tables::cpal;
use super::{Brush, ColorPainter, CompositeMode, GlyphId, Transform};
use crate::color::traversal_tests::test_glyph_defs::{COLORED_CIRCLES_V0, COLORED_CIRCLES_V1};
#[test]
fn has_colrv1_glyph_test() {
let colr_font = fontcull_font_test_data::COLRV0V1_VARIABLE;
let font = FontRef::new(colr_font).unwrap();
let get_colrv1_glyph = |codepoint: &[char]| {
font.charmap().map(codepoint[0]).and_then(|glyph_id| {
font.color_glyphs()
.get_with_format(glyph_id, crate::color::ColorGlyphFormat::ColrV1)
})
};
assert!(get_colrv1_glyph(COLORED_CIRCLES_V0).is_none());
assert!(get_colrv1_glyph(COLORED_CIRCLES_V1).is_some());
}
struct DummyColorPainter {}
impl DummyColorPainter {
pub fn new() -> Self {
Self {}
}
}
impl Default for DummyColorPainter {
fn default() -> Self {
Self::new()
}
}
impl ColorPainter for DummyColorPainter {
fn push_transform(&mut self, _transform: Transform) {}
fn pop_transform(&mut self) {}
fn push_clip_glyph(&mut self, _glyph: GlyphId) {}
fn push_clip_box(&mut self, _clip_box: BoundingBox<f32>) {}
fn pop_clip(&mut self) {}
fn fill(&mut self, _brush: Brush) {}
fn push_layer(&mut self, _composite_mode: CompositeMode) {}
fn pop_layer(&mut self) {}
}
#[test]
fn paintcolrglyph_cycle_test() {
let colr_font = fontcull_font_test_data::COLRV0V1_VARIABLE;
let font = FontRef::new(colr_font).unwrap();
let cycle_glyph_id = font.charmap().map(PAINTCOLRGLYPH_CYCLE[0]).unwrap();
let colrv1_glyph = font
.color_glyphs()
.get_with_format(cycle_glyph_id, crate::color::ColorGlyphFormat::ColrV1);
assert!(colrv1_glyph.is_some());
let mut color_painter = DummyColorPainter::new();
let result = colrv1_glyph
.unwrap()
.paint(LocationRef::default(), &mut color_painter);
assert!(result.is_err());
}
#[test]
fn no_cliplist_test() {
let colr_font = fontcull_font_test_data::COLRV1_NO_CLIPLIST;
let font = FontRef::new(colr_font).unwrap();
let cycle_glyph_id = GlyphId::new(1);
let colrv1_glyph = font
.color_glyphs()
.get_with_format(cycle_glyph_id, crate::color::ColorGlyphFormat::ColrV1);
assert!(colrv1_glyph.is_some());
let mut color_painter = DummyColorPainter::new();
let result = colrv1_glyph
.unwrap()
.paint(LocationRef::default(), &mut color_painter);
assert!(result.is_ok());
}
#[test]
fn colrv0_no_bbox_test() {
let colr_font = fontcull_font_test_data::COLRV0V1;
let font = FontRef::new(colr_font).unwrap();
let colrv0_glyph_id = GlyphId::new(168);
let colrv0_glyph = font
.color_glyphs()
.get_with_format(colrv0_glyph_id, super::ColorGlyphFormat::ColrV0)
.unwrap();
assert!(colrv0_glyph
.bounding_box(LocationRef::default(), Size::unscaled())
.is_none());
}
#[test]
fn cpal_test() {
use crate::color::Color;
let cpal_font = fontcull_font_test_data::COLRV0V1;
let font = FontRef::new(cpal_font).unwrap();
let palettes = font.color_palettes();
assert_eq!(palettes.len(), 3);
let first_palette = palettes.get(0).unwrap();
assert_eq!(first_palette.colors().len(), 14);
assert_eq!(
first_palette.colors().first(),
Some(&Color {
blue: 0,
green: 0,
red: 255,
alpha: 255
})
);
assert_eq!(first_palette.colors().get(14), None);
assert_eq!(
first_palette.palette_type(),
Some(cpal::PaletteType::empty())
);
let second_palette = palettes.get(1).unwrap();
assert_eq!(
second_palette.colors().first(),
Some(&Color {
blue: 74,
green: 41,
red: 42,
alpha: 255
})
);
assert_eq!(
second_palette.palette_type(),
Some(cpal::PaletteType::USABLE_WITH_DARK_BACKGROUND)
);
let third_palette = palettes.get(2).unwrap();
assert_eq!(
third_palette.colors().first(),
Some(&Color {
blue: 24,
green: 113,
red: 252,
alpha: 255
})
);
assert_eq!(
third_palette.palette_type(),
Some(cpal::PaletteType::USABLE_WITH_LIGHT_BACKGROUND)
);
assert!(palettes.get(3).is_none());
}
}