use kurbo::{Affine, BezPath, PathEl, Point};
use skrifa::{
color::{Brush, ColorPainter, CompositeMode, Extend, Transform},
metrics::BoundingBox,
outline::{DrawError, DrawSettings, OutlinePen},
prelude::{LocationRef, Size},
raw::{tables::cpal::ColorRecord, FontRef, TableProvider},
GlyphId, MetadataProvider, OutlineGlyphCollection,
};
use thiserror::Error;
use tiny_skia::Color;
pub(crate) struct SvgPathPen {
path: BezPath,
transform: Affine,
}
impl SvgPathPen {
pub(crate) fn new() -> Self {
SvgPathPen {
path: Default::default(),
transform: Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, 0.0]),
}
}
pub(crate) fn new_with_transform(transform: Affine) -> Self {
SvgPathPen {
path: Default::default(),
transform,
}
}
fn transform_point(&self, x: f32, y: f32) -> Point {
self.transform * Point::new(x as f64, y as f64)
}
pub(crate) fn into_inner(self) -> BezPath {
self.path
}
}
impl OutlinePen for SvgPathPen {
fn move_to(&mut self, x: f32, y: f32) {
self.path.move_to(self.transform_point(x, y));
}
fn line_to(&mut self, x: f32, y: f32) {
self.path.line_to(self.transform_point(x, y));
}
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.path
.quad_to(self.transform_point(cx0, cy0), self.transform_point(x, y));
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.path.curve_to(
self.transform_point(cx0, cy0),
self.transform_point(cx1, cy1),
self.transform_point(x, y),
);
}
fn close(&mut self) {
self.path.close_path();
}
}
#[derive(Debug, Clone, Copy)]
pub struct ColorStop {
pub offset: f32,
pub color: Color,
}
#[derive(Debug, Clone)]
pub struct ColorFill {
pub paint: Paint,
pub clip_paths: Vec<BezPath>,
pub offset_x: f64,
pub offset_y: f64,
}
#[derive(Debug, Clone)]
pub enum Paint {
Solid(Color),
LinearGradient {
p0: Point,
p1: Point,
stops: Vec<ColorStop>,
extend: Extend,
transform: Affine,
},
RadialGradient {
c0: Point,
r0: f32,
c1: Point,
r1: f32,
stops: Vec<ColorStop>,
extend: Extend,
transform: Affine,
},
SweepGradient {
c0: Point,
start_angle: f32,
end_angle: f32,
stops: Vec<ColorStop>,
extend: Extend,
transform: Affine,
},
}
#[derive(Error, Debug)]
pub enum GlyphPainterError {
#[error("glyph {0} not found")]
GlyphNotFound(GlyphId),
#[error("Unsupported font feature: {0}")]
UnsupportedFontFeature(&'static str),
#[error("{0}")]
DrawError(#[from] DrawError),
}
pub struct GlyphPainter<'a> {
pub x: f64,
pub y: f64,
location: LocationRef<'a>,
size: Size,
scale: f32,
outlines: OutlineGlyphCollection<'a>,
foreground: Color,
is_colr: bool,
colors: &'a [ColorRecord],
builder: Result<ColorFillsBuilder, GlyphPainterError>,
}
struct ColorFillsBuilder {
paths: Vec<BezPath>,
transforms: Vec<Affine>,
fills: Vec<ColorFill>,
}
pub const fn foreground_paint() -> skrifa::color::Brush<'static> {
skrifa::color::Brush::Solid {
palette_index: GlyphPainter::FOREGROUND_PALETTE_IDX,
alpha: 1.0,
}
}
impl<'a> GlyphPainter<'a> {
const FOREGROUND_PALETTE_IDX: u16 = 0xFFFF;
pub fn new(
font: &FontRef<'a>,
location: LocationRef<'a>,
foreground: Color,
size: Size,
) -> Self {
let upem = font.head().map(|h| h.units_per_em());
let scale = upem.map(|upem| size.linear_scale(upem)).unwrap_or(1.0);
let outlines = font.outline_glyphs();
let is_colr = font.colr().is_ok();
let colors = match font.cpal().map(|c| c.color_records_array()) {
Ok(Some(Ok(c))) => c,
_ => &[],
};
GlyphPainter {
x: 0.0,
y: 0.0,
location,
size,
scale,
outlines,
foreground,
is_colr,
colors,
builder: Ok(ColorFillsBuilder {
paths: Vec::new(),
transforms: Vec::new(),
fills: Vec::new(),
}),
}
}
pub fn into_fills(self) -> Result<Vec<ColorFill>, GlyphPainterError> {
self.builder.map(|i| i.fills)
}
fn set_err(&mut self, err: GlyphPainterError) {
if self.builder.is_ok() {
self.builder = Err(err);
}
}
}
impl ColorFillsBuilder {
fn current_transform(&self) -> Affine {
self.transforms.last().copied().unwrap_or_default()
}
}
impl<'a> ColorPainter for GlyphPainter<'a> {
fn push_transform(&mut self, transform: Transform) {
let Ok(builder) = self.builder.as_mut() else {
return;
};
let transform = Affine::new([
transform.xx as f64,
transform.yx as f64,
transform.xy as f64,
transform.yy as f64,
transform.dx as f64,
transform.dy as f64,
]);
let new_transform = match builder.transforms.last().copied() {
Some(prev_transform) => transform * prev_transform,
None => transform,
};
builder.transforms.push(new_transform);
}
fn pop_transform(&mut self) {
let Ok(builder) = self.builder.as_mut() else {
return;
};
builder.transforms.pop();
}
fn push_clip_glyph(&mut self, glyph_id: GlyphId) {
let Ok(builder) = self.builder.as_mut() else {
return;
};
let Some(glyph) = self.outlines.get(glyph_id) else {
self.set_err(GlyphPainterError::GlyphNotFound(glyph_id));
return;
};
let (size, scale) = if self.is_colr {
(Size::unscaled(), self.scale as f64)
} else {
(self.size, 1.0)
};
let draw_settings = DrawSettings::unhinted(size, self.location);
let mut path_pen = SvgPathPen::new_with_transform(
builder
.current_transform()
.then_scale_non_uniform(scale, -scale),
);
match glyph.draw(draw_settings, &mut path_pen) {
Ok(_) => builder.paths.push(path_pen.into_inner()),
Err(err) => {
self.set_err(err.into());
}
}
}
fn push_clip_box(&mut self, clip_box: BoundingBox) {
let Ok(builder) = self.builder.as_mut() else {
return;
};
let path = BezPath::from_vec(vec![
PathEl::MoveTo(Point::new(clip_box.x_min as f64, clip_box.y_min as f64)),
PathEl::LineTo(Point::new(clip_box.x_max as f64, clip_box.y_min as f64)),
PathEl::LineTo(Point::new(clip_box.x_max as f64, clip_box.y_max as f64)),
PathEl::LineTo(Point::new(clip_box.x_min as f64, clip_box.y_max as f64)),
PathEl::ClosePath,
]);
let transform = builder
.current_transform()
.then_scale_non_uniform(self.scale as f64, -self.scale as f64);
builder.paths.push(transform * path);
}
fn pop_clip(&mut self) {
if let Ok(builder) = self.builder.as_mut() {
builder.paths.pop();
}
}
fn fill(&mut self, brush: Brush<'_>) {
macro_rules! color_or_exit {
($palette_idx:expr, $alpha:expr) => {
if $palette_idx == Self::FOREGROUND_PALETTE_IDX {
let mut color = self.foreground;
color.set_alpha($alpha);
color
} else {
let Some(color) = self.colors.get($palette_idx as usize) else {
self.set_err(GlyphPainterError::UnsupportedFontFeature(
"color palette index out of bounds",
));
return;
};
let max = u8::MAX as f32;
Color::from_rgba8(
color.red,
color.green,
color.blue,
($alpha * max).clamp(0.0, max) as u8,
)
}
};
}
macro_rules! color_stops_or_exit {
($color_stops:expr) => {{
let mut stops = Vec::with_capacity($color_stops.len());
for stop in $color_stops.iter() {
stops.push(ColorStop {
offset: stop.offset,
color: color_or_exit!(stop.palette_index, stop.alpha),
});
}
stops
}};
}
let Ok(builder) = self.builder.as_mut() else {
return;
};
let transform = builder
.current_transform()
.then_scale_non_uniform(self.scale as f64, -self.scale as f64);
let paint = match brush {
Brush::Solid {
palette_index,
alpha,
} => Paint::Solid(color_or_exit!(palette_index, alpha)),
Brush::LinearGradient {
p0,
p1,
color_stops,
extend,
} => Paint::LinearGradient {
p0: Point::new(p0.x as f64, p0.y as f64),
p1: Point::new(p1.x as f64, p1.y as f64),
stops: color_stops_or_exit!(color_stops),
extend,
transform,
},
Brush::RadialGradient {
c0,
r0,
c1,
r1,
color_stops,
extend,
} => Paint::RadialGradient {
c0: Point::new(c0.x as f64, c0.y as f64),
r0,
c1: Point::new(c1.x as f64, c1.y as f64),
r1,
stops: color_stops_or_exit!(color_stops),
extend,
transform,
},
Brush::SweepGradient {
c0,
start_angle,
end_angle,
color_stops,
extend,
} => Paint::SweepGradient {
c0: Point::new(c0.x as f64, c0.y as f64),
start_angle,
end_angle,
stops: color_stops_or_exit!(color_stops),
extend,
transform,
},
};
builder.fills.push(ColorFill {
paint,
clip_paths: builder.paths.clone(),
offset_x: self.x,
offset_y: self.y,
});
}
fn push_layer(&mut self, _: CompositeMode) {
self.set_err(GlyphPainterError::UnsupportedFontFeature("colr layers"));
}
}