use crate::parser::ttf::TtfFont;
use crate::text::ShapedGlyph;
use resvg::tiny_skia;
use skrifa::MetadataProvider as _;
use skrifa::instance::{LocationRef, Size};
use skrifa::outline::{
DrawSettings, Engine, GlyphStyles, HintingInstance, HintingOptions, OutlinePen, SmoothMode,
Target,
};
use crate::render::raster_pixels::DevicePixelVector;
const DEVICE_QUANTIZATION_TIE_TOLERANCE: f32 = 0.001;
struct GlyphPathPen<'a> {
builder: &'a mut tiny_skia::PathBuilder,
pen_x: f32,
baseline_y: f32,
shear: f32,
}
#[derive(Debug, Clone, Copy)]
pub(super) struct FoundationGlyphPositioning {
requested_origin: DevicePixelVector,
quantized_origin: DevicePixelVector,
}
impl FoundationGlyphPositioning {
pub(super) fn new(requested_origin: DevicePixelVector) -> Option<Self> {
if !requested_origin.x.is_finite() || !requested_origin.y.is_finite() {
return None;
}
Some(Self {
requested_origin,
quantized_origin: DevicePixelVector::new(
quantize_inline(requested_origin.x),
quantize_baseline(requested_origin.y),
),
})
}
pub(super) const fn origin(self) -> DevicePixelVector {
self.quantized_origin
}
fn glyph_origin(self, inline_offset: f32, baseline_offset: f32) -> DevicePixelVector {
DevicePixelVector::new(
quantize_inline(self.requested_origin.x + inline_offset) - self.quantized_origin.x,
quantize_baseline(self.requested_origin.y + baseline_offset) - self.quantized_origin.y,
)
}
}
fn quantize_inline(value: f32) -> f32 {
((value + 0.125 + DEVICE_QUANTIZATION_TIE_TOLERANCE) * 4.0).floor() * 0.25
}
fn quantize_baseline(value: f32) -> f32 {
(value + 0.5 + DEVICE_QUANTIZATION_TIE_TOLERANCE).floor()
}
pub(super) struct FoundationGlyphOutline(tiny_skia::Path);
impl FoundationGlyphOutline {
pub(super) fn embolden(self, stroke_width: f32) -> Option<Self> {
if stroke_width <= 0.0 {
return Some(self);
}
let stroke = tiny_skia::Stroke {
width: stroke_width,
..Default::default()
};
let stroked = tiny_skia::PathStroker::new().stroke(&self.0, &stroke, 1.0)?;
let mut combined = tiny_skia::PathBuilder::new();
combined.push_path(&self.0);
combined.push_path(&stroked);
combined.finish().map(Self)
}
pub(super) fn bounds(&self) -> crate::types::Rect {
let bounds = self.0.bounds();
crate::types::Rect::from_xywh(bounds.left(), bounds.top(), bounds.width(), bounds.height())
}
pub(super) fn rasterize(&self, frame: &super::GlyphMaskFrame) -> Option<image::GrayImage> {
let width = frame.dimensions.width;
let height = frame.dimensions.height;
let mut pixmap = tiny_skia::Pixmap::new(width, height)?;
let transform = tiny_skia::Transform::from_translate(
frame.baseline_in_mask.x,
frame.baseline_in_mask.y,
);
let mut paint = tiny_skia::Paint::default();
paint.set_color(tiny_skia::Color::WHITE);
paint.anti_alias = true;
pixmap.fill_path(
&self.0,
&paint,
tiny_skia::FillRule::Winding,
transform,
None,
);
let mut mask = image::GrayImage::new(width, height);
for (index, pixel) in pixmap.pixels().iter().enumerate() {
let x = index as u32 % width;
let y = index as u32 / width;
mask.put_pixel(x, y, image::Luma([pixel.alpha()]));
}
Some(mask)
}
}
impl GlyphPathPen<'_> {
fn point(&self, x: f32, y: f32) -> (f32, f32) {
(self.pen_x + x + self.shear * y, self.baseline_y - y)
}
}
impl OutlinePen for GlyphPathPen<'_> {
fn move_to(&mut self, x: f32, y: f32) {
let (x, y) = self.point(x, y);
self.builder.move_to(x, y);
}
fn line_to(&mut self, x: f32, y: f32) {
let (x, y) = self.point(x, y);
self.builder.line_to(x, y);
}
fn quad_to(&mut self, control_x: f32, control_y: f32, x: f32, y: f32) {
let (control_x, control_y) = self.point(control_x, control_y);
let (x, y) = self.point(x, y);
self.builder.quad_to(control_x, control_y, x, y);
}
fn curve_to(
&mut self,
first_x: f32,
first_y: f32,
second_x: f32,
second_y: f32,
x: f32,
y: f32,
) {
let (first_x, first_y) = self.point(first_x, first_y);
let (second_x, second_y) = self.point(second_x, second_y);
let (x, y) = self.point(x, y);
self.builder
.cubic_to(first_x, first_y, second_x, second_y, x, y);
}
fn close(&mut self) {
self.builder.close();
}
}
pub(super) fn foundation_run_outline(
font: &TtfFont,
size_px: f32,
glyphs: &[ShapedGlyph],
points_to_pixels: f32,
shear: f32,
positioning: FoundationGlyphPositioning,
) -> Option<FoundationGlyphOutline> {
if !size_px.is_finite() || size_px <= 0.0 {
return None;
}
let font_ref = skrifa::FontRef::from_index(&font.data, font.face_index.get()).ok()?;
let outlines = font_ref.outline_glyphs();
let hinting = (shear == 0.0)
.then(|| {
HintingInstance::new(
&outlines,
Size::new(size_px),
LocationRef::default(),
HintingOptions {
engine: Engine::Auto(Some(GlyphStyles::new(&outlines))),
target: Target::from(SmoothMode::Light),
},
)
.ok()
})
.flatten();
let mut builder = tiny_skia::PathBuilder::new();
let mut pen_x = 0.0;
for shaped in glyphs {
let glyph = outlines.get(skrifa::GlyphId::new(u32::from(shaped.glyph_id)))?;
let glyph_origin = positioning.glyph_origin(
pen_x + shaped.x_offset * points_to_pixels,
-shaped.y_offset * points_to_pixels,
);
let mut pen = GlyphPathPen {
builder: &mut builder,
pen_x: glyph_origin.x,
baseline_y: glyph_origin.y,
shear,
};
let settings = hinting.as_ref().map_or_else(
|| DrawSettings::unhinted(Size::new(size_px), LocationRef::default()),
|instance| DrawSettings::hinted(instance, false),
);
glyph.draw(settings, &mut pen).ok()?;
pen_x += shaped.x_advance * points_to_pixels;
}
builder.finish().map(FoundationGlyphOutline)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn foundation_positioning_quantizes_each_device_axis_like_skia() {
let positioning = FoundationGlyphPositioning::new(DevicePixelVector::new(62.3, 212.2))
.expect("finite device position");
assert_eq!(positioning.origin(), DevicePixelVector::new(62.25, 212.0));
assert_eq!(
positioning.glyph_origin(10.2, 0.6),
DevicePixelVector::new(10.25, 1.0)
);
}
#[test]
fn foundation_positioning_preserves_half_sample_ties_despite_float_noise() {
let positioning =
FoundationGlyphPositioning::new(DevicePixelVector::new(62.499_98, 212.499_98))
.expect("finite device position");
assert_eq!(positioning.origin(), DevicePixelVector::new(62.5, 213.0));
assert_eq!(
positioning.glyph_origin(0.0, 0.0),
DevicePixelVector::new(0.0, 0.0)
);
}
}