1use std::sync::Arc;
2
3use skrifa::instance::Size;
4use skrifa::outline::{DrawSettings, OutlinePen};
5use skrifa::MetadataProvider;
6use valo_geometry::{Cap, Join, Path, PathBuilder, Rect};
7
8use crate::font::Font;
9
10pub const SDF_PAD: u32 = 8;
14
15#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct GlyphStroke {
20 pub width: f32,
22 pub cap: Cap,
24 pub join: Join,
26 pub miter_limit: f32,
28}
29
30pub struct GlyphImage {
36 pub width: u32,
38 pub height: u32,
40 pub left: i32,
42 pub top: i32,
44 pub data: Vec<u8>,
46}
47
48pub trait GlyphRaster: Send {
58 fn alpha(&mut self, font: &Font, glyph: u32, px: f32, dx: f32) -> Option<GlyphImage>;
60
61 fn stroked(
63 &mut self,
64 font: &Font,
65 glyph: u32,
66 px: f32,
67 dx: f32,
68 stroke: &GlyphStroke,
69 ) -> Option<GlyphImage>;
70
71 fn sdf(&mut self, font: &Font, glyph: u32, px: f32) -> Option<GlyphImage>;
73
74 fn color(&mut self, font: &Font, glyph: u32, px: f32) -> Option<GlyphImage>;
76
77 fn path(&mut self, font: &Font, glyph: u32, px: f32) -> Option<Arc<Path>>;
79}
80
81impl GlyphRaster for Rasterizer {
82 fn alpha(&mut self, font: &Font, glyph: u32, px: f32, dx: f32) -> Option<GlyphImage> {
83 Rasterizer::alpha(self, font, glyph, px, dx)
84 }
85
86 fn stroked(
87 &mut self,
88 font: &Font,
89 glyph: u32,
90 px: f32,
91 dx: f32,
92 stroke: &GlyphStroke,
93 ) -> Option<GlyphImage> {
94 Rasterizer::stroked(self, font, glyph, px, dx, stroke)
95 }
96
97 fn sdf(&mut self, font: &Font, glyph: u32, px: f32) -> Option<GlyphImage> {
98 Rasterizer::sdf(self, font, glyph, px)
99 }
100
101 fn color(&mut self, font: &Font, glyph: u32, px: f32) -> Option<GlyphImage> {
102 Rasterizer::color(self, font, glyph, px)
103 }
104
105 fn path(&mut self, font: &Font, glyph: u32, px: f32) -> Option<Arc<Path>> {
106 glyph_path(font, glyph, px)
107 }
108}
109
110#[derive(Default)]
116pub struct Rasterizer {
117 context: swash::scale::ScaleContext,
118 stroker: tiny_skia::PathStroker,
119}
120
121impl Rasterizer {
122 pub fn new() -> Self {
124 Self::default()
125 }
126
127 pub fn alpha(&mut self, font: &Font, glyph: u32, px: f32, dx: f32) -> Option<GlyphImage> {
134 let image = self.render(font, glyph, px, dx)?;
135 Some(GlyphImage {
136 width: image.placement.width,
137 height: image.placement.height,
138 left: image.placement.left,
139 top: image.placement.top,
140 data: image.data,
141 })
142 }
143
144 pub fn stroked(
149 &mut self,
150 font: &Font,
151 glyph: u32,
152 px: f32,
153 dx: f32,
154 stroke: &GlyphStroke,
155 ) -> Option<GlyphImage> {
156 let outline = glyph_outline(font, glyph, px, dx)?;
157 let stroked = self.stroker.stroke(&outline, &skia_stroke(stroke), 1.0)?;
158 mask_of(&stroked)
159 }
160
161 pub fn sdf(&mut self, font: &Font, glyph: u32, px: f32) -> Option<GlyphImage> {
168 let alpha = self.render(font, glyph, px, 0.0)?;
169 let pad = SDF_PAD;
170 let w = alpha.placement.width + 2 * pad;
171 let h = alpha.placement.height + 2 * pad;
172 let mut coverage = vec![0u8; (w * h) as usize];
173 for y in 0..alpha.placement.height {
174 for x in 0..alpha.placement.width {
175 coverage[((y + pad) * w + x + pad) as usize] =
176 alpha.data[(y * alpha.placement.width + x) as usize];
177 }
178 }
179 let field = crate::sdf::signed_distances(&coverage, w as usize, h as usize);
180 Some(GlyphImage {
181 width: w,
182 height: h,
183 left: alpha.placement.left - pad as i32,
184 top: alpha.placement.top + pad as i32,
185 data: crate::sdf::encode(&field, SDF_PAD as f32),
186 })
187 }
188
189 pub fn color(&mut self, font: &Font, glyph: u32, px: f32) -> Option<GlyphImage> {
195 let font_ref = swash::FontRef::from_index(font.data(), font.face_index() as usize)?;
196 let mut scaler = self
197 .context
198 .builder(font_ref)
199 .size(px)
200 .hint(false)
201 .variations(swash_variations(font))
202 .build();
203 let image = swash::scale::Render::new(&[
204 swash::scale::Source::ColorOutline(0),
205 swash::scale::Source::ColorBitmap(swash::scale::StrikeWith::BestFit),
206 ])
207 .render(&mut scaler, glyph as swash::GlyphId);
208 let Some(image) = image else {
209 return crate::colr::raster(font, glyph, px);
212 };
213 if image.content != swash::scale::image::Content::Color {
214 return crate::colr::raster(font, glyph, px);
215 }
216 let mut data = image.data;
217 for px in data.chunks_exact_mut(4) {
218 let a = px[3] as u32;
219 px[0] = ((px[0] as u32 * a + 127) / 255) as u8;
222 px[1] = ((px[1] as u32 * a + 127) / 255) as u8;
223 px[2] = ((px[2] as u32 * a + 127) / 255) as u8;
224 }
225 Some(GlyphImage {
226 width: image.placement.width,
227 height: image.placement.height,
228 left: image.placement.left,
229 top: image.placement.top,
230 data,
231 })
232 }
233
234 pub(crate) fn color_bounds(&mut self, font: &Font, glyph: u32, px: f32) -> Option<Rect> {
238 let image = self.color(font, glyph, px)?;
239 let mut left = image.width;
240 let mut top = image.height;
241 let mut right = 0;
242 let mut bottom = 0;
243 for y in 0..image.height {
244 for x in 0..image.width {
245 let alpha = image.data[((y * image.width + x) * 4 + 3) as usize];
246 if alpha == 0 {
247 continue;
248 }
249 left = left.min(x);
250 top = top.min(y);
251 right = right.max(x + 1);
252 bottom = bottom.max(y + 1);
253 }
254 }
255 (left < right && top < bottom).then(|| {
256 Rect::new(
257 image.left as f32 + left as f32,
258 -image.top as f32 + top as f32,
259 (right - left) as f32,
260 (bottom - top) as f32,
261 )
262 })
263 }
264
265 fn render(
266 &mut self,
267 font: &Font,
268 glyph: u32,
269 px: f32,
270 dx: f32,
271 ) -> Option<swash::scale::image::Image> {
272 let font_ref = swash::FontRef::from_index(font.data(), font.face_index() as usize)?;
273 let mut scaler = self
274 .context
275 .builder(font_ref)
276 .size(px)
277 .hint(false)
278 .variations(swash_variations(font))
279 .build();
280 swash::scale::Render::new(&[swash::scale::Source::Outline])
281 .offset(swash::zeno::Vector::new(dx, 0.0))
282 .render(&mut scaler, glyph as swash::GlyphId)
283 }
284}
285
286fn skia_stroke(stroke: &GlyphStroke) -> tiny_skia::Stroke {
287 tiny_skia::Stroke {
288 width: stroke.width,
289 miter_limit: stroke.miter_limit,
290 line_cap: match stroke.cap {
291 Cap::Butt => tiny_skia::LineCap::Butt,
292 Cap::Round => tiny_skia::LineCap::Round,
293 Cap::Square => tiny_skia::LineCap::Square,
294 },
295 line_join: match stroke.join {
296 Join::Miter => tiny_skia::LineJoin::Miter,
297 Join::Round => tiny_skia::LineJoin::Round,
298 Join::Bevel => tiny_skia::LineJoin::Bevel,
299 },
300 dash: None,
301 }
302}
303
304fn glyph_outline(font: &Font, glyph: u32, px: f32, dx: f32) -> Option<tiny_skia::Path> {
308 let font_ref = skrifa::FontRef::from_index(font.data(), font.face_index()).ok()?;
309 let outline = font_ref.outline_glyphs().get(skrifa::GlyphId::new(glyph))?;
310 let mut pen = TsPathPen::default();
311 outline
312 .draw(
313 DrawSettings::unhinted(Size::new(px), font.variation_location()),
314 &mut pen,
315 )
316 .ok()?;
317 pen.builder
318 .finish()?
319 .transform(tiny_skia::Transform::from_row(1.0, 0.0, 0.0, -1.0, dx, 0.0))
320}
321
322fn mask_of(path: &tiny_skia::Path) -> Option<GlyphImage> {
327 let bounds = path.compute_tight_bounds()?;
328 let (left, top) = (bounds.left().floor(), bounds.top().floor());
329 let width = (bounds.right().ceil() - left) as u32;
330 let height = (bounds.bottom().ceil() - top) as u32;
331 let mut mask = tiny_skia::Mask::new(width, height)?;
332 mask.fill_path(
333 path,
334 tiny_skia::FillRule::Winding,
335 true,
336 tiny_skia::Transform::from_translate(-left, -top),
337 );
338 Some(GlyphImage {
339 width,
340 height,
341 left: left as i32,
342 top: -top as i32,
343 data: mask.data().to_vec(),
344 })
345}
346
347pub fn glyph_path(font: &Font, glyph: u32, px: f32) -> Option<Arc<Path>> {
353 let font_ref = skrifa::FontRef::from_index(font.data(), font.face_index()).ok()?;
354 let outline = font_ref.outline_glyphs().get(skrifa::GlyphId::new(glyph))?;
355 let mut pen = PathPen {
356 builder: PathBuilder::new(),
357 };
358 outline
359 .draw(
360 DrawSettings::unhinted(Size::new(px), font.variation_location()),
361 &mut pen,
362 )
363 .ok()?;
364 let path = pen.builder.build();
365 (!path.is_empty()).then_some(path)
369}
370
371#[derive(Default)]
374pub(crate) struct TsPathPen {
375 pub(crate) builder: tiny_skia::PathBuilder,
376}
377
378impl OutlinePen for TsPathPen {
379 fn move_to(&mut self, x: f32, y: f32) {
380 self.builder.move_to(x, y);
381 }
382
383 fn line_to(&mut self, x: f32, y: f32) {
384 self.builder.line_to(x, y);
385 }
386
387 fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
388 self.builder.quad_to(cx, cy, x, y);
389 }
390
391 fn curve_to(&mut self, c0x: f32, c0y: f32, c1x: f32, c1y: f32, x: f32, y: f32) {
392 self.builder.cubic_to(c0x, c0y, c1x, c1y, x, y);
393 }
394
395 fn close(&mut self) {
396 self.builder.close();
397 }
398}
399
400struct PathPen {
402 builder: PathBuilder,
403}
404
405impl OutlinePen for PathPen {
406 fn move_to(&mut self, x: f32, y: f32) {
407 self.builder.move_to((x, -y));
408 }
409
410 fn line_to(&mut self, x: f32, y: f32) {
411 self.builder.line_to((x, -y));
412 }
413
414 fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
415 self.builder.quad_to((cx, -cy), (x, -y));
416 }
417
418 fn curve_to(&mut self, c0x: f32, c0y: f32, c1x: f32, c1y: f32, x: f32, y: f32) {
419 self.builder.cubic_to((c0x, -c0y), (c1x, -c1y), (x, -y));
420 }
421
422 fn close(&mut self) {
423 self.builder.close();
424 }
425}
426
427fn swash_variations(font: &Font) -> impl Iterator<Item = (&str, f32)> + '_ {
430 font.variation_coordinates()
431 .iter()
432 .filter_map(|(tag, value)| Some((std::str::from_utf8(tag).ok()?, *value)))
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::font::FaceSet;
439
440 fn fira() -> FaceSet {
441 let path = concat!(
442 env!("CARGO_MANIFEST_DIR"),
443 "/../../assets/fonts/fira_sans.ttf"
444 );
445 let mut c = FaceSet::default();
446 c.register("Fira Sans", std::fs::read(path).unwrap())
447 .unwrap();
448 c
449 }
450
451 #[test]
457 fn stroked_raster_bounds_hold_the_miter_spikes() {
458 let fonts = fira();
459 let font = fonts.family("Fira Sans").unwrap();
460 let mut raster = Rasterizer::new();
461 let stroke = GlyphStroke {
462 width: 5.0,
463 cap: Cap::Butt,
464 join: Join::Miter,
465 miter_limit: 16.0,
466 };
467 let glyph = fonts.get(font).glyph_for('M').unwrap();
468 let fill = raster.alpha(fonts.get(font), glyph, 72.0, 0.0).unwrap();
469 let stroked = raster
470 .stroked(fonts.get(font), glyph, 72.0, 0.0, &stroke)
471 .unwrap();
472 let reach = (stroked.top - fill.top) as f32;
473 assert!(
474 reach > stroke.width,
475 "the stroked cell reaches only {reach}px above the fill — a \
476 miter spike of 8.6px does not fit"
477 );
478
479 let bevelled = raster
483 .stroked(
484 fonts.get(font),
485 glyph,
486 72.0,
487 0.0,
488 &GlyphStroke {
489 join: Join::Bevel,
490 ..stroke
491 },
492 )
493 .unwrap();
494 assert!(
495 bevelled.top < stroked.top,
496 "bevel {} vs miter {}",
497 bevelled.top,
498 stroked.top
499 );
500 }
501
502 #[test]
507 fn sdf_and_mask_tiers_agree_on_placement() {
508 let fonts = fira();
509 let font = fonts.family("Fira Sans").unwrap();
510 let mut raster = Rasterizer::new();
511 for ch in ['H', 'g', 'x', 'Q'] {
512 let glyph = fonts.get(font).glyph_for(ch).unwrap();
513 let alpha = raster.alpha(fonts.get(font), glyph, 64.0, 0.0).unwrap();
514 let sdf = raster.sdf(fonts.get(font), glyph, 64.0).unwrap();
515 let pad = SDF_PAD as i32;
516 for (axis, a, s) in [
517 ("top", alpha.top, sdf.top - pad),
518 ("left", alpha.left, sdf.left + pad),
519 ] {
520 assert!(
521 (a - s).abs() <= 1,
522 "'{ch}' {axis}: mask {a} vs sdf-adjusted {s}"
523 );
524 }
525 }
526 }
527}