use std::sync::Arc;
use ab_glyph_rasterizer::Rasterizer;
use cranpose_core::collections::bounded_lru::BoundedLruCache;
use cranpose_text::{GlyphId, GlyphOutline, OutlineSegment, OutlineTransform, Typeface};
use cranpose_ui_graphics::whole;
use tiny_skia::{LineCap, LineJoin, Paint, PathBuilder, Pixmap, Stroke, Transform};
use crate::text_mask_gamma::unit_to_u8;
pub(crate) const STROKE_MITER_LIMIT: f32 = 4.0;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct GlyphRaster {
pub stroke: Option<f32>,
pub embolden: f32,
pub skew: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct GlyphMaskKey {
pub font_hash: u64,
pub glyph_id: u32,
pub size_bits: u32,
pub stroke_bits: u32,
pub embolden_bits: u32,
pub skew_bits: u32,
}
impl GlyphMaskKey {
pub fn of(typeface: &Typeface, glyph: GlyphId, size: f32, raster: GlyphRaster) -> Self {
Self {
font_hash: typeface.content_hash(),
glyph_id: glyph.0,
size_bits: size.to_bits(),
stroke_bits: raster.stroke.map_or(0, f32::to_bits),
embolden_bits: raster.embolden.to_bits(),
skew_bits: raster.skew.to_bits(),
}
}
pub fn is_plain_fill(self) -> bool {
self.stroke_bits == 0 && self.embolden_bits == 0 && self.skew_bits == 0
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct GlyphMask {
pub alpha: Arc<[u8]>,
pub width: usize,
pub height: usize,
pub left: i32,
pub top: i32,
}
pub fn rasterize_glyph(
typeface: &Typeface,
glyph: GlyphId,
size: f32,
raster: GlyphRaster,
rasterizer: &mut Rasterizer,
) -> Option<GlyphMask> {
let transform = OutlineTransform {
size,
skew: raster.skew,
..OutlineTransform::default()
};
let outline = GlyphOutline::of(typeface, glyph, transform)?;
let stroked = match raster.stroke {
Some(width) if width.is_finite() && width > 0.0 => Some((width, StrokeKind::Stroke)),
_ if raster.embolden > 0.0 => Some((raster.embolden, StrokeKind::StrokeAndFill)),
_ => None,
};
match stroked {
Some((width, kind)) => stroked_mask(&outline, width, kind),
None => filled_mask(&outline, rasterizer),
}
}
fn filled_mask(outline: &GlyphOutline, rasterizer: &mut Rasterizer) -> Option<GlyphMask> {
let (width, height) = (usize_of(outline.width()), usize_of(outline.height()));
if width == 0 || height == 0 {
return None;
}
let mut alpha = vec![0u8; width * height];
outline.draw(rasterizer, |x, y, coverage| {
if let Some(pixel) = alpha.get_mut(usize_of(y) * width + usize_of(x)) {
*pixel = unit_to_u8(coverage);
}
});
Some(GlyphMask {
alpha: alpha.into(),
width,
height,
left: outline.left(),
top: outline.top(),
})
}
#[derive(Clone, Copy)]
enum StrokeKind {
Stroke,
StrokeAndFill,
}
fn stroked_mask(outline: &GlyphOutline, width: f32, kind: StrokeKind) -> Option<GlyphMask> {
let pad = whole::to_i32(whole::ceil(width * 0.5 * STROKE_MITER_LIMIT)).max(1) + 1;
let raster_width = outline.width() + 2 * u32::try_from(pad).ok()?;
let raster_height = outline.height() + 2 * u32::try_from(pad).ok()?;
let mut pixmap = Pixmap::new(raster_width, raster_height)?;
let path = outline_path(outline, f32_of(pad))?;
let mut paint = Paint::default();
paint.set_color_rgba8(255, 255, 255, 255);
paint.anti_alias = true;
if let StrokeKind::StrokeAndFill = kind {
pixmap.fill_path(
&path,
&paint,
tiny_skia::FillRule::Winding,
Transform::identity(),
None,
);
}
let stroke = Stroke {
width,
line_cap: LineCap::Butt,
line_join: LineJoin::Miter,
miter_limit: STROKE_MITER_LIMIT,
..Stroke::default()
};
pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
let alpha: Arc<[u8]> = pixmap
.data()
.as_chunks::<4>()
.0
.iter()
.map(|pixel| pixel[3])
.collect();
Some(GlyphMask {
alpha,
width: usize_of(raster_width),
height: usize_of(raster_height),
left: outline.left() - pad,
top: outline.top() - pad,
})
}
fn outline_path(outline: &GlyphOutline, pad: f32) -> Option<tiny_skia::Path> {
let mut builder = PathBuilder::new();
let mut pen = None;
let moved = |point: cranpose_ui_graphics::Point| (point.x + pad, point.y + pad);
for segment in outline.segments() {
let (start, end) = match segment {
OutlineSegment::Line(a, b) | OutlineSegment::Quad(a, _, b) => (a, b),
OutlineSegment::Cubic(a, _, _, b) => (a, b),
};
if pen != Some(start) {
if pen.is_some() {
builder.close();
}
let (x, y) = moved(start);
builder.move_to(x, y);
}
match segment {
OutlineSegment::Line(_, b) => {
let (x, y) = moved(b);
builder.line_to(x, y);
}
OutlineSegment::Quad(_, c, b) => {
let ((cx, cy), (x, y)) = (moved(c), moved(b));
builder.quad_to(cx, cy, x, y);
}
OutlineSegment::Cubic(_, c0, c1, b) => {
let ((ax, ay), (bx, by), (x, y)) = (moved(c0), moved(c1), moved(b));
builder.cubic_to(ax, ay, bx, by, x, y);
}
}
pen = Some(end);
}
builder.close();
builder.finish()
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct GlyphMaskCacheStats {
pub entries: usize,
pub hits: u64,
pub misses: u64,
}
pub struct GlyphMaskCache {
masks: BoundedLruCache<GlyphMaskKey, Option<GlyphMask>>,
rasterizer: Rasterizer,
lookups: u64,
misses: u64,
}
impl GlyphMaskCache {
pub fn with_capacity_at_least_one(capacity: usize) -> Self {
Self {
masks: BoundedLruCache::with_capacity_at_least_one(capacity),
rasterizer: Rasterizer::new(0, 0),
lookups: 0,
misses: 0,
}
}
pub fn stats(&self) -> GlyphMaskCacheStats {
GlyphMaskCacheStats {
entries: self.masks.len(),
hits: self.lookups - self.misses,
misses: self.misses,
}
}
pub fn mask(
&mut self,
typeface: &Typeface,
glyph: GlyphId,
size: f32,
raster: GlyphRaster,
) -> Option<&GlyphMask> {
let key = GlyphMaskKey::of(typeface, glyph, size, raster);
let Self {
masks,
rasterizer,
lookups,
misses,
} = self;
*lookups = lookups.saturating_add(1);
masks
.find_or_insert_with(key, || {
drawn_mask(typeface, glyph, (size, raster), (rasterizer, misses))
})
.as_ref()
}
pub fn cached(&mut self, key: &GlyphMaskKey) -> Option<&GlyphMask> {
self.masks.get(key)?.as_ref()
}
}
#[inline(never)]
fn drawn_mask(
typeface: &Typeface,
glyph: GlyphId,
(size, raster): (f32, GlyphRaster),
(rasterizer, misses): (&mut Rasterizer, &mut u64),
) -> Option<GlyphMask> {
*misses = misses.saturating_add(1);
rasterize_glyph(typeface, glyph, size, raster, rasterizer)
}
fn usize_of(value: u32) -> usize {
usize::try_from(value).unwrap_or(0)
}
fn f32_of(value: i32) -> f32 {
f32::from(i16::try_from(value).unwrap_or(i16::MAX))
}