use parley::{GlyphRun, Layout, PositionedLayoutItem};
use skrifa::{
OutlineGlyph,
outline::{DrawSettings, OutlinePen},
prelude::*,
};
use tiny_skia::{
FillRule, LineCap, LineJoin, Paint, PathBuilder, PixmapMut, Point, Rect, Stroke, Transform,
};
use crate::meme::{Color, TextOutline};
pub fn render_layout(
layout: Layout<()>,
pixmap: &mut PixmapMut<'_>,
transform: Transform,
fg_color: Color,
outline: Option<TextOutline>,
) {
fn render_with(color: Color, pen: &mut TinySkiaPen<'_, '_>, layout: &Layout<()>) {
for line in layout.lines() {
for item in line.items() {
match item {
PositionedLayoutItem::GlyphRun(glyph_run) => {
render_glyph_run(color, &glyph_run, pen);
}
PositionedLayoutItem::InlineBox(inline_box) => {
pen.set_origin(Point::from_xy(inline_box.x, inline_box.y));
pen.set_color(color);
pen.fill_rect(inline_box.width, inline_box.height);
}
}
}
}
}
if let Some(TextOutline { color, width }) = outline {
render_with(
color,
&mut TinySkiaPen::new(PenMode::Stroke(2.0 * width), pixmap, transform),
&layout,
);
}
render_with(
fg_color,
&mut TinySkiaPen::new(PenMode::Fill, pixmap, transform),
&layout,
);
}
fn render_glyph_run(color: Color, glyph_run: &GlyphRun<'_, ()>, pen: &mut TinySkiaPen<'_, '_>) {
let mut run_x = glyph_run.offset();
let run_y = glyph_run.baseline();
let run = glyph_run.run();
let font = run.font();
let font_size = run.font_size();
let normalized_coords = run
.normalized_coords()
.iter()
.map(|coord| NormalizedCoord::from_bits(*coord))
.collect::<Vec<_>>();
let font_collection_ref = font.data.as_ref();
let font_ref = FontRef::from_index(font_collection_ref, font.index).unwrap();
let outlines = font_ref.outline_glyphs();
for glyph in glyph_run.glyphs() {
let glyph_x = run_x + glyph.x;
let glyph_y = run_y - glyph.y;
run_x += glyph.advance;
let glyph_id = GlyphId::from(glyph.id);
if let Some(glyph_outline) = outlines.get(glyph_id) {
pen.set_origin(Point::from_xy(glyph_x, glyph_y));
pen.set_color(color);
pen.draw_glyph(&glyph_outline, font_size, &normalized_coords);
}
}
let style = glyph_run.style();
let run_metrics = run.metrics();
if let Some(decoration) = &style.underline {
let offset = decoration.offset.unwrap_or(run_metrics.underline_offset);
let size = decoration.size.unwrap_or(run_metrics.underline_size);
render_decoration(color, pen, glyph_run, offset, size);
}
if let Some(decoration) = &style.strikethrough {
let offset = decoration
.offset
.unwrap_or(run_metrics.strikethrough_offset);
let size = decoration.size.unwrap_or(run_metrics.strikethrough_size);
render_decoration(color, pen, glyph_run, offset, size);
}
}
fn render_decoration(
color: Color,
pen: &mut TinySkiaPen<'_, '_>,
glyph_run: &GlyphRun<'_, ()>,
offset: f32,
width: f32,
) {
let y = glyph_run.baseline() - offset;
let x = glyph_run.offset();
pen.set_color(color);
pen.set_origin(Point { x, y });
pen.fill_rect(glyph_run.advance(), width);
}
struct TinySkiaPen<'p, 'pref>
where
'p: 'pref,
{
mode: PenMode,
pixmap: &'pref mut PixmapMut<'p>,
origin: Point,
transform: Transform,
paint: Paint<'static>,
open_path: PathBuilder,
}
#[derive(Clone, Copy)]
enum PenMode {
Stroke(f32),
Fill,
}
impl<'p, 'pref> TinySkiaPen<'p, 'pref>
where
'p: 'pref,
{
fn new(
mode: PenMode,
pixmap: &'pref mut PixmapMut<'p>,
transform: Transform,
) -> TinySkiaPen<'p, 'pref> {
TinySkiaPen {
mode,
pixmap,
origin: Point::default(),
transform,
paint: Paint::default(),
open_path: PathBuilder::new(),
}
}
fn set_origin(&mut self, origin: Point) {
self.origin = origin
}
fn set_color(&mut self, color: Color) {
self.paint.set_color(color.into());
}
fn fill_rect(&mut self, width: f32, height: f32) {
let rect = Rect::from_xywh(self.origin.x, self.origin.y, width, height).unwrap();
self.pixmap
.fill_rect(rect, &self.paint, self.transform, None);
}
fn draw_glyph(
&mut self,
glyph: &OutlineGlyph<'_>,
size: f32,
normalized_coords: &[NormalizedCoord],
) {
let location_ref = LocationRef::new(normalized_coords);
let settings = DrawSettings::unhinted(Size::new(size), location_ref);
glyph.draw(settings, self).unwrap();
let builder = core::mem::replace(&mut self.open_path, PathBuilder::new());
if let Some(path) = builder.finish() {
match self.mode {
PenMode::Stroke(width) => self.pixmap.stroke_path(
&path,
&self.paint,
&Stroke {
width,
line_cap: LineCap::Round,
line_join: LineJoin::Round,
..Default::default()
},
self.transform,
None,
),
PenMode::Fill => self.pixmap.fill_path(
&path,
&self.paint,
FillRule::Winding,
self.transform,
None,
),
}
}
}
}
impl OutlinePen for TinySkiaPen<'_, '_> {
fn move_to(&mut self, x: f32, y: f32) {
self.open_path.move_to(self.origin.x + x, self.origin.y - y);
}
fn line_to(&mut self, x: f32, y: f32) {
self.open_path.line_to(self.origin.x + x, self.origin.y - y);
}
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.open_path.quad_to(
self.origin.x + cx0,
self.origin.y - cy0,
self.origin.x + x,
self.origin.y - y,
);
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.open_path.cubic_to(
self.origin.x + cx0,
self.origin.y - cy0,
self.origin.x + cx1,
self.origin.y - cy1,
self.origin.x + x,
self.origin.y - y,
);
}
fn close(&mut self) {
self.open_path.close();
}
}