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
48#[derive(Default)]
54pub struct Rasterizer {
55 context: swash::scale::ScaleContext,
56 stroker: tiny_skia::PathStroker,
57}
58
59impl Rasterizer {
60 pub fn new() -> Self {
62 Self::default()
63 }
64
65 pub fn alpha(&mut self, font: &Font, glyph: u32, px: f32, dx: f32) -> Option<GlyphImage> {
72 let image = self.render(font, glyph, px, dx)?;
73 Some(GlyphImage {
74 width: image.placement.width,
75 height: image.placement.height,
76 left: image.placement.left,
77 top: image.placement.top,
78 data: image.data,
79 })
80 }
81
82 pub fn stroked(
87 &mut self,
88 font: &Font,
89 glyph: u32,
90 px: f32,
91 dx: f32,
92 stroke: &GlyphStroke,
93 ) -> Option<GlyphImage> {
94 let outline = glyph_outline(font, glyph, px, dx)?;
95 let stroked = self.stroker.stroke(&outline, &skia_stroke(stroke), 1.0)?;
96 mask_of(&stroked)
97 }
98
99 pub fn sdf(&mut self, font: &Font, glyph: u32, px: f32) -> Option<GlyphImage> {
106 let alpha = self.render(font, glyph, px, 0.0)?;
107 let pad = SDF_PAD;
108 let w = alpha.placement.width + 2 * pad;
109 let h = alpha.placement.height + 2 * pad;
110 let mut coverage = vec![0u8; (w * h) as usize];
111 for y in 0..alpha.placement.height {
112 for x in 0..alpha.placement.width {
113 coverage[((y + pad) * w + x + pad) as usize] =
114 alpha.data[(y * alpha.placement.width + x) as usize];
115 }
116 }
117 let field = crate::sdf::signed_distances(&coverage, w as usize, h as usize);
118 Some(GlyphImage {
119 width: w,
120 height: h,
121 left: alpha.placement.left - pad as i32,
122 top: alpha.placement.top + pad as i32,
123 data: crate::sdf::encode(&field, SDF_PAD as f32),
124 })
125 }
126
127 pub fn color(&mut self, font: &Font, glyph: u32, px: f32) -> Option<GlyphImage> {
133 let font_ref = swash::FontRef::from_index(font.data(), font.face_index() as usize)?;
134 let mut scaler = self
135 .context
136 .builder(font_ref)
137 .size(px)
138 .hint(false)
139 .variations(swash_variations(font))
140 .build();
141 let image = swash::scale::Render::new(&[
142 swash::scale::Source::ColorOutline(0),
143 swash::scale::Source::ColorBitmap(swash::scale::StrikeWith::BestFit),
144 ])
145 .render(&mut scaler, glyph as swash::GlyphId);
146 let Some(image) = image else {
147 return crate::colr::raster(font, glyph, px);
150 };
151 if image.content != swash::scale::image::Content::Color {
152 return crate::colr::raster(font, glyph, px);
153 }
154 let mut data = image.data;
155 for px in data.chunks_exact_mut(4) {
156 let a = px[3] as u32;
157 px[0] = ((px[0] as u32 * a + 127) / 255) as u8;
160 px[1] = ((px[1] as u32 * a + 127) / 255) as u8;
161 px[2] = ((px[2] as u32 * a + 127) / 255) as u8;
162 }
163 Some(GlyphImage {
164 width: image.placement.width,
165 height: image.placement.height,
166 left: image.placement.left,
167 top: image.placement.top,
168 data,
169 })
170 }
171
172 pub(crate) fn color_bounds(&mut self, font: &Font, glyph: u32, px: f32) -> Option<Rect> {
176 let image = self.color(font, glyph, px)?;
177 let mut left = image.width;
178 let mut top = image.height;
179 let mut right = 0;
180 let mut bottom = 0;
181 for y in 0..image.height {
182 for x in 0..image.width {
183 let alpha = image.data[((y * image.width + x) * 4 + 3) as usize];
184 if alpha == 0 {
185 continue;
186 }
187 left = left.min(x);
188 top = top.min(y);
189 right = right.max(x + 1);
190 bottom = bottom.max(y + 1);
191 }
192 }
193 (left < right && top < bottom).then(|| {
194 Rect::new(
195 image.left as f32 + left as f32,
196 -image.top as f32 + top as f32,
197 (right - left) as f32,
198 (bottom - top) as f32,
199 )
200 })
201 }
202
203 fn render(
204 &mut self,
205 font: &Font,
206 glyph: u32,
207 px: f32,
208 dx: f32,
209 ) -> Option<swash::scale::image::Image> {
210 let font_ref = swash::FontRef::from_index(font.data(), font.face_index() as usize)?;
211 let mut scaler = self
212 .context
213 .builder(font_ref)
214 .size(px)
215 .hint(false)
216 .variations(swash_variations(font))
217 .build();
218 swash::scale::Render::new(&[swash::scale::Source::Outline])
219 .offset(swash::zeno::Vector::new(dx, 0.0))
220 .render(&mut scaler, glyph as swash::GlyphId)
221 }
222}
223
224fn skia_stroke(stroke: &GlyphStroke) -> tiny_skia::Stroke {
225 tiny_skia::Stroke {
226 width: stroke.width,
227 miter_limit: stroke.miter_limit,
228 line_cap: match stroke.cap {
229 Cap::Butt => tiny_skia::LineCap::Butt,
230 Cap::Round => tiny_skia::LineCap::Round,
231 Cap::Square => tiny_skia::LineCap::Square,
232 },
233 line_join: match stroke.join {
234 Join::Miter => tiny_skia::LineJoin::Miter,
235 Join::Round => tiny_skia::LineJoin::Round,
236 Join::Bevel => tiny_skia::LineJoin::Bevel,
237 },
238 dash: None,
239 }
240}
241
242fn glyph_outline(font: &Font, glyph: u32, px: f32, dx: f32) -> Option<tiny_skia::Path> {
246 let font_ref = skrifa::FontRef::from_index(font.data(), font.face_index()).ok()?;
247 let outline = font_ref.outline_glyphs().get(skrifa::GlyphId::new(glyph))?;
248 let mut pen = TsPathPen::default();
249 outline
250 .draw(
251 DrawSettings::unhinted(Size::new(px), font.variation_location()),
252 &mut pen,
253 )
254 .ok()?;
255 pen.builder
256 .finish()?
257 .transform(tiny_skia::Transform::from_row(1.0, 0.0, 0.0, -1.0, dx, 0.0))
258}
259
260fn mask_of(path: &tiny_skia::Path) -> Option<GlyphImage> {
265 let bounds = path.compute_tight_bounds()?;
266 let (left, top) = (bounds.left().floor(), bounds.top().floor());
267 let width = (bounds.right().ceil() - left) as u32;
268 let height = (bounds.bottom().ceil() - top) as u32;
269 let mut mask = tiny_skia::Mask::new(width, height)?;
270 mask.fill_path(
271 path,
272 tiny_skia::FillRule::Winding,
273 true,
274 tiny_skia::Transform::from_translate(-left, -top),
275 );
276 Some(GlyphImage {
277 width,
278 height,
279 left: left as i32,
280 top: -top as i32,
281 data: mask.data().to_vec(),
282 })
283}
284
285pub fn glyph_path(font: &Font, glyph: u32, px: f32) -> Option<Arc<Path>> {
291 let font_ref = skrifa::FontRef::from_index(font.data(), font.face_index()).ok()?;
292 let outline = font_ref.outline_glyphs().get(skrifa::GlyphId::new(glyph))?;
293 let mut pen = PathPen {
294 builder: PathBuilder::new(),
295 };
296 outline
297 .draw(
298 DrawSettings::unhinted(Size::new(px), font.variation_location()),
299 &mut pen,
300 )
301 .ok()?;
302 let path = pen.builder.build();
303 (!path.is_empty()).then_some(path)
307}
308
309#[derive(Default)]
312pub(crate) struct TsPathPen {
313 pub(crate) builder: tiny_skia::PathBuilder,
314}
315
316impl OutlinePen for TsPathPen {
317 fn move_to(&mut self, x: f32, y: f32) {
318 self.builder.move_to(x, y);
319 }
320
321 fn line_to(&mut self, x: f32, y: f32) {
322 self.builder.line_to(x, y);
323 }
324
325 fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
326 self.builder.quad_to(cx, cy, x, y);
327 }
328
329 fn curve_to(&mut self, c0x: f32, c0y: f32, c1x: f32, c1y: f32, x: f32, y: f32) {
330 self.builder.cubic_to(c0x, c0y, c1x, c1y, x, y);
331 }
332
333 fn close(&mut self) {
334 self.builder.close();
335 }
336}
337
338struct PathPen {
340 builder: PathBuilder,
341}
342
343impl OutlinePen for PathPen {
344 fn move_to(&mut self, x: f32, y: f32) {
345 self.builder.move_to((x, -y));
346 }
347
348 fn line_to(&mut self, x: f32, y: f32) {
349 self.builder.line_to((x, -y));
350 }
351
352 fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
353 self.builder.quad_to((cx, -cy), (x, -y));
354 }
355
356 fn curve_to(&mut self, c0x: f32, c0y: f32, c1x: f32, c1y: f32, x: f32, y: f32) {
357 self.builder.cubic_to((c0x, -c0y), (c1x, -c1y), (x, -y));
358 }
359
360 fn close(&mut self) {
361 self.builder.close();
362 }
363}
364
365fn swash_variations(font: &Font) -> impl Iterator<Item = (&str, f32)> + '_ {
368 font.variation_coordinates()
369 .iter()
370 .filter_map(|(tag, value)| Some((std::str::from_utf8(tag).ok()?, *value)))
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use crate::font::FaceSet;
377
378 fn fira() -> FaceSet {
379 let path = concat!(
380 env!("CARGO_MANIFEST_DIR"),
381 "/../../assets/fonts/fira_sans.ttf"
382 );
383 let mut c = FaceSet::default();
384 c.register("Fira Sans", std::fs::read(path).unwrap())
385 .unwrap();
386 c
387 }
388
389 #[test]
395 fn stroked_raster_bounds_hold_the_miter_spikes() {
396 let fonts = fira();
397 let font = fonts.family("Fira Sans").unwrap();
398 let mut raster = Rasterizer::new();
399 let stroke = GlyphStroke {
400 width: 5.0,
401 cap: Cap::Butt,
402 join: Join::Miter,
403 miter_limit: 16.0,
404 };
405 let glyph = fonts.get(font).glyph_for('M').unwrap();
406 let fill = raster.alpha(fonts.get(font), glyph, 72.0, 0.0).unwrap();
407 let stroked = raster
408 .stroked(fonts.get(font), glyph, 72.0, 0.0, &stroke)
409 .unwrap();
410 let reach = (stroked.top - fill.top) as f32;
411 assert!(
412 reach > stroke.width,
413 "the stroked cell reaches only {reach}px above the fill — a \
414 miter spike of 8.6px does not fit"
415 );
416
417 let bevelled = raster
421 .stroked(
422 fonts.get(font),
423 glyph,
424 72.0,
425 0.0,
426 &GlyphStroke {
427 join: Join::Bevel,
428 ..stroke
429 },
430 )
431 .unwrap();
432 assert!(
433 bevelled.top < stroked.top,
434 "bevel {} vs miter {}",
435 bevelled.top,
436 stroked.top
437 );
438 }
439
440 #[test]
445 fn sdf_and_mask_tiers_agree_on_placement() {
446 let fonts = fira();
447 let font = fonts.family("Fira Sans").unwrap();
448 let mut raster = Rasterizer::new();
449 for ch in ['H', 'g', 'x', 'Q'] {
450 let glyph = fonts.get(font).glyph_for(ch).unwrap();
451 let alpha = raster.alpha(fonts.get(font), glyph, 64.0, 0.0).unwrap();
452 let sdf = raster.sdf(fonts.get(font), glyph, 64.0).unwrap();
453 let pad = SDF_PAD as i32;
454 for (axis, a, s) in [
455 ("top", alpha.top, sdf.top - pad),
456 ("left", alpha.left, sdf.left + pad),
457 ] {
458 assert!(
459 (a - s).abs() <= 1,
460 "'{ch}' {axis}: mask {a} vs sdf-adjusted {s}"
461 );
462 }
463 }
464 }
465}