use crate::parser::OptionLog;
use skrifa::instance::LocationRef;
use skrifa::prelude::Size;
use skrifa::raw::types::Point;
use skrifa::{
MetadataProvider,
color::{Brush, ColorStop, Extend, Transform},
outline::DrawSettings,
};
use std::fmt::Write as _;
use svgtypes::Color;
use super::transform::{skrifa_to_tsp_transform, tsp_to_skrifa_transform};
struct Builder<'a> {
path: &'a mut String,
min_x: f32,
min_y: f32,
max_x: f32,
max_y: f32,
}
impl<'a> Builder<'a> {
fn new(path: &'a mut String) -> Self {
Self {
path,
min_x: f32::MAX,
min_y: f32::MAX,
max_x: f32::MIN,
max_y: f32::MIN,
}
}
fn add_point(&mut self, x: f32, y: f32) {
self.min_x = self.min_x.min(x);
self.min_y = self.min_y.min(y);
self.max_x = self.max_x.max(x);
self.max_y = self.max_y.max(y);
}
fn bounds(&self) -> Option<tiny_skia_path::Rect> {
if self.min_x <= self.max_x && self.min_y <= self.max_y {
tiny_skia_path::Rect::from_ltrb(self.min_x, self.min_y, self.max_x, self.max_y)
} else {
None
}
}
fn finish(&mut self) {
if !self.path.is_empty() {
self.path.pop(); }
}
}
impl skrifa::outline::OutlinePen for Builder<'_> {
fn move_to(&mut self, x: f32, y: f32) {
self.add_point(x, y);
write!(self.path, "M {} {} ", x, y).unwrap();
}
fn line_to(&mut self, x: f32, y: f32) {
self.add_point(x, y);
write!(self.path, "L {} {} ", x, y).unwrap();
}
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.add_point(cx0, cy0);
self.add_point(x, y);
write!(self.path, "Q {} {} {} {} ", cx0, cy0, x, y).unwrap();
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.add_point(cx0, cy0);
self.add_point(cx1, cy1);
self.add_point(x, y);
write!(self.path, "C {} {} {} {} {} {} ", cx0, cy0, cx1, cy1, x, y).unwrap();
}
fn close(&mut self) {
self.path.push_str("Z ");
}
}
trait XmlWriterExt {
fn write_color_attribute(&mut self, name: &str, ts: Color);
fn write_transform_attribute(&mut self, name: &str, ts: Transform);
fn write_spread_method_attribute(&mut self, method: Extend);
}
impl XmlWriterExt for xmlwriter::XmlWriter {
fn write_color_attribute(&mut self, name: &str, color: Color) {
self.write_attribute_fmt(
name,
format_args!("rgb({}, {}, {})", color.red, color.green, color.blue),
);
}
fn write_transform_attribute(&mut self, name: &str, ts: Transform) {
if ts == Transform::default() {
return;
}
self.write_attribute_fmt(
name,
format_args!(
"matrix({} {} {} {} {} {})",
ts.xx, ts.yx, ts.xy, ts.yy, ts.dx, ts.dy
),
);
}
fn write_spread_method_attribute(&mut self, extend: Extend) {
self.write_attribute(
"spreadMethod",
match extend {
Extend::Pad => "pad",
Extend::Repeat => "repeat",
Extend::Reflect => "reflect",
Extend::Unknown => return,
},
);
}
}
pub(crate) struct GlyphPainter<'a> {
pub(crate) font: &'a skrifa::FontRef<'a>,
pub(crate) location: LocationRef<'a>,
pub(crate) svg: &'a mut xmlwriter::XmlWriter,
pub(crate) path_buf: &'a mut String,
pub(crate) gradient_index: usize,
pub(crate) clip_path_index: usize,
pub(crate) foreground_color: Color,
pub(crate) transform: Transform,
pub(crate) outline_transform: Transform,
pub(crate) transforms_stack: Vec<Transform>,
pub(crate) clip_stack: Vec<Option<tiny_skia_path::Rect>>,
}
impl<'a> GlyphPainter<'a> {
fn write_gradient_stops(&mut self, stops: &[ColorStop]) {
for stop in stops {
let color = self.palette_index_to_color(stop.palette_index, stop.alpha);
self.svg.start_element("stop");
self.svg.write_attribute("offset", &stop.offset);
self.svg.write_color_attribute("stop-color", color);
let opacity = f32::from(color.alpha) / 255.0;
self.svg.write_attribute("stop-opacity", &opacity);
self.svg.end_element();
}
}
fn paint_solid(&mut self, color: Color) {
self.svg.start_element("path");
self.svg.write_color_attribute("fill", color);
let opacity = f32::from(color.alpha) / 255.0;
self.svg.write_attribute("fill-opacity", &opacity);
self.svg
.write_transform_attribute("transform", self.outline_transform);
self.svg.write_attribute("d", self.path_buf);
self.svg.end_element();
}
fn paint_linear_gradient(
&mut self,
p0: Point<f32>,
p1: Point<f32>,
color_stops: &[ColorStop],
extend: Extend,
) {
let gradient_id = format!("lg{}", self.gradient_index);
self.gradient_index += 1;
let gradient_transform = paint_transform(self.outline_transform, self.transform);
self.svg.start_element("linearGradient");
self.svg.write_attribute("id", &gradient_id);
self.svg.write_attribute("x1", &p0.x);
self.svg.write_attribute("y1", &p0.y);
self.svg.write_attribute("x2", &p1.x);
self.svg.write_attribute("y2", &p1.y);
self.svg.write_attribute("gradientUnits", &"userSpaceOnUse");
self.svg.write_spread_method_attribute(extend);
self.svg
.write_transform_attribute("gradientTransform", gradient_transform);
self.write_gradient_stops(color_stops);
self.svg.end_element();
self.svg.start_element("path");
self.svg
.write_attribute_fmt("fill", format_args!("url(#{})", gradient_id));
self.svg
.write_transform_attribute("transform", self.outline_transform);
self.svg.write_attribute("d", self.path_buf);
self.svg.end_element();
}
fn paint_radial_gradient(
&mut self,
c0: Point<f32>,
r0: f32,
c1: Point<f32>,
r1: f32,
color_stops: &[ColorStop],
extend: Extend,
) {
let gradient_id = format!("rg{}", self.gradient_index);
self.gradient_index += 1;
let gradient_transform = paint_transform(self.outline_transform, self.transform);
self.svg.start_element("radialGradient");
self.svg.write_attribute("id", &gradient_id);
self.svg.write_attribute("cx", &c1.x);
self.svg.write_attribute("cy", &c1.y);
self.svg.write_attribute("r", &r1);
self.svg.write_attribute("fr", &r0);
self.svg.write_attribute("fx", &c0.x);
self.svg.write_attribute("fy", &c0.y);
self.svg.write_attribute("gradientUnits", &"userSpaceOnUse");
self.svg.write_spread_method_attribute(extend);
self.svg
.write_transform_attribute("gradientTransform", gradient_transform);
self.write_gradient_stops(color_stops);
self.svg.end_element();
self.svg.start_element("path");
self.svg
.write_attribute_fmt("fill", format_args!("url(#{})", gradient_id));
self.svg
.write_transform_attribute("transform", self.outline_transform);
self.svg.write_attribute("d", self.path_buf);
self.svg.end_element();
}
fn paint_sweep_gradient(
&mut self,
_c0: Point<f32>,
_start_angle: f32,
_end_angle: f32,
_color_stops: &[ColorStop],
_extend: Extend,
) {
}
}
fn paint_transform(outline_transform: Transform, transform: Transform) -> Transform {
let outline_transform = skrifa_to_tsp_transform(outline_transform);
let gradient_transform = skrifa_to_tsp_transform(transform);
let gradient_transform = outline_transform
.invert()
.log_none(|| log::warn!("Failed to calculate transform for gradient in glyph."))
.unwrap_or_default()
.pre_concat(gradient_transform);
tsp_to_skrifa_transform(gradient_transform)
}
fn map_rect(
rect: tiny_skia_path::Rect,
ts: tiny_skia_path::Transform,
) -> Option<tiny_skia_path::Rect> {
let mut points = [
tiny_skia_path::Point::from_xy(rect.left(), rect.top()),
tiny_skia_path::Point::from_xy(rect.right(), rect.top()),
tiny_skia_path::Point::from_xy(rect.left(), rect.bottom()),
tiny_skia_path::Point::from_xy(rect.right(), rect.bottom()),
];
ts.map_points(&mut points);
let min_x = points.iter().map(|p| p.x).fold(f32::MAX, f32::min);
let min_y = points.iter().map(|p| p.y).fold(f32::MAX, f32::min);
let max_x = points.iter().map(|p| p.x).fold(f32::MIN, f32::max);
let max_y = points.iter().map(|p| p.y).fold(f32::MIN, f32::max);
tiny_skia_path::Rect::from_ltrb(min_x, min_y, max_x, max_y)
}
fn intersect_rects(
a: tiny_skia_path::Rect,
b: tiny_skia_path::Rect,
) -> Option<tiny_skia_path::Rect> {
tiny_skia_path::Rect::from_ltrb(
a.left().max(b.left()),
a.top().max(b.top()),
a.right().min(b.right()),
a.bottom().min(b.bottom()),
)
}
impl GlyphPainter<'_> {
fn clip_with_path(&mut self, path: &str) {
let clip_id = format!("cp{}", self.clip_path_index);
self.clip_path_index += 1;
self.svg.start_element("clipPath");
self.svg.write_attribute("id", &clip_id);
self.svg.start_element("path");
self.svg
.write_transform_attribute("transform", self.outline_transform);
self.svg.write_attribute("d", &path);
self.svg.end_element();
self.svg.end_element();
self.svg.start_element("g");
self.svg
.write_attribute_fmt("clip-path", format_args!("url(#{})", clip_id));
}
fn outline_glyph(&mut self, glyph_id: skrifa::GlyphId) -> Option<tiny_skia_path::Rect> {
self.path_buf.clear();
let mut bounds = None;
let outlined = if let Some(outliner) = self.font.outline_glyphs().get(glyph_id) {
let mut builder = Builder::new(self.path_buf);
let size = Size::unscaled();
let ok = outliner
.draw(DrawSettings::unhinted(size, self.location), &mut builder)
.is_ok();
if ok {
builder.finish();
bounds = builder.bounds();
}
ok
} else {
false
};
if !outlined {
self.path_buf.clear();
}
self.outline_transform = self.transform;
bounds
}
fn paint_brush(&mut self, brush: Brush<'_>) {
match brush {
Brush::Solid {
palette_index,
alpha,
} => {
let color = self.palette_index_to_color(palette_index, alpha);
self.paint_solid(color);
}
Brush::LinearGradient {
p0,
p1,
color_stops,
extend,
} => self.paint_linear_gradient(p0, p1, color_stops, extend),
Brush::RadialGradient {
c0,
r0,
c1,
r1,
color_stops,
extend,
} => self.paint_radial_gradient(c0, r0, c1, r1, color_stops, extend),
Brush::SweepGradient {
c0,
start_angle,
end_angle,
color_stops,
extend,
} => self.paint_sweep_gradient(c0, start_angle, end_angle, color_stops, extend),
}
}
fn palette_index_to_color(&self, palette_index: u16, alpha: f32) -> Color {
let lookup = || -> Option<Color> {
let palettes = self.font.color_palettes();
let palette = palettes.get(0)?;
let color = palette.colors().get(palette_index as usize)?;
Some(Color {
red: color.red,
blue: color.blue,
green: color.green,
alpha: color.alpha,
})
};
let mut color = if palette_index == u16::MAX {
self.foreground_color
} else {
lookup().unwrap_or(self.foreground_color)
};
color.alpha = ((color.alpha as f32) * alpha) as u8;
color
}
}
impl<'a> skrifa::color::ColorPainter for GlyphPainter<'a> {
fn push_transform(&mut self, transform: Transform) {
self.transforms_stack.push(self.transform);
self.transform = self.transform * transform;
}
fn pop_transform(&mut self) {
if let Some(ts) = self.transforms_stack.pop() {
self.transform = ts;
}
}
fn fill_glyph(
&mut self,
glyph_id: skrifa::GlyphId,
brush_transform: Option<Transform>,
brush: Brush<'_>,
) {
self.outline_glyph(glyph_id);
if let Some(brush_transform) = brush_transform {
self.push_transform(brush_transform);
self.paint_brush(brush);
self.pop_transform();
} else {
self.paint_brush(brush);
}
}
fn push_clip_glyph(&mut self, glyph_id: skrifa::GlyphId) {
let bounds = self.outline_glyph(glyph_id);
let path = self.path_buf.clone();
self.clip_with_path(&path);
let root_bounds =
bounds.and_then(|b| map_rect(b, skrifa_to_tsp_transform(self.outline_transform)));
self.clip_stack.push(root_bounds);
}
fn push_clip_box(&mut self, clip_box: skrifa::raw::types::BoundingBox<f32>) {
let x_min = clip_box.x_min;
let x_max = clip_box.x_max;
let y_min = clip_box.y_min;
let y_max = clip_box.y_max;
let clip_path = format!(
"M {} {} L {} {} L {} {} L {} {} Z",
x_min, y_min, x_max, y_min, x_max, y_max, x_min, y_max
);
self.outline_transform = self.transform;
self.clip_with_path(&clip_path);
let bounds = tiny_skia_path::Rect::from_ltrb(x_min, y_min, x_max, y_max)
.and_then(|b| map_rect(b, skrifa_to_tsp_transform(self.outline_transform)));
self.clip_stack.push(bounds);
}
fn pop_clip(&mut self) {
self.svg.end_element();
self.clip_stack.pop();
}
fn fill(&mut self, brush: Brush<'_>) {
if self.clip_stack.is_empty() {
log::warn!("Unclipped COLR fills are not supported.");
return;
}
let mut region: Option<tiny_skia_path::Rect> = None;
for bounds in &self.clip_stack {
let Some(bounds) = bounds else { return };
region = Some(match region {
Some(region) => match intersect_rects(region, *bounds) {
Some(r) => r,
None => return,
},
None => *bounds,
});
}
let Some(region) = region else { return };
self.path_buf.clear();
write!(
self.path_buf,
"M {} {} L {} {} L {} {} L {} {} Z",
region.left(),
region.top(),
region.right(),
region.top(),
region.right(),
region.bottom(),
region.left(),
region.bottom()
)
.unwrap();
self.outline_transform = Transform::default();
self.paint_brush(brush);
}
fn push_layer(&mut self, composite_mode: skrifa::color::CompositeMode) {
use skrifa::color::CompositeMode;
let composite_mode = match composite_mode {
CompositeMode::SrcOver => "normal",
CompositeMode::Screen => "screen",
CompositeMode::Overlay => "overlay",
CompositeMode::Darken => "darken",
CompositeMode::Lighten => "lighten",
CompositeMode::ColorDodge => "color-dodge",
CompositeMode::ColorBurn => "color-burn",
CompositeMode::HardLight => "hard-light",
CompositeMode::SoftLight => "soft-light",
CompositeMode::Difference => "difference",
CompositeMode::Exclusion => "exclusion",
CompositeMode::Multiply => "multiply",
CompositeMode::HslHue => "hue",
CompositeMode::HslSaturation => "saturation",
CompositeMode::HslColor => "color",
CompositeMode::HslLuminosity => "luminosity",
_ => {
"normal"
}
};
self.svg.start_element("g");
self.svg.write_attribute_fmt(
"style",
format_args!("mix-blend-mode: {}; isolation: isolate", composite_mode),
);
}
fn pop_layer(&mut self) {
self.svg.end_element();
}
}